走进Realm的世界
Realm是什么
Realm是由Y Combinator公司孵化出来的一款可以用于iOS(同样适用于Swift&Objective-C)和Android的跨平台移动数据库。历经几年才打造出来,为了彻底解决性能问题,核心数据引擎用C++打造,并不是建立在SQLite之上的ORM,所以Realm相比SQLite和CoreData而言更快、更好、更容易去使用和完成数据库的操作花费更少的代码。它旨在取代CoredData和sqlite,它不是对coreData的简单封装、相反的,Realm它使用了它自己的一套持久化存储引擎。而且Realm是完全免费的,这不仅让它变得更加的流行也使开发者使用起来没有任何限制。
Realm的特点
Realm以难以令人置信的快速和易用让开发者能够用仅仅几行代码完成你所需要的一切功能。它旨在打造让用户得在到移动领域离线时的最好体验,我整理了Realm具有的如下特点:
- 易安装:正如你在将要看到的使用Realm工作。安装Realm就像你想象中一样简单。在Cocoapods中使用简单命令,你就可以使用Realm工作。
- 速度上:Realm是令人无法想象的快速使用数据库工作的库。Realm比SQLite和CoreData更快,这里的数据就是最好的证明。
- 跨平台:Realm数据库文件能够跨平台和可以同时在iOS和Andriod使用。无论你是使用Java, Objective-C, or Swift,你都可以使用你的高级模型。
- 可扩展性:在开发你的移动App特别是如果你的应用程序涉及到大量的用户和大量的记录时,具有良好的可扩展性是非常重要的。
- 好的文档&支持:Realm团队提供了可读的,非常有组织的并且丰富的文档。如果你遇到什么问题通过Twitter、GitHub或者Stackoverflow与他们交流。
- 可靠性:Realm已经被巨头公司使用在他们的移动App中,像Pinterest, Dubsmash, and Hipmunk。
- 免费性:使用Realm的所有功能都是免费的。
- 懒加载:只有当你真正访问对象的值时候才真正从磁盘中加载进来。
必备条件
- iOS7及以后, OS X 10.9及以后。
- 至少Xcode7.3及以后。
- Realm针对iOS分为两部分,一个是Swift版本,一个是Objective-C版本。
Realm安装
- 动态Framework库。直接下载最新Realm的release版本,拖入项目工程中即可。
- CocoaPods管理工具。使用CocoaPods0.39版本以及上。
- Carthage管理工具。使用Carthage 0.17.0版本及以上。
- 静态Framework库。直接下载最新Realm的release版本,拖入项目工程中即可。
(注意:本文将重点讲解使用CocoaPods的方式安装Realm到项目中)
配置Xcode和所需的工具
在配置Xcode项目之前,请确保你的电脑上已经安装了CocoaPods,因为我们会使用CocoaPods在项目中安装Realm。如果你不熟悉CocoaPods,你可以查看给你的网上的相关教程。
现在创建一个新的工程,选择"Single View Application",命名为RealmDemo或者任何你想的名称。请确保创建的项目是Objective-C语言。现在在终端打开你的项目目录并执行如下的CocoaPods命令:
Pod init
然后,使用Xcode打开Podfile文件,向其中添加如下内容:
realm-podfile.png接下来执行Pod install命令去下载Realm安装到项目中。完成之后,你将看到新的Xcode项目的workspace在Podfile文件被生成。请打开Xcode的RealmDemo.xworkspace和不要打开xcodeproj。打开workspace之后,你将看到Pod相关文件已经被集成进去了。
现在Xcode已经准备好可以使用Realm工作了,但是我们还将需要安装如下工具为的是让我们使用Realm更轻松,容易。
在Xcode中安装Realm插件(仅仅用于Xcode8以下版本)
Realm团队已经提供了非常有用的Xcode插件,它将被使用在生成Realm的模型。为了安装插件,我们使用Alcatraz.对于那些不了解什么是Alcatraz的人来说,可以自行去搜索下,Alcatraz真的非常好用。为了安装Alcatraz需要通过以下终端中的命令行并且重启你的Xcode。
curl -fsSL https://raw.githubusercontent.com/supermarin/Alcatraz/master/Scripts/install.sh | sh
之后在Xcode中,选择Window,就可以看到下拉列表中的Package Manager选项,就表明已经安装成功。
在Xcode弹出窗口中选择需要的插件或者模板,在搜索框中你可以搜索任何插件或者各种各样的模板去自定义你的Xcode。在搜索框中输入"Realm"和"RealmPlugin"插件将显示出来,点击进行安装。
正如你看到的Class RealmTestClass1,它有1000个记录和它展示了不同的列数据。接下来我们将讨论Realm模型支持的属性类型。
Realm的数据库创建
目前Realm提供了3种方式创建数据库对象,一种是存储在默认路径下的数据库,一种是我们可以指定自己指定数据库文件的存储路径和只读属性,还有一种可以使用内存数据库。
- 默认Realm数据库
RLMRealm *realm = [RLMRealm defaultRealm];
- 自定义数据库(名称、路径)
//注意:新版本中关于配置Relam的文件路径包括名称等等都是在RLMRealmConfiguration对象中完成。
RLMRealmConfiguration *config = [RLMRealmConfiguration defaultConfiguration];
// Use the default directory, but replace the filename with the username
config.fileURL = [[[config.fileURL URLByDeletingLastPathComponent]
URLByAppendingPathComponent:username]
URLByAppendingPathExtension:@"realm"];
// Set this as the configuration used for the default Realm
[RLMRealmConfiguration setDefaultConfiguration:config];
或者是
NSString *docPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject];
NSString *dbPath = [docPath stringByAppendingPathComponent:@"guojie.realm"];
RLMRealm *realm1 = [RLMRealm realmWithURL:url];
当然了,RLMRealmConfiguration对象中还可以设置是否只读数据库
RLMRealmConfiguration *config = [RLMRealmConfiguration defaultConfiguration];
// Get the URL to the bundled file
config.fileURL = [[NSBundle mainBundle] URLForResource:@"MyBundledData" withExtension:@"realm"];
// Open the file in read-only mode as application bundles are not writeable
config.readOnly = YES;
- 内存数据库
正常的Realm数据库是存储在硬盘上的,但是我们可以通过在RLMRealmConfiguration中设置inMemoryIdentifier属性来创建一个内存数据库。
RLMRealmConfiguration *cfg = [RLMRealmConfiguration defaultConfiguration];
cfg.inMemoryIdentifier = @"test";
RLMRealm *realm = [RLMRealm realmWithConfiguration:cfg error:nil];
注意:内存数据库在每次程序退出时不会保存数据。如果某个内存Realm实例没有被引用,所有的数据在实例对象释放的适合也会被释放。建议你在app中用强引用来钳制所有新建的内存Realm数据库实例。
Realm的构建数据模型
Realm的数据模型是用传统的 Objective-C 接口(interface)和属性(@property)定义的。 只要定义 RLMObject的一个子类或者一个现成的模型类,你就能轻松创建一个Realm的数据模型对象。Realm模型对象和其他的Objective-C的功能很相似–你可以给它们添加你自己的方法和protocol然后和其他的对象一样使用。 唯一的限制就是从它们被创建开始,只能在一个进程中被使用。如果你已经安装了Xcode Plugin(Xcode版本<8.0),你将直接很方便在Xcode中的New File对话框中选择Realm Model Object类的模板创建。你可以用它来创建interface和implementation文件。废话不说,直接上代码。
现在一起来使用Xcode中的Realmh插件去创建Realm类。打开Xcode和创建新文件。在右边框中选择Realm:
Dog_objcetive-c.png用一个Dog对象来表示一条小狗,紧接向类中添加属性。然后类在添加属性之后向如下这样:
- Dog.h
@interface Dog : RLMObject
/** 名字 */
@property (nonatomic, copy) NSString *name;
/** 年龄 */
@property (nonatomic, assign) NSInteger age;
/** 品种 */
@property (nonatomic, copy) NSString *type;
RLM_ARRAY_TYPE(Dog)
@end
- Dog.m
@implementation Dog
/**
设置主键
*/
+ (NSString *)primaryKey{
return @"numId";
}
/**
添加索引的属性
*/
+ (NSArray<NSString *> *)indexedProperties{
return @[@"name"];
}
/**
添加默认值
*/
+ (NSDictionary *)defaultPropertyValues{
return @{@"type":@"taidi"
};
}
@end
说明:
- Realm支持的属性类型如下:BOOL, bool, int, NSInteger, long, float, double, CGFloat, NSString, NSDate 和 NSData。
- 你可以使用RLMArray<Object>和RLMObject来模拟对一或对多的关系(Realm也支持RLMObject继承)
- Realm忽略了Objective-C的property attributes(如nonatomic, atomic, strong, copy, weak 等等)。 所以,推荐在创建模型的时候不要使用任何的property attributes。但是,假如你设置了,这些attributes会一直生效直到RLMObject被写入realm数据库。
- 定义了RLM_ARRAY_TYPE(Dog) 这个宏表示支持RLMArray<Dog>该属性
- 另外Realm提供了以下几个方法供对属性进行自定义:
- (NSArray<NSString *> *)indexedProperties;: 可以被重写来来提供特定属性(property)的属性值(attrbutes)例如某个属性值要添加索引。
- (nullable NSDictionary *)defaultPropertyValue;: 为新建的对象属性提供默认值。
- (nullable NSString *)primaryKey;: 可以被重写来设置模型的主键。定义主键可以提高效率并且确保唯一性。
- (nullable NSArray *)ignoredProperties;:可以被重写来防止Realm存储模型属性。
- (void)transactionWithBlock:(attribute((noescape)) void(^)(void))block;:自动更新对象数据。
- (NSArray<NSString *> *)requiredProperties;可选属性
Realm的数据增删改查
存储数据
(1).创建Dog对象,依次设置各个属性
```Objective-C
Dog *myDog = [[Dog alloc]init];
myDog.numId = @"1";
myDog.name = @"haha";
myDog.age = 10;
myDog.type = @"taidi";
```
(2).使用字典创建Dog对象
```Objective-C
Dog *myDog2 = [[Dog alloc]initWithValue:@{@"numId":@"2",
@"name" :@"xiaoxiao",
@"age" :@11,
@"type" :@"heibei"
}];
```
(3).使用数组创建Dog对象
```Objective-C
Dog *myDog3 = [[Dog alloc]initWithValue:@[@"3",@"小小",@12,@"smy"]];
```
(4).除此之外,还支持嵌套的对象
如果在对象的属性中有RLMObjects对象或者RLMArrays对象,在这种情况下当你使用字典或者数组途径创建对象时,你就可以使用数组或者字典对象替换属性来表示它的属性。例如下面的嵌套对象例子:
// Instead of using already existing dogs...
Person *person1 = [[Person alloc] initWithValue:@[@"Jane", @30, @[aDog, anotherDog]]];
// ...we can create them inline
Person *person2 = [[Person alloc] initWithValue:@[@"Jane", @30, @[@[@"Buster", @5],
@[@"Buddy", @6]]]];
说明:
1. 最明显的是使用指定的初始化程序创建一个对象。注意被设置为必须的属性在被添加之前一定要被设置值。
2. 对象可以通过传入与属性名称一样的字典Key和Values来创建。
3. 最后,对象可以使用数组创建,需要注意的是在数组中值的顺序必须跟Model中相应属性顺序保持一致。
4. 需要注意的是: RLMArray仅仅只能包含RLMObject对象,而不包括基本数据类型例如NSString。
(5).存储数据到Realm中
RLMRealm *realm = [RLMRealm defaultRealm];
[realm beginWriteTransaction];
[realm addObject:myDog];
[realm commitWriteTransaction];
请注意:当你同时执行多个写入操作时,它们将会阻碍执行的所在线程。所以你应该考虑在单独的线程中处理而不是UI线程。另一个就是,当你在执行写入操作时,读取操作不会被阻塞。这一点是很有用的,因为你的App可以执行许多读取操作时在后台同一时间写入事务可能也会在后台执行中。
删除数据
- 删除指定的数据:
- (void)deleteObject:(RLMObject *)object;
- 删除一组数据:
- (void)deleteObjects:(id)array;
- 删除全部数据:
- (void)deleteAllObjects;
修改数据
如果该条数据不存在则会新家一条数据。
- 针对单条数据进行的修改或新增:
- (void)addOrUpdateObject:(RLMObject *)object;
- 针对一组数据的修改或新增:
- (void)addOrUpdateObjectsFromArray:(id)array;
- 针对带有主键的数据修改或新增
如果你的模型类中包含一个主键,Realm可以智能的以这个主键来更新或添加数据。
- (void)addOrUpdateObject:(RLMObject *)object;
- 使用KVC修改数据
如果你是一个iOS开发者,你将很熟悉KVC,Realm类中例如对象、结果集、list也是兼容KVC的。这样可以帮助你利用runtime设置/更新属性。看看这个例子:
RLMResults<Dog *> *dogs = [Dog allObjects];
[[RLMRealm defaultRealm] transactionWithBlock:^{
[[Dog firstObject] setValue:@20 forKeyPath:@"age"];
// set each person's name property to "wangwu"
[dogs setValue:@"wangwu" forKeyPath:@"name"];
}];
说明:对于增加、删除、修改必须要在事务中进行操作。
查询数据
查询会返回RLMResults结果集,它里面包含了RLMObject对象的集合。RLMResults的接口方法非常类似于NSArray一样可以使用下标索引访问元素。与NSArray不同的是,RLMResults的元素类型只有单一的RLMObject的子类型。所有的查询都是懒查询,即只有当访问属性时,数据才会被读取出来。查询的结果不是拷贝本地磁盘中的数据而是直接原始数据,所以修改某一处的数据将会直接修改磁盘中的数据。在结果集被使用的过程中,查询的执行是会被延迟的。这意味着链接几个临时中间的RLMResults所进行的排序和过滤数据的操作不执行额外的工作去处理中间的状态。一旦查询已经被执行或者通知block已经被添加,RLMResult在可能的情况下在后台线程上执行查询以保证RLMResult的结果始终是Relam中最新的。
- 查询全部数据
RLMResults<Dog *> *dogs = [Dog allObjects];
或者指定Realm数据库:
NSString *path = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject];
NSString *realmPath = [path stringByAppendingPathComponent:@"guojie.realm"];
RLMRealm *realm = [RLMRealm realmWithPath:realmPath];
RLMResults *results = [Dog allObjectsInRealm:realm];
在条件查询中,如果你熟悉谓词,那么你已经知道该如何查询了。RLMObjects, RLMRealm, RLMArray, and RLMResults都提供了允许让你通过传递一个谓词实例、谓词字符串、谓词格式字符串来查询具体RLMObject实例的方法。
- 条件查询
假设要查询名字是guojie和年龄是10的dog对象:
RLMResults *results = [Dog objectsWhere:@"name = 'haha' AND age = 10"];
也可以使用谓词查询:
NSPredicate *pred = [NSPredicate predicateWithFormat:@"type = '%@' AND name = '%@'", @"taidi", @"haha"];
RLMResults *results = [Dog objectsWithPredicate:pred]
- 条件排序
假设已经查询出type是taidi,name是haha的数据结果集后,需要将结果按照num字段进行递减排序:
NSPredicate *pred = [NSPredicate predicateWithFormat:@"type = '%@' AND name = '%@'", @"taidi", @"haha"];
RLMResults *results = [Dog objectsWithPredicate:pred];
results = [results sortedResultsUsingProperty:@"numId" ascending:NO];
请注意:结果集的顺序只能保证在查询排序时保持一致,出于性能的原因,查询顺序不保证被保存。如果你需要为何查询的顺序,则需要其他一些解决方案。
- 链式查询(结果过滤)
假设要查询的还是type是taidi,name是haha的结果集,还可以这样处理:
RLMResults *results1 = [Dog objectsWhere:@"type = 'taidi'"];
RLMResults *results2 = [results1 objectsWhere:@"name = 'haha'"];
通知
每当一次写事务完成Realm实例都会向其他线程上的实例发出通知,可以通过注册一个block来响应通知:
self.token = [realm addNotificationBlock:^(NSString *note, RLMRealm * realm) {
[_listTableView reloadData];
}];
只要有任何的引用指向这个返回的notification token,它就会保持激活状态。在这个注册更新的类里,你需要有一个强引用来钳制这个token, 因为一旦notification token被释放,通知也会自动解除注册。
@property (nonatomic, strong) RLMNotificationToken *token;
另外可以使用下面的方式解除通知:
[realm removeNotification:self.token];
Realm版本迁移
当你和数据库打交道的时候,你需要改变数据模型(Model),但是因为Realm中的数据模型被定义为标准的Objective-C interfaces,要改变模型,就像改变其他Objective-C interface一样轻而易举。举个例子,假设有个数据模型Person:
@interface Person : RLMObject
@property NSString *firstName;
@property NSString *lastName;
@property int age;
@end
当我们想添加一个字段fullName属性而不是first和last names时,我们可以这样做:
@interface Person : RLMObject
@property NSString *fullName;
@property int age;
@end
接下来执行迁移:
// Inside your [AppDelegate didFinishLaunchingWithOptions:]
RLMRealmConfiguration *config = [RLMRealmConfiguration defaultConfiguration];
config.schemaVersion = 1;
config.migrationBlock = ^(RLMMigration *migration, uint64_t oldSchemaVersion) {
//如果从没迁移过,oldSchemaVersion == 0
if (oldSchemaVersion < 1) {
// The enumerateObjects:block: method iterates
// over every 'Person' object stored in the Realm file
[migration enumerateObjects:Person.className
block:^(RLMObject *oldObject, RLMObject *newObject) {
// 设置新增属性的值
newObject[@"fullName"] = [NSString stringWithFormat:@"%@ %@",
oldObject[@"firstName"],
oldObject[@"lastName"]];
}];
}
};
[RLMRealmConfiguration setDefaultConfiguration:config];
在迁移的过程中重命名属性名称
// Inside your [AppDelegate didFinishLaunchingWithOptions:]
RLMRealmConfiguration *config = [RLMRealmConfiguration defaultConfiguration];
config.schemaVersion = 1;
config.migrationBlock = ^(RLMMigration *migration, uint64_t oldSchemaVersion) {
// We haven’t migrated anything yet, so oldSchemaVersion == 0
if (oldSchemaVersion < 1) {
// The renaming operation should be done outside of calls to `enumerateObjects:`.
[migration renamePropertyForClass:Person.className oldName:@"yearsSinceBirth" newName:@"age"];
}
};
[RLMRealmConfiguration setDefaultConfiguration:config];
JSON
Realm并没有直接的支持JSON,但是它可以使用[NSJSONSerialization JSONObjectWithData:options:error:]转化之后的结果。在结合标准的API中的createOrUpdateInRealm方法就可以解决。
// A Realm Object that represents a city
@interface City : RLMObject
@property NSString *name;
@property NSInteger cityId;
// other properties left out ...
@end
@implementation City
@end // None needed
NSData *data = [@"{\"name\": \"San Francisco\", \"cityId\": 123}" dataUsingEncoding: NSUTF8StringEncoding];
RLMRealm *realm = [RLMRealm defaultRealm];
// 插入包含JSON的NSData数据
[realm transactionWithBlock:^{
id json = [NSJSONSerialization JSONObjectWithData:data options:0 error:NULL];
[City createOrUpdateInRealm:realm withValue:json];
}];
说明:如果在JSON中包含有嵌套的对象或数组,那么会被自动被映射到一对多关系相关的嵌套对象中。
多线程访问Realm
当需要从多线程去访问同一个Realm时,你必须初始化一个新的Realm,去得到在App中每一个线程有一个不同的实例。只要你指定相同的Realm配置,那么所有的Realm实例将指向磁盘上的同一个文件。Realm是不支持在线程之间共享Relam实例的。当在一个单一事务中通过批量处理来写入非常大的数据,使用多线程是非常有用的。为了避免阻塞主线程,写入事务可以放在使用GCD在后台进行。Realm对象不是线程安全的,所有决定了不能线程间共享。因此你必须在你想要读取或者写入的每一个线程中获取到Realm的对象实例。如下是在后台队列中插入100万个对象的例子:
dispatch_async(queue, ^{
@autoreleasepool {
// Get realm and table instances for this thread
RLMRealm *realm = [RLMRealm defaultRealm];
// Break up the writing blocks into smaller portions
// by starting a new transaction
for (NSInteger idx1 = 0; idx1 < 1000; idx1++) {
[realm beginWriteTransaction];
// Add row via dictionary. Property order is ignored.
for (NSInteger idx2 = 0; idx2 < 1000; idx2++) {
[Person createInRealm:realm
withValue:@{@"name" : randomString,
@"birthdate" : randomDate}];
}
// Commit the write transaction
// to make this data available to other threads
[realm commitWriteTransaction];
}
}
});
总结
Realm是非常适合去管理本地存储和数据库。Realm给了你代码层面非常强大的可扩展性和力量。对于大多数App甚至是游戏,我认为你是需要使用数据库,Realm值得你去使用。
参考资料
https://www.appcoda.com/realm-database-swift/
http://blog.devzeng.com/blog/simple-usage-of-realm-in-ios.html
http://blog.csdn.net/xiaoluodecai/article/details/50618445
http://www.cocoachina.com/ios/20150505/11756.html
http://ios.jobbole.com/85041/
https://www.raywenderlich.com/112544/realm-tutorial-getting-started