更新时间:2021-01-06 11:52
普通 KV 存储只能存储简单数据类型或封装好的 OC 对象,且不支持搜索。当业务有 sqlite 访问需求时,可由统一存储的 DAO 功能进行简化和封装。基本工作原理如下:
xml
格式的配置文件,描述各 sqlite 操作的函数、返回的数据类型、需要加密的字段等。DAOInterface(@protocol)
,接口方法名、参数与配置文件里的描述一致。xml
配置文件传给 APDataCenter 的 daoWithPath 方法,生成 DAO 访问对象。该对象直接强转为 id<DAOInterface>
。insertItem
和 getItem
是插入和读取数据的两个方法,接收参数并格式化到 SQL 表达式里。createTable
方法会在底层被默认调用一次。
<module name="Demo" initializer="createTable" tableName="demoTable" version="1.0">
<update id="createTable">
create table if not exists ${T} (index integer primary key, content text)
</update>
<insert id="insertItem" arguments="content">
insert into ${T} (content) values(#{content})
</insert>
<select id="getItem" arguments="index" result="string">
select * from ${T} where index = #{index}
</select>
</module>
@protocol DemoProtocol <APDAOProtocol>
- (APDAOResult*)insertItem:(NSString*)content;
- (NSString*)getItem:(NSNumber*)index;
@end
demo_config.xml
,在 Main Bundle 中。insertItem
方法写入一个数据,获取它的索引,再用该索引把写入的数据读出来。
NSString* filePath = [[NSBundle mainBundle] pathForResource:@"demo_config" ofType:@"xml"];
id<DemoProtocol> proxy = [[APDataCenter defaultDataCenter] daoWithPath:filePath userDependent:YES];
[proxy insertItem:@"something"];
long long lastIndex = [proxy lastInsertRowId];
NSString* content = [proxy getItem:[NSNumber numberWithInteger:lastIndex]];
NSLog(@"content %@", content);
lastInsertRowId
是 APDAOProtocol
的一个方法,用来取最后插入的行的 rowId
。想让 DAO 对象支持该方法,只要在声明 DemoProtocol
时继承自 APDAOProtocol
即可。
<module name="demo" initializer="createTable" tableName="tableDemo" version="1.5" resetOnUpgrade="true" upgradeMinVersion="1.2">
initializer
参数可选。对于 initializer
指定的 update
方法,DAO 认为是数据库建表方法,会在第一次 DAO 请求时默认执行一次。tableName
指定下面方法里默认操作的表名,在 SQL 语句里可以用 ${T}或${t}
代替,不用每次都写表名。建议每个配置文件只操作一张表。tableName
可空,应对同一配置文件操作相同格式的多张表的情况。比如聊天消息分表处理,可以调用 DAO 对象的 setTableName
方法设置需要操作的表名。version
是配置文件的版本号,格式为 x.x
,创建 table 后,会以 tableName
作为 key,把表的版本存到数据库文件的 TableVersions
表里,配合 upgrade
块进行表的更新。resetOnUpgrade
,如果为 TRUE 或 YES,当 version
更新后,会删除原表,而不是调用 ungrade
块。无此参数为默认为 false。upgradeMinVersion
,如果不为空,对于小于这个版本的数据库文件,直接重置,否则执行升级操作。
<const table_columns="(id, time, content, uin, read)"/>
table_columns
是常量的名字,等号(=)后方为常量值,在配置文件里可以使用 ${常量名}
来引用。
<select id="find" arguments="id, time" result=":messageModelMap">
select * from ${T} where id = #{id} and time > @{time}
</select>
@arguments
:
,
分隔。调用者传进来的参数依赖 arguments
里的描述依次命名。DAO 对象的 selector 调用时不会携带参数名,所以必须在这里按顺序命名。$
符号时,表示这个参数不接受 nil 值。业务的调用 DAO 接口,是允许传 nil 参数的,如果某个参数前面有 $
符号,当调用者不小心传入了 nil 值,DAO 调用会自动失败。防止发生不可预知的问题。例如,在上方的代码中,对应的 selector 为:
- (MessageModel*)find:(NSNumber*)id time:(NSNumber*)time;
如果 DAO 对象调用 [daoProxy find:@1234 time:@2014]
,那么拼接好后的 SQL 语句是:
select * from tableDemo where id = ? and time > 2014
并且 @1234
这个 NSNumber
会交给 sqlite 绑定。
@result
:
result
里可以填写 DAO 方法的返回值,用 []
括起来表示返回数组类型,会对 select 执行的返回一直进行迭代,直到 sqlite 无结果返回。如果不用 []
括起来,表示只返回一个结果,对 select 执行的返回只迭代一次,类似 FMDB 库里 FMResultSet
只调用一次 next 方法。
返回类型:
int
:只有一个结果,返回 [NSNumber numberWithInt]
类型,注意溢出的可能。long long
:只有一个结果,返回 [NSNumber numberWithLongLong]
类型。bool
:只有一个结果,返回 [NSNumber numberWithBool]
类型。double
:只有一个结果,返回 [NSNumber numberWithDouble]
类型。string
:只有一个结果,返回 NSString*
类型。binary
:只有一个结果,返回 NSData*
类型。[int]
:返回数组,数组里为[NSNumber numberWithInt]
。[long long]
:返回数组,数组里为 [NSNumber numberWithLongLong]
。[bool]
:返回数组,数组里为 [NSNumber numberWithBool]
。[double]
:返回数组,数组里为 [NSNumber numberWithDouble]
。[string]
:返回数组,数组里为 NSString*
。[binary]
:返回数组,数组里为 NSData*
。[{}]
:返回数组,数组里是列名 > 列值的 map。[AType]
:返回数组,数组里是填好的自定义类。{}
:只有一个结果,返回列名 > 列值的 map。AType
:只有一个结果,返回填好的自定义类。[:AMap]
:返回数组,数组里是使用 xml
里定义的 AMap 映射出来的对象。:AMap
:只有一个结果,使用配置文件里定义的 AMap 来描述对象。例如,上面的例子中,返回类型为 :messageModelMap
。具体返回的 Objective-C
类型,以及需要特殊映射的列都会在 messageModelMap
里定义。参考 map 关键字。
@foreach
:
select 也支持 foreach
字段,用法下文介绍的 insert
、update
、delete
里的相似。不同的是,select
方法如果指定了 foreach
参数,那么会执行 N 次 SQL 的 select 操作,并把结果放到数组里返回。所以如果 DAO 的 select
方法是 foreach
的,它的返回值在 protocol 里一定要定义成 NSArray*
。
<insert id="addMessages" arguments="messages" foreach="messages.model">
insert or replace into ${T} (id, content, uin, read) values(#{model.msgId}, #{model.content}, #{model.uin}, #{model.read})
</insert>
<update id="createTable">
<step>
create table if not exists ${T} (id integer primary key, content text, uin integer, read boolean)
</step>
<step>
create index if not exists uin_idx on ${T} (uin)
</step>
</update>
<delete id="remove" arguments="msgId">
delete from ${T} where msgId = #{msgId}
</delete>
@foreach
:
foreach
字段时,这个方法被调用时,会依次对参数数组里的每个元素执行一次 SQL。例如代理方法为:
- (void)addMessages:(NSArray*)messages;
messages 为 MessageModel 数组,那么对于 messages 里的每个 model,都会执行一次 SQL 调用,这样就能实现把数组里的元素一次性插入数据库而上层不需要关心循环的调用。底层会把这次操作合并为一个事务而提升效率。
<upgrade toVersion='3.2'>
<step resumable="true">alter table ${T} add column1 text</step>
<step resumable="true">alter table ${T} add column2 text</step>
</upgrade>
@resumable
:
<map id="messageModelMap" result="MessageModel">
<result property="msgId" column="id"/>
</map>
messageModelMap
的映射,实际生成的 Objective-C 对象是 MessageModel 类。
<upgrade toVersion="1.1">
<step>
alter table...
</step>
<step>
alter table...
</step>
</upgrade>
<crypt class="MessageModel" property="content"/>
描述 class 这个类的 property 属性进行加密处理,当向数据库写入时从这个属性里取出的值会进行加密;当数据库读出时,生成对象向这个属性里赋值会先解密再赋值。
比如执行 DAO 方法,model 是 MessageModel 类。因为取了 model 的 content 属性,所以会加密后再写入数据库。
<insert id="insertMessage" arguments="model">
insert into ${T} (content) values(#{model.content})
</insert>
执行这个 select 方法时,返回对象是 MessageModel 类。底层从数据库里取出数据向 MessageModel 的实例写入 content 属性时,会将数据先解密再写入。最后返回处理好的 MessageModel 对象。
<select id="getMessage" arguments="msgId" result="MessageModel">
select * from ${T} where msgId = #{msgId}
</select>
加密方式的设置方法定义在 APDAOProtocol 中,如下:
/**
* 设置加密器,用于加密表里标记为需要加密的列的数据。向表里写数据时,碰到这个列,会调用进行加密。
*
* @param crypt 加密结构体,会被拷贝一份。如果传入的 crypt 是外部创建的,需要外部进行 free。如果是 APDefaultEncrypt(),不需要进行释放。
*/
- (void)setEncryptMethod:(APDataCrypt*)crypt;
/**
* 设置解密器,用于解密表里标记为需要加密的列的数据。从表里读数据时,碰到这个列,会调用进行解密。
*
* @param crypt 解密结构体,会被拷贝一份。如果传入的 crypt 是外部创建的,需要外部进行 free。如果是 APDefaultDecrypt(),不需要进行释放。
*/
- (void)setDecryptMethod:(APDataCrypt*)crypt;
如果不进行设置,会使用 APDataCenter 的默认加密,见 KV 存储。如果一个 DAO 代理对象是 id
<insert id="addMessages" arguments="messages, onlyRead" foreach="messages.model">
<if true="model.read or (not onlyRead)">
insert or replace into ${T} (msgId, content, read) values(#{model.msgId}, #{model.content}, #{model.read})
</if>
</insert>
表达式支持的运算符如下:
():括号
+:正号
-:负号
+:加号
-:减号
*:乘号
/:除号
\:整除
%:取模
>:大于
<:小于
>=:大于等于
<=:小于等于
==:等于
!=:不等于
and:逻辑与,不区分大小写
or:逻辑或,不区分大小写
not:逻辑非,不区分大小写
xor:异或,不区分大小写
.
来访问它的属性,比如上面例子 model.read,如果参数是数组或字典,可以用 参数名.count
取元素数。一个较复杂的表达式如下:
<if true="(title != nil and year > 2010) or authors.count >= 2">
title,year,authors 都是调用者传来的参数,调用时 title 是可以传 nil 的;那么上面含义为”当书名不为空,并且出版年份大于 2010 年,或作者数大于 2。
<choose>
<when true="title != nil">
AND title like #{title}
</when>
<when true="author != nil and author.name != nil">
AND author_name like #{author.name}
</when>
<otherwise>
AND featured = 1
</otherwise>
</choose>
<foreach item="iterator" collection="list" open="(" separator="," close=")" reverse="yes">
@{iterator}
</foreach>
比如一个方法从外部接收字符串数组参数,list 内容为@[@”1”, @”2”, @”3”],有另一个参数是 prefix=@”abc”,使用 ()
包裹,使用,
分隔。那么执行结果为:(abc1,abc2,abc3)
<update id="proc" arguments="list, prefix">
<foreach item="iterator" collection="list" open="(" separator="," close=")">
{prefix}{iterator}
</foreach>
</update>
foreach 语句通常用于拼接 select 语句里的 in 块,比如:
select * from ${T} where id in
<foreach item="id" collection="idList" open="(" separator="," close=")">
#{id}
</foreach>
<where onVoid="quit">
<if true="state != nil">
state = #{state}
</if>
<if true="title != nil">
AND title like #{title}
</if>
<if true="author != nil and author.name != nil">
AND author_name like #{author.name}
</if>
</where>
where 会处理多余的 AND、OR(大小写无所谓),并在任何条件都不符合时连 where 都不返回。用于在 SQL 语句里拼接有大量判断条件的 where 子句。比如上例,如果只有最后一个判断成立,该语句会正确返回 where author_name like XXX,而不是 where AND author_name like XXX。
<set>
<if false="username != nil">username=#{username},</if>
<if false="password != nil">password=#{password},</if>
<if true="email != nil">email=#{email},</if>
<if true="bio != nil">bio=#{bio},</if>
</set>
set 会处理结尾多余的,
,并在任何条件都不符合时什么都不返回。与 where 语句类似,只是它处理的是后缀的逗号。
<trim prefix="WHERE" prefixOverrides="AND | OR | and | or " onVoid="ignoreAfter">
</trim>
<!--
等价于<where>
-->
<trim prefix="SET" suffixOverrides=",">
</trim>
<!--
等价于<set>
-->
where 和 set 语句可以使用 trim 替换。Trim 语句定义了语句的整体前缀,以及对每个子句需要处理的多余的前缀与后缀列表(用|划分)。
onVoid 参数可以出现在 where、set、trim 里,有两个取值 ignoreAfter 和 quit。分别代表当这个 trim 语句里任何子句都不成立,导致生成一个空串时,采取什么逻辑。ignoreAfter 代码忽略下面的格式化语句,直接返回当前生成的 SQL 语句执行,quit 代表不再执行这条 SQL 语句,但会返回成功。
<sql id="userColumns"> id,username,password </sql>
<select id="selectUsers" result="{}">
select ${userColumns}
from some_table
where id = #{id}
</select>
定义可重用的 SQL 代码段,在其它语句中使用 ${name} 从原文引入进来。name 不能为 T 或 t,因为 ${T} 和 ${t} 代表默认的表名了。SQL 块里面可以再引用别的 SQL 块。
<insert id="insertTemplates" arguments="templates" foreach="templates.model">
<try>
insert into ${T} (tplId, tplVersion, time, data, tag) values(%{'#{model.*}', tplId, tplVersion, time, data, tag})
<except>
update ${T} set %{'* = #{model.*}', data, tag} where tplId = #{model.tplId} and tplVersion = #{model.tplVersion}
</except>
</try>
</insert>
有时,同一个 model 可能多次插入数据库,用 insert or replace 会导致当 model 主键冲突(同主键的 model 已经在数据库存在)时,原先的数据被删除掉,重新 insert。这样会导致同条数据的 rowid 发生变化。用 try except 语句块可以解决这个问题(当然不仅限于解决这种问题)。try except 只能出现在 DAO Method 定义里,前后不能再有其它语句。try 和 except 里面可以包含其它语句块。
当这条 DAO 方法执行时,如果 try 里面的语句执行失败,会自动尝试执行 except 里的语句。如果都失败,这次 DAO 调用才会返回失败。
@{something},用于方法参数,参数名为 something,在格式化 SQL 语句时会把对象内容拼到 SQL 语句中;因为参数都为 id 类型,所以默认使用 [NSString stirngWithFormat:@”%@”, id] 来格式化;@{something or “”} 这种格式,表示传入的参数如果为 nil,会转成一个空字符串而不是 NULL。
不建议使用 @{} 的方式来引用参数,效率比较低,有 SQL 注入风险。如果参数对象是个 NSString,拼接进去后,会自动添加引号将字符串括起来,保证 SQL 语句格式的正确性。如果用户在配置文件里自己写了引号,底层不会自动添加引号了。
使用 @{something no “”},这种格式,可以强制不添加引号。
<select id="getMessage" arguments="id" result="[MessageModel]">
select * from ${T} where id = @{id}
</select>
比如上例,id 参数传进来是个 NSString,上面的写法是正确的,生成的 SQL 会自动把 id 格式化进去,并且在前后添加引号。
#{something},用于方法参数,参数名为 something,在格式化 SQL 语句时会转为一个?
,然后将对象绑定给 sqlite;建议书写 SQL 时尽量使用这种方式,效率更高。 #{something or “”} 这种格式,表示传入的参数如果为 nil,会转成一个空字符串而不是 NULL。
${something},用来引用配置文件里的内容,比如引用默认表名 ${T} 或 ${t}、引用配置文件里定义的常量和 SQL 代码块。
对于 @ 和 # 引用,可以使用.
来访问参数对象的属性。比如传入的参数名是 model,并且是一个 MessageModel 类型,它有属性 NSString* content。那么 @{model.content},会取出其 content 属性的值。内部实现为 [NSObject valueForKey:],所以如果参数是一个字典(字典的 valueForKey 等价于 dict[@””]),那么也可以使用 #{adict.aaa} 引用 adict[@”aaa”] 值。
每个生成的 DAO 对象代理对象都支持 APDAOProtocol。
@protocol MyDAOOperations <APDAOProtocol>
- (APDAOResult*)insertMessage:(MyMessageModel*)model;
@end
具体方法见代码的函数注释。
#import <Foundation/Foundation.h>
#import <sqlite3.h>
#import "APDataCrypt.h"
#import "APDAOResult.h"
#import "APDAOTransaction.h"
@protocol APDAOProtocol;
typedef NS_ENUM (NSUInteger, APDAOProxyEventType)
{
APDAOProxyEventShouldUpgrade = 0, // 即将升级
APDAOProxyEventUpgradeFailed, // 表升级失败
APDAOProxyEventTableCreated, // 表被创建
APDAOProxyEventTableDeleted, // 表被删除
};
typedef void(^ APDAOProxyEventHandler)(id<APDAOProtocol> proxy, APDAOProxyEventType eventType, NSDictionary* arguments);
/**
* 这个 Protocol 定义的方法每个 DAO 代理对象都支持,使用时使用 id<APDAOProtocol>对 DAO 对象进行转换。
*/
@protocol APDAOProtocol <NSObject>
/**
* 配置文件里 module 可以设置表名,如果想实现配置文件作为一个模板,操作不同的表,可以生成 DAO 对象后手工向 DAO 对象设计表名。
* 比如要对与每个 id 的对话消息进行分表的情况。
*/
@property (atomic, strong) NSString* tableName;
/**
* 返回这个 proxy 操作的表所在数据库文件的路径
*/
@property (atomic, strong, readonly) NSString* databasePath;
/**
* 获取这个 proxy 操作的数据库文件的句柄
*/
@property (atomic, assign, readonly) sqlite3* sqliteHandle;
/**
* 注册全局变量参数,这些参数配置文件里的所有方法都可以使用,在配置文件里使用#{name}和@{name}来访问。
*/
@property (atomic, strong) NSDictionary* globalArguments;
/**
* 这个 proxy 的事件回调,业务自行设置。回调线程不确定。
注意循环引用的问题,业务对象持有 proxy,这个 handler 方法里不要访问业务对象或 proxy,proxy 可以在回调的第一个参数里拿到。
*/
@property (atomic, copy) APDAOProxyEventHandler proxyEventHandler;
/**
* 设置加密器,用于加密表里标记为需要加密的列的数据。向表里写数据时,碰到这个列,会调用进行加密。
*
* @param crypt 加密结构体,会被拷贝一份。如果传入的 crypt 是外部创建的,需要外部进行 free。如果是 APDefaultEncrypt(),不需要进行释放。
*/
@property (atomic, assign) APDataCrypt* encryptMethod;
/**
* 设置解密器,用于解密表里标记为需要加密的列的数据。从表里读数据时,碰到这个列,会调用进行解密。
*
* @param crypt 解密结构体,会被拷贝一份。如果传入的 crypt 是外部创建的,需要外部进行 free。如果是 APDefaultDecrypt(),不需要进行释放。
*/
@property (atomic, assign) APDataCrypt* decryptMethod;
/**
* 返回 sqlite 的最后一条 rowId
*
* @return sqlite3_last_insert_rowid()
*/
- (long long)lastInsertRowId;
/**
* 获取配置文件定义的所有方法列表
*/
- (NSArray*)allMethodsList;
/**
* 删除配置文件里定义的表,可以用于特殊情况下的数据还原。删除表后,DAO 对象仍可以正常使用,再次调用其它方法,会重新创建表。
*/
- (APDAOResult*)deleteTable;
/**
* 删除符合某个正则规则的所有表,请确保只删除本 Proxy 操作的表,否则可能发生异常
*
* @param pattern 正则表达式
* @param autovacuum 删除完成是否自动调用 vacuum 清理数据库空间
* @param progress 进度回调,可传 nil,回调不保证主线程。为百分之后的结果
*
* @return 操作是否成功
*/
- (APDAOResult*)deleteTablesWithRegex:(NSString*)pattern autovacuum:(BOOL)autovacuum progress:(void(^)(float progress))progress;
/**
* 调用自己的数据库链接执行 vacuum 操作
*/
- (void)vacuumDatabase;
/**
* DAO 对象可以自己把操作放在事务里提升速度,实际调用的是该 DAO 对象操作的数据库文件 APSharedPreferences 的 daoTransaction 方法。
*/
- (APDAOResult*)daoTransaction:(APDAOTransaction)transaction;
/**
* 创建一个数据库副连接,为接下来可能发生的并发select 操作加速使用。可以调用多次,创建多个数据库连接待用。
* 这些创建的链接会自动关闭,业务层无须处理。
* @param autoclose 在空闲状态多少秒后自动关闭,0 表示使用系统值
*/
- (void)prepareParallelConnection:(NSTimeInterval)autoclose;
@end
在文档使用中是否遇到以下问题
更多建议
匿名提交