“我正在参与「启航方案」”
本文要点说下常用的本地数据库操作,Sqlite和封装的FMDB的运用,以及Model的存与取。
效果图
什么是数据库
- 数据库(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;
}