iOS 上的数据库用的不多,看一看主流的 FMDB 以及其封装框架是怎么实现的。
简单SQL
检索
检索数据
检索单个列
1 | select prod_name from products; |
检索多个列
1 | select prod_id, prod_name, prod_price from products; |
检索所有列
1 | select * from produces |
检索的列去重
1 | select distinct prod_name from products; |
限制展示个数
1 | select prod_name from products limit 5; |
排序检索
简单排序
1 | select prod_name from products order by prod_name; |
多列排序
1 | select prod_id, prod_name, prod_price from products order by prod_name, prod_id; |
指定排序方向
1 | select prod_id, prod_name, prod_price from products order by prod_name desc, prod_id limit 6; |
过滤
过滤数据
where 过滤
1 | select prod_name from products where prod_price = 123; |
范围值
1 | select prod_name from products where prod_id between 1 and 100; |
空值
1 | select price_id from products where prod_name is null; |
and or 操作符
1 | select prod_price from products where (vend_id = 1003 or vend_id = 1004) and prod_price >= 100; |
指定范围
1 | select prod_name from products where vend_id in (1003, 1004); |
正则过滤
字符包含匹配
1 | select prod_name from products where prod_name regexp '1000'; |
.
匹配任意一个字符:
1 | select prod_name from products where prod_name regexp '.000'; |
OR 匹配任意一个条件
1 | select prod_name from products where prod_name regexp '1000|2000|3000'; |
匹配几个字符串之一
1 | select prod_name from products where prod_name regexp '[123] Ton'; |
1 | select prod_name from products where prod_name regexp '[1-5a-z] Ton'; |
^
表示否定:
1 | select prod_name from products where prod_name regexp '[^123] Ton'; |
匹配多个实例
元字符 | 说明 |
---|---|
+ | 1个或多个 |
? | 0个或一个 |
* | 0个或多个 |
{n} | 指定数目匹配 |
{n,} | 不少于指定数目匹配 |
{n,m} | 匹配数目范围 |
直接跟在要匹配的字符后面,如果要匹配的是字符串,需要给字符串加上括号
定位符
元字符 | 说明 |
---|---|
^ | 文本开始 |
$ | 文本结束 |
函数
数据聚合
对一个列的所有行进行操作,返回一个聚合的值
sum() 求和avg() 求平均值
1 | select sum(prod_price) as sum_price from products; |
count() 求数量
1 | // 排除 prod_price 为 null 的 |
min() max()求最小最大
1 | select min(prod_price) as min_price from products; |
分组
分组的作用是对执行分组,并对结果进行聚合
创建分组
获取不同值的 vend_id
并将每个值的数量以 num_prods
字段展示
1 | select vend_id, count(*) as num_prods from products group by vend_id; |
过滤分组
having
在数据分组后进行过滤,where
在数据分组前进行过滤
1 | select vend_id, count(*) as num_prods from products where prod_price >= 10 group by vend_id having count(*) >= 2; |
分组和排序
在上面的基础上再对某一列排序:
1 | select vend_id, count(*) as num_prods from products where prod_price >= 10 group by vend_id having count(*) >= 2 order by num_prods; |
联结表
联结
定义
检索数据的时候对多张表进行操作。
譬如一个商品表,要记录这个商品的供应商的信息。如果供应商的信息存在这个商品表中,就会存在诸多重复数据。因此,应该讲供应商信息也单独创建一个表。供应商的 ID 为供应商表的主键。商品表只保存这个供应商的 ID,这个供应商的 ID 叫做商品表的外键,供应商表和商品表通过这个外键进行的关联。
创建联结
1 | select vend_name, prod_name, prod_price from vendors, products where vendors.vend_id = products.vend_id order by vend_name, prod_name; |
上面从供应商(vendors
) 和商品(products
)两张表中取 vend_name
,prod_name
,prod_price
这三列的数据。通过 vend_id
进行关联。
相当于在运行时把两张表通过主键和外键关联,结合成了一张表。
外连接
要查询表A中所有满足条件的行,并顺便把表B中的相应行取出来:
1 | SELECT * FROM player LEFT JOIN team on player.team_id = team.team_id where player.age < 30 |
从 player 表中把年纪小于 30 的都取出来,并且把 player 对应的 team 的相关信息从 team 表中取出。
on 关键字用于配合 left join,找到 player 相关的 team
增删改
插入
插入一行
1 | insert into customers( |
插入多个行
1 | insert into customers( |
插入检索的数据
1 | insert into customers( |
把从 custnew
表中检索出的这几列插入到 customers
中
更新数据
1 | update customers |
删除数据
1 | delete from customers |
操作表
创建表
1 | create table customer ( |
创建表的时候要指定字段,类型;
非空的字段要手动通过 not null
标识;
可以通过 default
指定默认值;
表的主键通过创建表的时候用 primary key
指定,并且主键是必须唯一的,所以可以通过 auto_increment
标识其自增。
最后 engine=innodb
指定引擎。
更新表结构
添加列和删除列分别使用 add
和 drop
1 | alter table vendors |
删除表
1 | drop table customer2; |
数据库基础
事务
什么是事务?事务就有四大特性:ACID
- A:原子性
- C:一致性
- I:隔离性
- D:持久性
并发存在异常
- 脏读:读到了其他事务还没有提交的数据。
- 不可重复读:对某数据进行读取,发现两次读取的结果不同,也就是说没有读到相同的内容。这是因为有其他事务对这个数据同时进行了修改或删除。
- 幻读:事务A根据条件查询得到了N条数据,但此时事务B更改或者增加了M条符合事务A查询条件的数据,这样当事务A再次进行查询的时候发现会有N+M条数据,产生了幻读。
数据库设计范式
设计数据库模型的时候,需要对内部属性之间的联系的合理化程度进行定义。这种规范叫做范式(NF)。
1NF 指的是数据库表中的任何属性都是原子性的,不可再分。
2NF 指的数据表里的非主属性都要和这个数据表的候选键有完全依赖关系。比如:
1 | 一张表中的字段如下 |
3NF 在满足 2NF 的同时,对任何非主属性都不传递依赖于候选键。比如:
1 | (球员编号) → (姓名,年龄,球队名称,球队教练) |
超键:能唯一标识元组的属性集叫做超键。
候选键:如果超键不包括多余的属性,那么这个超键就是候选键。
FMDB
基本结构
FMDB 是 sqlite 的简单封装,主要用来执行 sql 语句,并取出数据。主要有三个类:
FMDatabase
: FMDB 最重要的类,用来保存 database 的实例,并对这个 db 执行 sql 语句。FMResultSet
:保存了 sql 语句的执行结果。FMDatabaseQueue
:在多线程环境下执行 sql。
基本使用
建立开启数据库和关闭
1 | // 创建数据库 |
当文件不存在时,fmdb 会自己创建一个。
1 | // 关闭数据库 |
创建删除表
1 | //3.创建表 |
1 | // 如果表格存在 则销毁 |
增删改查
增
1 | for (int i = 0; i < 4; i++) { |
通过 executeUpdate
来执行非 select 的增删改语句。另外,有三种方式给 sql 传参:
- 使用
?
做占位符,那么传参就必须是 oc 对象。 - 使用
%@
等做占位符,传参可以是任意相依类型。 - 使用数组传参,占位符还是使用
?
,数组中保存的也是 oc 对象。
删改
这两个和增加数据没什么不同,因此就放在一起了:
1 | //1.不确定的参数用?来占位 (后面参数必须是oc对象,需要将int包装成OC对象) |
1 | //修改学生的名字 |
查
表的查询要通过 executeQuery
执行:
1 | //查询整个表 |
查询结果会保存在 FMResultSet
类的实例中。即使操作结果只有一行,也需要先调用 FMResultSet
的next
方法。
FMDB 提供如下多个方法来获取不同类型的数据:
1 | intForColumn: |
当然,一个一个自己获取列名也太麻烦了,可以使用 FMResultSet
提供的 resultDictionary
方法,获取整个字典:
1 | while ([resultSet next]) { |
在使用 while
循环的时候,不需要手动关闭 FMResultSet,因为 [FMResultSet next]
遍历到最后会调用 [FMResultSet close]
线程安全
FMDatabase
本身不是线程安全的,所以不要在多线程中使用。需要使用 FMDatabaseQueue
来帮助保证线程安全:
1 | // 创建,最好放在一个单例的类中 |
事务
在数据库中,事务可以保证数据操作的完整性:
1 | [_dataBaseQueue inDatabase:^(FMDatabase * _Nonnull db) { |
通过beginTransaction
开启一个事务。任意情况下发生错误的时候可以通过 rollback
回退,否则通过 commit
提交事务。
本地调试
本地调试可以使用免费的数据库查看工具 ,比如:
源码解析
初始化 FMDatabase
初始化使用的是 + [FMDatabase databaseWithPath:@"path"]
方法,它会在内部调用 initWithPath:
方法。它其实就是创建了一个 FMDatabase
的实例。
1 | - (instancetype)initWithPath:(NSString*)aPath { |
要求输入一个数据库的路径,并保存在 _databasePath
中。这一步里还没有打开 db,所以 _db
还是 nil。
打开 db
- [FMDatabase open]
方法打开了 db。
1 | - (BOOL)open { |
如果已经打开了就直接返回。否则调用 sqlite 提供的的 sqlite3_open()
方法,打开的数据库。
创建 select sql
这一节主要讲如何创建 select 的 sql,select 会从数据库中获取数据,所以需要创建一个专门的类用来拿数据,也就是下面的的 FMResultSet
创建 sqlite3_stmt
所有 sql 语句都会被转化为 sqlite3_stmt
类型。由于这一过程比较耗时,所以一般将转化好的 sqlite3_stmt
保存到 _cachedStatements
字典中,以便相同 sql 反复使用:
1 | - (FMResultSet *)executeQuery:(NSString *)sql withArgumentsInArray:(NSArray*)arrayArgs orDictionary:(NSDictionary *)dictionaryArgs orVAList:(va_list)args { |
cachedStatementForQuery:
就是在字典中查找 sql 对应的 sqlite3_stmt
的方法,它返回的 FMStatement
是 sqlite3_stmt
的封装。
绑定参数
参数随着方法一起传了进来,一般参数有两种,一种是字典类型的,根据 sql 中的参数名插入,还有一种是数组型的,依次替换 sql 中的占位符。(代码很长,就不贴了)
首先通过 sqlite3_bind_parameter_count()
获得 sql 的参数个数。然后检查传入的是字典还是数组。如果是字典,遍历字典,通过 sqlite3_bind_parameter_index()
拿到键对应的参数索引,然后绑定;如果是数组就依次绑定到对应的列中。绑定也是使用的 sqlite 提供的针对不同类型的一系列绑定方法。
绑定完了后,将这个 sqlite3_stmt
暂存。由于是已经绑定了参数,所以可见前面 sqlite3_reset()
做的就是将参数清空。
SQLite 支持使用占位符
?
,并且在必要的时候绑定参数。所以你不需要把实际的值放入字符串中去。这是一个安全上的考量,它可以守护程序避免 SQL 注入。它也可以帮助你减少必须 escape 值(sql 提供的转义用的命令)这样的不必要的麻烦。
创建 FMResultSet 保存结果
现在 sql 已经创建完成,只欠执行了。FMDB 并没有立即执行,而是创建了一个 FMResultSet
对象,用来保存每次 sql 的结果。因为一个 db 可以执行多个 sql,所以就要创建多个 FMResultSet
。所以在创建 sql 的最后,还要创建一个 FMResultSet
:
1 | - (FMResultSet *)executeQuery:(NSString *)sql withArgumentsInArray:(NSArray*)arrayArgs orDictionary:(NSDictionary *)dictionaryArgs orVAList:(va_list)args { |
执行 sql
执行 sql 在 FMResultSet
中进行:
1 | - (BOOL)nextWithError:(NSError **)outErr { |
其实上面的关键就是调用 sqlite 的 sqlite3_step()
方法。
获取数据
前面执行完 sql 之后,你就可以拿到数据了,FMResultSet
中提供了方法将当前行转化为一个字典:
1 | - (NSDictionary*)resultDictionary { |
过程就是通过循环,拿出每一列的数据,加入到字典中。当然我们也可以自己获取列数据及列名。
创建 update sql
select sql 需要配合 FMResultSet
,更新数据库内容则比较简单。直接使用 FMDatabase
更新即可。update sql 的代码几乎和 select sql 一模一样,不同的是,更新操作在创建好 sql 后,直接执行 sqlite3_step()
,执行完后根据是否要缓存选择性执行重置 sqlite3_reset()
或者关闭 sqlite3_finalize()
。代码太长且重复,就不贴了。
加解密
FMDB 封装了为 db 加解密的方法。解密使用如下方法,在打开 db 前使用,否则报错:
1 | - (BOOL)setKey:(NSString*)key { |
其实是一个非常简单的封装,就是将 String 转化为 Data,然后使用 sqlite3_key
进行解密。
有解密必然是要现有加密的,使用 sqlite3_rekey()
方法,可以完成没有密码的时候创建密码,有密码的时候修改密码或者清除密码的操作。代码和解密类似,也不贴了。
关闭数据库
关闭数据库,需要做两方面处理,一方面是清除 fmdb 创建的缓存,一方面是释放 sqlite 资源:
1 | - (BOOL)close { |
这里先尝试用 sqlite3_close()
关闭,如果不行,那么再 sqlite3_next_stmt()
来获取每个 stmt,然后将他们 sqlite3_finalize()
。整个过程在一个大的 while 循环中,直到数据库关闭为止。
多线程
有些费时的更新操作我们不希望在主线程中进行。FMDB 提供了 FMDatabaseQueue
这个类帮助我们创建了后台线程。其实就是封装了子线程的操作,其实你也可以自己创建子线程,然后进行 sql,两者没什么区别。
初始化创建队列
创建队列的代码如下:
1 | static const void * const kDispatchQueueSpecificKey = &kDispatchQueueSpecificKey; |
主要分为两步,第一步是创建 db。第二步是创建队列。之后还用 dispatch_queue_set_specific
绑定了 FMDatabaseQueue
对象以及 queue
,这个的用处下面再说。
执行 sql
FMDB 为 FMDatabaseQueue
提供了一个方法批量处理某一个 db 的 sql。它接收一个 sql 的 block:
1 | - (void)inDatabase:(void (^)(FMDatabase *db))block { |
可以看到,主要就是在之前创建的 queue 中同步执行 sql。那么 dispatch_get_specific
有何用意呢?简单的说就是如果当前队列为 _queue
,下面的同步操作就会产生死锁。所以这里 dispatch_get_specific
就是为了验证一下,现在是不是在 _queue
队列中。如果是,那么 currentSyncQueue
就不为空,那么直接通过断言触发异常。
其实这个判断,就是要求使用者不要用在刚刚创建的 _queue
中调用执行 sql 的方法,而是直接在主队列中调用,方法执行的时候会自动将 sql 执行在 _queue
中。
事务处理
事务处理的方法如下:
1 | - (void)inTransaction:(void (^)(FMDatabase *db, BOOL *rollback))block { |
事务处理主要还是调用事务的相关 sql 语句,使用者可以通过 sql 是否执行成功,来决定是否需要回滚。
这里 block 的入参使用的是 BOOL *roolback
,然后传入一个 BOOL 的地址 &shouldRoolBack
,可以实现不用返回值传递数据。注意,这种取地址的写法在使用的时候要用 *shouldRoolBack
来取地址上的值,因为是基本类型的指针:
1 | BOOL roolback = YES; |
非基本类型的指针的赋值直接就是 p = Person()
这种地址的赋值就行了,不存在 *p
的情况。基本类型的指针必须通过 *r
拿到堆上的值 *r = No
总结
FMDB 的整个过程相对简单,简单来说就是先初始化控制类 FMDatabase
,然后通过这个类打开 db,执行 sql,关闭数据库等操作。执行的 sql 需要转化为 sqlite 使用的 sqlite3_stmt
类型,并缓存。对于有结果的 sql,或创建一个 FMResultSet
来保存 sql 已经其相应结果。多线程通过 FMDatabaseQueue
实现,它可以为 sql 开启后台线程执行,并且封装了 sqlite 的原子性操作的语句来实现事务。