“我正在参与「启航方案」”

本文要点说下常用的本地数据库操作,Sqlite和封装的FMDB的运用,以及Model的存与取。

效果图

iOS中Sqlite和FMDB的详解

什么是数据库

  • 数据库(Database)是依照数据结构来安排、存储和办理数据的仓库
  • 数据库能够分为2大品种 :关系型数据库(主流)、目标型数据库

iOS中的数据存储方法

  • Plist(NSArray\NSDictionary),只能存储数组,字典,可是数组和字典里边不能有自定义目标
  • Preference(偏好设置\NSUserDefaults)
  • NSCoding(NSKeyedArchiver\NSkeyedUnarchiver)
  • SQLite3
  • Core Data (面临目标)

什么是SQLite

  • SQLite是一款轻型的嵌入式数据库
  • 它占用资源十分的低,在嵌入式设备中,或许只需要几百K的内存就够了
  • 它的处理速度比Mysql、PostgreSQL这两款著名的数据库都还快

调试软件运用的是Navicat,支持大部分主流数据库(包括SQLite)

SQL句子的品种

数据定义句子(DDL:Data Definition Language)
  • 包括create和drop等操作
  • 在数据库中创立新表或删去表(create table或 drop table)
数据操作句子(DML:Data Manipulation Language)
  • 包括insert(添加)、update(修正)、delete(删去)等操作
数据查询句子(DQL:Data Query Language)
  • 能够用于查询获得表中的数据
  • 关键字select是DQL(也是所有SQL)用得最多的操作
  • 其他DQL常用的关键字有where,order by,group by和having

字段类型

SQLite将数据划分为以下几种存储类型:

  • integer : 整型值
  • real : 浮点值
  • text : 文本字符串
  • blob : 二进制数据(比方文件,模型) 实践上SQLite是无类型的,就算声明为integer类型,仍是能存储字符串文本(主键在外),建表时声明啥类型或许不声明类型都能够,也就意味着创表句子能够这么写:create table t_student(name, age); 为了保持杰出的编程标准、便利程序员之间的交流,编写建表句子的时分最好加上每个字段的详细类型

Sqlite运用:

一、创立表

  • create table 表名 (字段名1 字段类型1, 字段名2 字段类型2, …) ;
  • create table if not exists 表名 (字段名1 字段类型1, 字段名2 字段类型2, …)(判别表是否已存在,不存在则创立) ;

示例:create table t_student (id integer, name text, age inetger, score real) ;

杰出的数据库编程标准应该要确保每条记载的唯一性,为此,添加了主键束缚,也便是说,每张表都有必要有一个主键,用来标识记载的唯一性,在创表的时分用primary key声明一个主键: 示例:create table t_student (id integer primary key, name text, age integer) ; 主键的规划准则:

  • 主键应当是对用户没有意义的
  • 永远也不要更新主键
  • 主键不该包括动态变化的数据
  • 主键应当由计算机主动生成

integer类型的id作为t_student表的主键。

  • 只需声明为primary key,就阐明是一个主键字段
  • 主键字段默许就包括了not null 和 unique 两个束缚

假如想要让主键主动添加(有必要是integer类型),应该添加autoincrement, 示例:create table t_student (id integer primary key autoincrement, name text, age integer) ;

二、删表

留意:这个删是将整个表删去

  • drop table 表名 ;
  • drop table if exists 表名 ;(判别表是否存在,存在则删去)

示例:drop table if exists t_student ;

三、增(刺进数据insert)

  • insert into 表名 (字段1, 字段2, …) values (字段1的值, 字段2的值, …) ;

示例:insert into t_student (name, age) values (‘小虎牙’, 10) ; 数据库中的字符串内容应该用单引号 ‘ ’ 括住

四、删(删去数据delete)

  • delete from 表名 ;

示例:delete from t_student ;

留意:上面的示例会将t_student表中所有记载都删掉

五、改(更新数据update)

  • update 表名 set 字段1 = 字段1的值, 字段2 = 字段2的值, … ;

示例:

update t_student set name = ‘rc’, age = 18 ; 

留意:上面的示例会将t_student表中所有记载的name都改为rc,age都改为18

六、查(查询数据select)

  • select 字段1, 字段2, … from 表名 ; (查询字段1, 字段2数据)
  • select * from 表名;(查询表中所有的字段)

示例 : select name, age from t_student ;
select * from t_student ;

七、条件句子

假如只想更新或许删去某些固定的记载,那就有必要在DML句子后加上一些条件 条件句子的常见格局

  • where 字段 = 某个值 ; (不能用两个 =)
  • where 字段 is 某个值 ; (is 相当于 =)
  • where 字段 != 某个值 ;
  • where 字段 is not 某个值 ; (is not 相当于 !=)
  • where 字段 > 某个值 ;
  • where 字段1 = 某个值 and 字段2 > 某个值 ; (and相当于C言语中的 &&)
  • where 字段1 = 某个值 or 字段2 = 某个值 ; (or 相当于C言语中的 ||)

示例:

  • 将t_student表中年纪大于10 并且 姓名不等于rc的记载,年纪都改为 5 update t_student set age = 5 where age > 10 and name != ‘rc’ ;
  • 删去t_student表中年纪小于等于10 或许 年纪大于30的记载 delete from t_student where age <= 10 or age > 30 ;
  • 将t_student表中姓名等于rc的记载,score字段的值 都改为 age字段的值 update t_student set score = age where name = ‘rc’ ;

八、起别号

格局:

  • select 字段1 别号 , 字段2 别号 , … from 表名 别号 ;
  • select 字段1 别号, 字段2 as 别号, … from 表名 as 别号 ;
  • select 别号.字段1, 别号.字段2, … from 表名 别号 ;

示例:

  • 给name起个叫做myname的别号,给age起个叫做myage的别号 select name myname, age myage from t_student ;
  • 给t_student表起个别号叫做s,利用s来引证表中的字段 select s.name, s.age from t_student s ;

九、计算记载的数量

  • select count (字段) from 表名 ;
  • select count ( * ) from 表名 ; 示例:
  • select count (age) from t_student ;
  • select count ( * ) from t_student where score >= 60;

十、排序

依照某个字段的值,进行排序查找 select * from t_student order by 字段 ; 示例:select * from t_student order by age ;

默许是依照升序排序(由小到大),也能够变为降序(由大到小) 降序 :select * from t_student order by age desc ; 升序(默许):select * from t_student order by age asc ;

用多个字段进行排序 先依照年纪排序(升序),年纪持平就依照身高排序(降序) 示例:select * from t_student order by age asc, height desc ;

十一、limit

运用limit能够精确地控制查询成果的数量,比方每次只查询10条数据

  • select * from 表名 limit 数值1, 数值2 ; 越过最前面4条句子,然后取8条记载 示例:select * from t_student limit 4, 8 ;

limit常用来做分页查询,比方每页固定显现5条数据,那么应该这样取数据 第1页:limit 0, 5 第2页:limit 5, 5 第3页:limit 10, 5 … 第n页:limit 5*(n-1), 5

十二、简略束缚

建表时能够给特定的字段设置一些束缚条件,常见的束缚有

  • not null :规则字段的值不能为null
  • unique :规则字段的值有必要唯一
  • 指定字段的默许值

建议:尽量给字段设定严厉的束缚,以确保数据的标准性

name字段不能为null,并且唯一 age字段不能为null,并且默许为1 示例:create table t_student (id integer, name text not null unique, age integer not null default 1) ;

FMDB运用:

  • FMDB是iOS平台的SQLite数据库结构
  • FMDB以OC的方法封装了SQLite的C言语API

优点:

  • 运用起来愈加面向目标,省去了许多费事、冗余的C言语代码
  • 比照苹果自带的Core Data结构,愈加轻量级和灵敏
  • 供给了多线程安全的数据库操作方法,有效地防止数据紊乱

FMDB

FMDB有三个首要的类

  • FMDatabase :其目标就代表一个独自的SQLite数据库,用来履行SQL句子
  • FMResultSet :用来履行查询后的成果集
  • FMDatabaseQueue :用于在多线程中履行多个查询或更新,它是线程安全的

翻开数据库

经过指定SQLite数据库文件途径来创立FMDatabase目标

FMDatabase *db = [FMDatabase databaseWithPath:path];
if (![db open]) {
    NSLog(@"数据库翻开失利!");
}

文件途径有三种状况

  • 详细文件途径 :假如不存在会主动创立
  • 空字符串@”” :会在暂时目录创立一个空的数据库,当FMDatabase连接封闭时,数据库文件也被删去
  • nil :会创立一个内存中暂时数据库,当FMDatabase连接封闭时,数据库会被销毁

更新数据库

在FMDB中,除查询以外的所有操作,都称为“更新”,create、drop、insert、update、delete等 运用executeUpdate:方法履行更新

- (BOOL)executeUpdate:(NSString*)sql, ...
- (BOOL)executeUpdateWithFormat:(NSString*)format, ...
- (BOOL)executeUpdate:(NSString*)sql withArgumentsInArray:(NSArray *)arguments

示例 :[db executeUpdate:@"UPDATE t_student SET age = ? WHERE name = ?;", @18, @"rc"]

履行查询

- (FMResultSet *)executeQuery:(NSString*)sql, ...
- (FMResultSet *)executeQueryWithFormat:(NSString*)format, ...
- (FMResultSet *)executeQuery:(NSString *)sql withArgumentsInArray:(NSArray *)arguments
// 查询数据
FMResultSet *rs = [db executeQuery:@"SELECT * FROM t_student"];
// 遍历成果集
while ([rs next]) {
    NSString *name = [rs stringForColumn:@"name"];
    int age = [rs intForColumn:@"age"];
    double score = [rs doubleForColumn:@"score"];
}

FMDatabaseQueue

FMDatabase这个类是线程不安全的,假如在多个线程中一起运用一个FMDatabase实例,会形成数据紊乱等问题 为了确保线程安全,FMDB供给便利快捷的FMDatabaseQueue类

// FMDatabaseQueue的创立
FMDatabaseQueue *queue = [FMDatabaseQueue databaseQueueWithPath:path];
...
[queue inDatabase:^(FMDatabase *db) {
    [db executeUpdate:@"INSERT INTO t_student(name) VALUES (?)", @"rc"];
    [db executeUpdate:@"INSERT INTO t_student(name) VALUES (?)", @"Jack"];
    [db executeUpdate:@"INSERT INTO t_student(name) VALUES (?)", @"Rose"];
    FMResultSet *rs = [db executeQuery:@"select * from t_student"];
    while ([rs next]) {
        // …
    }
}];

业务、回滚 : 操作数据库时,会出现这种状况:更新10条记载,当更新到第5条时,服务器宕机了,后边的费事就来了,我们要每次判别哪条记载更新了,哪条记载没更新!这时分就用到了业务,将更新10条记载放到一个业务中,成功完结所有更新操作时再提交,只需其间一条记载更新失利就回滚,回到初始状态,简略的说便是要么悉数成功,要么悉数不成功!

[queue inTransaction:^(FMDatabase *db, BOOL *rollback) {
    [db executeUpdate:@"INSERT INTO t_student(name) VALUES (?)", @"rc"];
    [db executeUpdate:@"INSERT INTO t_student(name) VALUES (?)", @"Jack"];
    [db executeUpdate:@"INSERT INTO t_student(name) VALUES (?)", @"Rose"];
    FMResultSet *rs = [db executeQuery:@"select * from t_student"];
    while ([rs next]) {
        // …
    }
}];
//业务回滚
*rollback = YES;

对模型进行存取

以头条新闻为例,一条新闻为一条记载: 1.先在本地数据库查找是否存在缓存,存在则显现缓存的新闻, 2.不存在则去头条服务器恳求数据显现,一起缓存

思路很简略: 要做到恳求了今天头条的新闻缓存到本地数据库后,下次翻开直接在数据库取出数据后显现。 两种状况: 1、直接缓存后天返回的json数据 ,优点是简略,避免了模型转字典的过程,数据原始,可是假如对数据有更新,比方:新闻是否已读等。 2、将服务器返回的json转成model(模型)再缓存,缺点便是或许在模型中有为了便利开发而新增的字段,而这些字段是不需要进行缓存的。

第1种比较简略,就以第2种为例: 写了一个新闻缓存的东西类

NewsCacheTool.h
/**缓存新闻数据到本地数据库*/
+ (void)saveNewsToDatabase:(NSArray *)newsArray;
/**读取新闻(userID对运用户的id,同个运用或许存在多个账号登录状况)*/
+ (NSArray *)selectNewsToDatabase:(NSString *)userID;
/**清除缓存*/
+ (void)clearNewsCache:(void(^)(BOOL success))flag;
NewsCacheTool.m
static FMDatabase *_db;
// 第一次运用就开端创立表
+ (void)initialize{
    NSString *cachePath = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES)[0];
    NSString *filePath = [cachePath stringByAppendingPathComponent:@"News.sqlite"];
    _db = [FMDatabase databaseWithPath:filePath];
    if ([_db open]) {
        NSLog(@"翻开数据库成功");
        // 自增主键、userID、二进制数据流
        NSString *sql = @"create table if not exists t_news (id integer primary key autoincrement,userID text,dict blob);";
        BOOL success = [_db executeUpdate:sql];
        if (success) {
            NSLog(@"创立表成功");
        }else{
            NSLog(@"创立表失利");
        }
    }else{
        NSLog(@"翻开数据库失利");
    }
}
// 缓存数据,这里为了安全起见应该运用业务
+ (void)saveNewsToDatabase:(NSArray *)newsArray{
    // 遍历模型数组
    for (NewsModel *nesw in newsArray){
        // 用户的id应该从自己的服务器获得
        NSString *userID = @"001";
        // 这是模型转字典的,自己用runtime简略实现了,有许多优异的第三方库能够运用,自选
        NSDictionary * newsDic = [nesw getDictionayFromModel];
        NSError *error;
        NSData *data;
        if (@available(iOS 11.0, *)){
            data = [NSKeyedArchiver archivedDataWithRootObject:newsDic requiringSecureCoding:YES error:&error];
        }else{
            data = [NSKeyedArchiver archivedDataWithRootObject:newsDic];
        }
        if (data == nil || error) {
            NSLog(@"缓存失利:%@", error);
            return;
        }
        BOOL success = [_db executeUpdate:@"insert into t_news (userID,dict) values(?,?)",userID,data];
        if (success) {
            NSLog(@"刺进成功");
        }else{
            NSLog(@"刺进失利");
        }
    }
}
// 在数据库中读取数据
+ (NSArray *)selectNewsToDatabase:(NSString *)userID{
    NSString *sql = [NSString stringWithFormat:@"select * from t_news where userID = '%@';",userID];
    FMResultSet *set = [_db executeQuery:sql];
    NSMutableArray *array = [NSMutableArray array];
    while ([set next]) {
        NSData *data = [set dataForColumn:@"dict"];
        NSError *error;
        NSDictionary *dic;
        if (@available(iOS 11.0, *)) {
            dic = [NSKeyedUnarchiver unarchivedObjectOfClass:[NSObject class] fromData:data error:&error];
        } else {
            dic = [NSKeyedUnarchiver unarchiveObjectWithData:data];
        }
        if(dic){
            NewsModel *news = [[NewsModel alloc]initWithDictionary:dic];
            [array addObject:news];
        }
    }
    return array;
}
// 清除新闻缓存
+ (void)clearNewsCache:(void (^)(BOOL success))flag{
    BOOL success = [_db executeUpdate:@"delete from t_news;"];
    if(flag){
        flag(success);
    }
}

模型转字典的过程中,在NewsModel类中,用runtime简略实现了,实践开发中或许会多层嵌套字典或数据,市面上有许多老练优异的轮子,可自行挑选。

- (NSMutableDictionary *)getDictionayFromModel{
    NSMutableDictionary *dicM = [NSMutableDictionary dictionary];
    unsigned int outCount, I;
    // 拷贝特点列表
    objc_property_t *properties = class_copyPropertyList([self class], &outCount);
    for (i = 0; i<outCount; i++) {
        objc_property_t property = properties[I];
        const char *char_p = property_getName(property);
        // 特点名
        NSString *propertyName = [NSString stringWithUTF8String:char_p];
        // 特点值
        id propertyValue = [self valueForKey:(NSString *)propertyName];
        // 设置KeyValues
        if (propertyValue) [dicM setObject:propertyValue forKey:propertyName];
    }
    //释放
    free(properties);
    return dicM;
}

SqliteDemo