Realm
创建数据库
使用RLMRealm *realm = [RLMRealm defaultRealm];
默认的数据库配置,或者使用+ (nullable instancetype)realmWithConfiguration:(RLMRealmConfiguration *)configuration error:(NSError **)error;
进行全方位的配置。
调用了方法后数据库创建完成。
创建表
创建表也是在RLMRealm
构建的方法里完成的,牛逼的地方在于:它使用objc_copyClassList
把所有注册的类给拿到,然后把是从RLMObject
继承的类提取出来,把这些类生成对应的表。
所以对于使用者而言,只需要定义数据模型,并且这些模型类从RLMObject
继承。
- 属性就跟普通类一样定义,只是把属性的描述关键词去掉。大概是因为属性的getter/setter全部被重写了,这些属性都没用了。
Realm ignores Objective‑C property attributes like nonatomic, atomic, strong, copy, weak, etc. These aren’t meaningful for Realm storage; it has its own optimized storage semantics.
- 一对多属性
//这个协议不知道什么用
RLM_ARRAY_TYPE(Book)
//前一个尖括号是泛型,即数组的元素类型,后一个是协议
@property (nonatomic) RLMArray<Book*><Book> *books;
- 反向关系
图书馆(Library)里有书,属性books
,书(Book)可以属于图书馆,属性owner
。如果一本书加到一个新的图书馆里,那么是修改了Library的books,但是Book的owner不会发生改变。也就是两个属性有相互影响,让其中一个依赖另一个,这样维护一个属性的修改就可以了。
让owner
跟随books
修改:
//类Book
@property (readonly) RLMLinkingObjects *owners;
+(NSDictionary<NSString *,RLMPropertyDescriptor *> *)linkingObjectsProperties{
return @{
@"owners" : [RLMPropertyDescriptor descriptorWithClass:Library.class propertyName:@"books"]
};
}
- 还可以给RLMObject设置主键primaryKey,默认值defaultPropertyValues,忽略的属性ignoredProperties,必要属性requiredProperties,索引indexedProperties。比较有用的是主键和索引。
数据操作
增
Library *library = [[Library alloc] init];
[realm transactionWithBlock:^{
[realm addObject:library];
}];
构建和复制跟普通对象一样,存入数据库的时候使用事务。添加后对象就由realm管理了,对它属性的修改必须在写事务内操作,否则奔溃。
Terminating app due to uncaught exception 'RLMException', reason: 'Attempting to modify object outside of a write transaction - call beginWriteTransaction on an RLMRealm instance first.'
删改
[realm transactionWithBlock:^{
bk.name = @"和谐世界2";
[realm deleteObject:bk];
}];
查询
//查询全部
[Book allObjects]
//条件查询
RLMResults<Book *> *results = [Book objectsWhere:@"age == 101"];
where之后的字符串是用来构建NSPredicate的,所以按照它的语法来写。
自动更新
两个对象是对应着数据库里同一个数据,那么其中一个对象修改了,提交给数据库,另外一个对象也会自动跟随修改。
RLMResults *results = [Book objectsWhere:@"age == 119"];
Book *bk = results.firstObject;
NSLog(@"1: %@",bk.name);
RLMResults *results2 = [Book objectsWhere:@"age == 119"];
Book *bk2 = results2.firstObject;
NSLog(@"2: %@",bk2.name);
[realm transactionWithBlock:^{
bk.name = [NSString stringWithFormat:@"%@_修改+1",bk.name];
}];
NSLog(@"3: %@",bk2.name);
第三次输出的名称就是修改后的名称了。
查询结果也可以自动更新
但是这些更新都限于当前线程,realm不支持跨线程的数据共享,新的线程需要新的RLMRealm
对象且从新读取新的数据对象。
数据迁移
RLMRealmConfiguration *config = [RLMRealmConfiguration defaultConfiguration];
config.schemaVersion = 2;
config.migrationBlock = ^(RLMMigration * _Nonnull migration, uint64_t oldSchemaVersion) {
//数据迁移代码
if (oldSchemaVersion < 1) {
[migration enumerateObjects:Book.className block:^(RLMObject * _Nullable oldObject, RLMObject * _Nullable newObject) {
}];
}
};
[RLMRealmConfiguration setDefaultConfiguration:config];
realm的数据迁移逻辑是:
- 判断
config.schemaVersion
的版本是否和数据库相同,不同且大于0执行数据迁移操作 - 根据当前的类构建一个新的realm(realm的表是由类自动生成的,这一点优势也在这体现出来了),然后执行
config.migrationBlock
给新的realm填充数据 - 如果
migrationBlock
啥也不干,其实新的表也会建起来,只是之前的数据丢失了。所以这里可以理解为两步:1.realm自动完成新的表的构建 2.我们在migrationBlock
里完成对新表数据的填充 - 在
migrationBlock
里面有旧的版本号,这样可以一步步的升级上来。简单说就是,有了新版本,加入这一次的迁移代码,下一次的时候不要删除前面的代码,这样就可以逐步更新了,如:
config.migrationBlock = ^(RLMMigration * _Nonnull migration, uint64_t oldSchemaVersion) {
//数据迁移代码
if (oldSchemaVersion < 1) {
//从0更新到1的操作
}
if (oldSchemaVersion < 2) {
//从1更新到2的操作
}
if (oldSchemaVersion < 3) {
//从2更新到3的操作
}
};
如果直接从版本0、1、2直接更新到3,也可以把第三步放到前面,看具体需求。
最后realm并不是基于sqlite的,是另写的数据库。
FMDB
FMDB只是针对sqlite做的轻量级的封装,没有模型和表的映射、没有对数据的监控、也没有数据迁移的帮助等等ORM的特性,只是把原本需要执行sql的操作做了一个函数封装。
建库
NSString *dbPath = [NSHomeDirectory() stringByAppendingString:@"/Documents/book.db"];
FMDatabase *database = [FMDatabase databaseWithPath:dbPath];
构建一个FMDatabase
对象即可。
不过使用之间要打开数据库,建立数据库连接:[database open]
建表
很普通的执行sql语句:
BOOL state = [database executeStatements:
@"create table if not exists Book (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, age INTEGER)"];
if (!state) {
NSLog(@"create table error!");
return;
}
插入数据和更新数据
Book *bk = [[Book alloc] init];
bk.name = @"Big World";
bk.id = 123;
bk.age = 10000;
NSDictionary *bkKeyValues = @{
@"id":@(bk.id),
@"name":bk.name,
@"age":@(bk.age)
};
[database executeUpdate:
@"insert into Book values(:id, :name, :age)" withParameterDictionary:bkKeyValues];
核心方法是executeUpdate:
,这个函数是建立在sqlite3_prepare_v2
上的。
新增或者更新数据的时候,可以直接在sql语句内嵌入内容:
insert table Book (name) values('sqlite权威指南')
。
但如果数据比较长,则在内容部分填入占位字符,表示实际值后面再绑定:
- sql语句: "insert into Book values(:name, :age)"
- 使用
sqlite3_prepare_v2
执行语句 - 再用
sqlite3_bind_xxx
系列的函数绑定实际的数据。比如name字段是字符串,使用sqlite3_bind_text(stmt,1,"sqlite权威指南")
,这个索引是从1开始算的。
而占位字符有几种格式:?
,?number
,:string
,@string
,$string
,单纯的问号就是占位,它的索引是自动分配的,第二种number就是指定了索引,后面几种可以通过sqlite3_bind_parameter_index
这个函数来查找对应的索引,传入的内容就是:string
后面字符的内容。
在FMDB里,如果你更新使用字典来传入数据,就是使用:string
这种占位符,通过string内容:1. 从sqlite这边得到索引 2.从字典里拿到字段对应数据 ,把1和2的内容关联起来,使用sqlite3_bind_xxx
函数传入。
如果使用数组或者变参的方式传入数据,那么索引和数组里的数据意义对应:
NSArray *infos = @[bk.name, @(bk.age)];
[database executeUpdate:@"insert into Book (name, age) values(?,?)" withArgumentsInArray:infos];
这里占位可以使用最简单的问号?
,那么第一个占位就使用数组里第一个数据,第二个占位就使用第二个数据,依次类推。变参方式传入就是对应第一个参数,第二个参数......
也就是说,这个函数的核心是如何处理绑定参数索引和实际值之间的对应关系,理解了这个问题,这个函数就理解了。
更新数据跟插入数据逻辑一致,也是使用这个方法。
查询数据
查询时的输入逻辑和上面一样,使用sqlite3_prepare_v2
处理sql语句,使用占位字符和sqlite3_bind_xxx
来传入数据。查询时where
、limit
、offset
、order by
这些的值都可以这么处理。
插入和更新数据时只要执行完操作就可以了,而查询执行完executeQuery
函数后,得到的只是FMResultSet
对象,还要把数据提取出来:
NSDictionary *queryInfos = @{@"name":@"insert array", @"limitx":@(2), @"order":@"id desc", @"ment":@"desc"};
FMResultSet *result = [database executeQuery:@"select name, id from Book where name = :name order by :order limit :limitx " withParameterDictionary:queryInfos];
提取数据:
NSMutableArray *models = [[NSMutableArray alloc] init];
while ([result next]) {
Book *book = [[Book alloc] init];
book.id = [result longLongIntForColumnIndex:1];
book.name = [result stringForColumnIndex:0];
// book.age = [result longLongIntForColumnIndex:0];
[models addObject:book];
}
不断使用next
函数调到下一条数据,内部核心是sqlite3_step
。然后使用longLongIntForColumnIndex
等一系列方法把属性值一个个的提取出来,赋值到对象上。
这里便是原生的sqlite处理中最痛苦的一个环节,对于"一条数据-->一个对象"的转变需要一个字段一个字段的去处理。这样:
- 字段多写的很痛苦,都是枯燥的代码
- 对于每个表/模型都需要写一套代码,工作量大,而且一旦模型变动这个代码就要改。
这时就需要ORM来拯救世界了,可惜CoreData除了这个工作之外还有很多的功能,甚至把一些细节都封闭了,导致用起来反而挺麻烦的。
ORM, Object Relational Mapping的简写,从这里的工作里就可以很好的理解这个东西的意思。对象关系映射,把一种对象映射到另一种对象,这里工作的根本就是把数据库里的一条数据(数据库对象)自动的转为我们定义的模型对象。
多线程环境
使用FMDatabaseQueue
来调用:
FMDatabaseQueue *dbQueue = [[FMDatabaseQueue alloc] initWithPath:dbPath];
[dbQueue inDatabase:^(FMDatabase * _Nonnull db) {
[db executeUpdate:@"insert into Book values(200, 'dbQueueInsert', 2013)"];
}];
这是一个保守的方案:
dispatch_sync(_queue, ^() {
FMDatabase *db = [self database];
block(db);
...
}
...
_queue = dispatch_queue_create([[NSString stringWithFormat:@"fmdb.%@", self] UTF8String], NULL);
这个_queue
是一个串行的队列,所以使用FMDatabaseQueue
来访问数据库,不管你在哪个线程调用,最后都到这个里_queue
里处理,而它又是串行的,所以不会出现并发的情况。没有并发,就没有多线程的各种问题了。
如果你采用这种方案,那么就有建一个全局的FMDatabaseQueue
,在哪都用它,不同的queue之间是互发限制的,还是会并发,还是会触发问题。
再者,因为_queue
是串行的,而这里又使用了同步的方法dispatch_sync
,所以嵌套调用会导致死锁。FMDB做了队列的检测,在同一个队列里调用inDatabase
会crash。
//使用dispatch_get_specific获取队列标识
FMDatabaseQueue *currentSyncQueue = (__bridge id)dispatch_get_specific(kDispatchQueueSpecificKey);
assert(currentSyncQueue != self && "inDatabase: was called reentrantly on the same queue, which would lead to a deadlock");
说这个是保守方案是因为,它完全的隔绝了多线程访问的这种操作,每个操作都依次进行,不并发。但实际至少读和读之间是可以共存的。
其他库
key-value数据库YTKKeyValueStore
微信开源数据库wcdb
尝试
sqlite有挺多的特性可以支持ORM的实现,所以参照realm的一些思路,如runtime加载创建表,在FMDB的基础上实现了一个简单的ORM:可以自动建表,脱离sql进行增删改查。考虑到微信开源数据库wcdb和realm都已经是很成熟的方案了,我写了也没多大用,所以没有做太多的完善,只是算作一个尝试,让自己熟悉一下ORM的想法和对sqlite的熟悉。有兴趣的可以看一下TFDatabaseMapper。