使用Realm的一些总结

为什么我要用realm呢

前段时间新开了一个项目,在做技术选型的时候,尝试了一下把数据存储这块从FMDB换为Realm,最初的理由就是觉得Realm很酷,后来用了一段时间之后发现自己的选择是明智的:

  • 文档详细,甚至有中文版,当然中文版更新比较慢.
  • 使用中遇到问题去Stackoverflow基本上都能找到,Realm团队回答也特别快.
  • 查询速度超快(真正实现了懒加载,即用到时才从磁盘中查询).
  • 相比FMDB,API友好到爆炸.
  • 跨平台(我司iOS安卓都用了Realm).
  • 提供了Mac版Realm Browser方便查看数据,Mac app store下载即可.

安装

当然你可以直接下载Realm库拖到项目中,不过由于墙的存在,直接在realm官网下载会比较慢,强烈推荐使用CocoaPods或者Carthage安装,只需要pod 'Realm'或者在Cartfile中添加github "realm/realm-cocoa"即可.

使用

创建数据库

最简单的方式,使用默认配置

RLMRealm *realm = [RLMRealm defaultRealm];

默认情况下,realm数据库存储在Document路径下,默认数据库文件名字是default.realm,当然你也可以自定义这些选项:

 // 使用默认的目录,但是使用用户名来替换默认的文件名 
RLMRealmConfiguration *config = [RLMRealmConfiguration defaultConfiguration];
config.fileURL = [[[config.fileURL URLByDeletingLastPathComponent]  URLByAppendingPathComponent:@"yourname"] URLByAppendingPathExtension:@"realm"]; 
// 将这个配置应用到默认的 Realm 数据库当中
 [RLMRealmConfiguration setDefaultConfiguration:config];

默认情况下,realm是基于磁盘缓存的,但是假如我们有时候不想储存数据,又想需要灵活的进行数据读写,这时候我们可以创建一个内存数据库,创建内存数据库同样非常简单:

RLMRealmConfiguration *config = [RLMRealmConfiguration defaultConfiguration];
//指定inMemoryIdentifier即可
config.inMemoryIdentifier = @"MyInMemoryRealm";
RLMRealm *realm = [RLMRealm realmWithConfiguration:config error:nil];

但是用的时候我们要注意数据库实例被自动释放掉, 这就需要我们在app的生命周期内保持对realm内存数据库的强引用.

构建基于realm的数据model

构建一个基于realm的数据model非常简单,只需要继承自RLMObject即可,当然你也可以通过Xcode插件直接新建一个Realm class,不过我还是习惯手动创建.
以我的项目为例,我们创建一个Book模型:

@interface Book :RLMObject
@property (nonatomic, copy) NSString *ID;
@property (nonatomic, copy) NSString *title;
@property (nonatomic, copy) NSString *cover;
@property (nonatomic, assign) long date;
@property (nonatomic, strong) Day *firstDay;
@property (nonatomic, strong) RLMArray <Day *> *days;
@end
RLM_ARRAY_TYPE(Day)

@implementation Book
+ (NSString *)primaryKey {
    return @"ID";
}

//设置属性默认值
+ (NSDictionary *)defaultPropertyValues{ 
    return @{@"title":@"测试" };
}

//设置忽略属性,即不存到realm数据库中
+ (NSArray<NSString *> *)ignoredProperties {
    return @[@"days"];
}

//一般来说,属性为nil的话realm会抛出异常,但是如果实现了这个方法的话,就只有name为nil会抛出异常,也就是说现在cover属性可以为空了
+ (NSArray *)requiredProperties { 
    return @[@"name"];
}

//设置索引,可以加快检索的速度
+ (NSArray *)indexedProperties {
    return @[@"ID"];
}
@end

在这个Book模型中,有ID,title,cover,date,days这几个字段,realm支持BOOL,bool,int,NSInteger,long,long long,float,double,NSString,NSDate,NSData
以及 被特殊类型标记的 NSNumber这些类型.

  • Tips:如果想存储图片的话,可以把UIImage转为NSData存储,当然Realm限制了单个图片大小为16M,所以最好的测试是手动把图片存储到磁盘,然后Realm只尺寸图片的url,url可以是远程url或者本地的路径.

通过days这个字段可以看到realm实现To-One或者To-Many关系很简单,创建一对一关系的话直接定义为property即可,创建一对多关系的话,使用RLM_ARRAY_TYPE宏创建协议,然后定义RLMArray<Day*>类型就行啦.
通过primaryKey我们可以给model设置一个主键,这对于我们进行添加数据与更新数据的时候很有帮助,比如我们想要添加或者修改一条数据的话,只需要在事务中操作即可:

Book *book = [[Book alloc] init];
book.ID = @"ABCDEFG";
book.title = @"威尼斯之行";
book.cover = @"www.google.com";
//添加数据
RLMRealm *realm = [RLMRealm defaultRealm];
[realm beginWriteTransaction];
[realm addObject:book];
[realm commitWriteTransaction];

//修改数据
[realm beginWriteTransaction];
book.title = @"美西之行";
[realm commitWriteTransaction];

如果我们设置了主键(primaryKey),就可以直接这样,直接调用createOrUpdateInRealm:realm函数,如果ID为ABCDEFG的Book对象已经存在,那么对象就会直接更新,如果不存在,就会创建一个,这一点特别像在写rails的时候操作数据库的方法,非常方便

Book *book = [[Book alloc] init];
book.ID = @"ABCDEFG";
book.title = @"美西之旅";
book.cover = @"www.apple.com";
[realm beginWriteTransaction];
[Book createOrUpdateInRealm:realm withValue:book];
[realm commitWriteTransaction];

//还可以这样,直接通过键值对进行更新
[realm beginWriteTransaction];
[Book createOrUpdateInRealm:realm withValue:@{@"ID": @"ABCDEFG", @"title": @"美西之旅"}];
[realm commitWriteTransaction];

Realm数据库的CURD

C(create) & U(update)

其实在上面我们已经简单的说过创建跟更新的方法了,一般来说如果你设置了主键的话,会用的最多的基本上就是createOrUpdateInRealm方法,而且项目中我们一般面向对象编程,会把键值对,json等数据使用一些类似Mantle,YYModel的工具转换为对象,所以其他类似于通过字典创建更新数据,支持属嵌套属性(Nested Object)的创建等功能就不多细说了.

R(Retrieve)读取数据

Realm的数据查询非常强大,基本上接触过谓词NSPredicate之后都可以快速上手,查询的结果存储为RLMResults<model*>的容器,其实这玩意完全可以当做NSArray来用,同样支持下标操作,支持快速遍历,不同的是RLMResult需要指定类型,比如RLMResult<Book*>就是包含多个book的集合,另外我们开篇提到的Realm是懒加载的,也就是说查询到的结果只会在被确定访问某个属性的时候才去读取,否则我们的查询操作将会被延迟执行.
接下来我们举几个查询的例子:

//从默认数据库查询所有的书
RLMResults<Book *> *books = [Book allObjects];

//使用断言字符串查询
RLMResults<Book *> *books = [Book objectsWhere:@"title = '美西之旅 AND cover = 'www.apple.com''"]

//使用NSPredicate查询
NSPredicate *pred = [NSPredicate predicateWithFormat:@"title = '美西之旅 AND cover = 'www.apple.com''"];
books = [Book objectsWithPredicate:pred];

//链式查询
RLMResults<Book *> *books = [Book objectsWhere:@"title = '美西之旅'"];
RLMResults<Book *> *otherBooks = [books objectsWhere:@"cover = 'www.apple.com'"];

//排序
//按照data从大到小进行排序
RLMResults<Book *> *sortedBooks = [[Book objectsWhere:@"title = '美西之旅' "] sortedResultsUsingProperty:@"date" ascending:YES];

Tips1:有时候我们确实想使用NSArray而不是RLMArray或者RLMResult,我们可以简单的进行遍历转换:

//假设这是我们查询到的结果
RLMResult<Day*>days;
NSMutableArray *array = [NSMutableArray array];
    for (Day *day in days) {
        [array addObject:day];
}

Tips2:写程序的时候前人一直告诉我们DRY原则(Dont Repeat Yourself),每次写beginWriteTransactioncommitWriteTransaction也确实很让人烦,我们可以稍微封装一个方法:

- (void)update:(void (^)())block {
    [self.realm beginWriteTransaction];
    block();
    [self.realm commitWriteTransaction];
}

这样用的时候就可以这么用:

[book update:^{
     book.cover = @"new cover";
}];

更多关于断言的使用比如== ,<=,>=,AND,BETWEEN BEGINSWITH等使用方法可以查看这里的文档

D(Delete)删除数据

在realm中想要删除数据非常简单,假设我们有一个book模型需要删除,只需要:

//删除单条记录
[realm beginWriteTransaction];
[realm deleteObject:book];
[realm commitWriteTransaction];

//清空realm数据库,清空后Realm文件不会释放掉所占用的空间,这是为了保留空间以便日后提高存储速度
[realm beginWriteTransaction];
[realm deleteAllObjects];
[realm commitWriteTransaction];

我们可以看到,大多数操作都是在一个事务中进行的,如果在错误的事务中进行了数据的操作,realm会抛出异常.

Realm中的线程

首先单个线程中,我们当然无需考虑并发或者多线程处理的问题,也无需考虑线程锁,我们唯一的修改的操作也是在对象自己的realm事务中.
需要特别注意的一点是,我们不能让多个线程拥有同一个Realm对象的实例.
如果想要跨线程使用数据库的话,我们需要在每个线程都去初始化一个新的Realm实例,如果都是执行的同一个数据库的话,这些实例都是指向磁盘的同一个文件的.
Realm官方给了一个结合GCD(大中枢派发233333)大批量写入数据的例子:

dispatch_async(queue, ^{
    @autoreleasepool {
        // 在这个线程中获取 Realm 和表实例
        RLMRealm *realm = [RLMRealm defaultRealm];
        
        // 通过开启写入操作将写入闭包分成多个微小部分
        for (NSInteger idx1 = 0; idx1 < 1000; idx1++) {
            [realm beginWriteTransaction];
            
            // 通过字典插入行,忽略属性次序
            for (NSInteger idx2 = 0; idx2 < 1000; idx2++) {
                [Person createInRealm:realm
                            withValue:@{@"name"      :  randomString,
                                        @"birthdate" : randomDate}];
            }
            
            // 提交写入事务以确保数据在其他线程可用
            [realm commitWriteTransaction];
        }
    }
});

对JSON的支持

很遗憾Realm并不直接支持JSON(那你说个卵啊~)...
我们可以使用系统自带的系列化函数来序列化JSON数据

NSData *data = [@"{\"name\": \"旧金山\", \"cityId\": 123}" dataUsingEncoding: NSUTF8StringEncoding];
[realm transactionWithBlock:^{ 
    id json = [NSJSONSerialization JSONObjectWithData:data options:0 error:NULL]; 
    [City createOrUpdateInRealm:realm withValue:json];}
];

当然其实真正的工作中我们一般也不这么来搞,一般会用Mantle,YYModel等其他类似工具将json转换为对象之后直接进行存储,后面我也打算写一些Realm+YYModel的使用心得.

如何查看存储在Realm中的数据

如果使用模拟器进行调试,可以通过

[RLMRealmConfiguration defaultConfiguration].fileURL

打印出Realm 数据库地址,然后在Finder中⌘⇧G跳转到对应路径下,用Realm Browser打开对应的.realm文件就可以看到数据啦.
使用真机调试的话
Xcode->Window->Devices(⌘⇧2),然后找到对应的设备与项目,点击Download Container

Paste_Image.png

导出xcappdata文件后,显示包内容,进到AppData->Documents,使用Realm Browser打开.realm文件即可.

遇到的一些坑或者不爽的地方

当然,这么一个还算比较新的工具说是完美的肯定是不可能,我在使用中也在经常地骂娘,虽然后来发现好多是使用姿势不对23333

  1. 所有的操作都要在事务中,麻烦,不过我们可以像文中tips那样进行小小的封装.
  2. 要时刻注意操作的是不是同一个realm实例,不然会crash,crash log中,iOS跟安卓一样,90%的crash都是跟realm有关😭,还要注意对象是否可用,这个我们可以用invalidated字段来判断,也算是防御式编程了23333....
  3. 写数据迁移(migrate)不算太友好,不太喜欢,长期下来,迁移的代码画美不看......也可能是自己功力不够,所以文章中自己也没写关于迁移的内容,想要了解的可以看官方的migrate demo,什么时候移动端写数据迁移能像写rails那么爽就好了....
  4. 跨线程传递数据还算麻烦,不过可以接受.
  5. realm对象如果调用class方法会返回类似于RLMAccessor_2_Day的结果,而不是预想的Day,这点注意一下就好.
  6. RealmC++实现的,所以看着一堆.mm的源码,对我来说基本不会产生去阅读.的想法
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,117评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,328评论 1 293
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,839评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,007评论 0 206
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,384评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,629评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,880评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,593评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,313评论 1 243
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,575评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,066评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,392评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,052评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,082评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,844评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,662评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,575评论 2 270

推荐阅读更多精彩内容

  • 前言 由于最近项目中在用Realm,所以把自己实践过程中的一些心得总结分享一下。 Realm是由Y Combina...
    一缕殇流化隐半边冰霜阅读 72,327评论 213 516
  • 跨平台:现在很多应用都是要兼顾iOS和Android两个平台同时开发。如果两个平台都能使用相同的数据库,那就不用考...
    CoderZS阅读 2,372评论 2 16
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,100评论 18 139
  • 我是个多梦的孩子,只要入睡就会做梦。 只要逢节,我都会梦见爸爸! 前两天又梦见了爸爸,也记不清梦中的情节了! 今天...
    晴致生活馆阅读 392评论 8 1
  • title: 漏洞扫描基本概念date: 2016-06-15 10:30tags: Kali渗透测试 漏洞扫描 ...
    曼路x_x阅读 4,358评论 0 1