You are reading the documentation for an older version of Realm. You can view the latest documentation instead.
如果您的应用中只打算使用 Swift 进行开发,那么您应当考虑使用 Realm 的 Swfit 版本。 注意:不能同时使用 Objective‑C 版本和 Swift 版本的 Realm。
Objective‑C版本的 Realm 能够让您以一种安全、耐用以及迅捷的方式来高效地编写应用的数据模型层,如下例所示:
// 定义模型的做法和定义常规 Objective‑C 类的做法类似
@interface Dog : RLMObject
@property NSString *name;
@property NSInteger age;
@end
RLM_ARRAY_TYPE(Dog)
@interface Person : RLMObject
@property NSString *name;
@property NSData *picture;
@property RLMArray<Dog *><Dog> *dogs;
@end
// 使用的方法和常规 Objective‑C 对象的使用方法类似
Dog *mydog = [[Dog alloc] init];
mydog.name = @"大黄";
mydog.picture = nil; // 属性的值可以为空
NSLog(@"狗狗的名称: %@", mydog.name);
// 数据持久化操作十分简单
RLMRealm *realm = [RLMRealm defaultRealm];
[realm transactionWithBlock:^{
[realm addObject:mydog];
}];
// 可以在任何一个线程中执行检索操作
dispatch_async(dispatch_queue_create("background", 0), ^{
RLMResults<Dog *> *r = [Dog objectsWhere:@"age > 8"];
});
如果您的应用正在使用 Core Data 并打算换用 Realm 的话,我们最近发布了一篇关于如何执行转换的文章,点击此处查看!
从这里开始
下载 Realm Objective‑C 或者在GitHub上查看源码!
准备工作
- 使用 Realm 构建应用的基本要求:iOS >= 7, OS X >= 10.9 并且支持 WatchKit。;
- 需要使用 Xcode 6.4 或者以后的版本;
- 程序支持Objective‑C, Swift 1.2, Swift 2.0 & Swift 2.1.1。
安装
注意:动态框架与 iOS 7 不兼容,要支持 iOS 7 的话请查看“静态框架”。
- 下载最新的Realm发行版本,并解压;
- 前往Xcode 工程的”General”设置项中,从
ios/dynamic/
、osx/
、tvos/
或者watchos/
中将’Realm.framework’拖曳到”Embedded Binaries”选项中。确认Copy items if needed被选中后,点击Finish按钮; - 在单元测试目标的”Build Settings”中,在”Framework Search Paths”中添加
Realm.framework
的上级目录; - 如果希望使用 Swift 加载 Realm,请拖动
Swift/RLMSupport.swift
文件到 Xcode 工程的文件导航栏中并选中Copy items if needed; -
如果在 iOS、watchOS 或者 tvOS 项目中使用 Realm,请在您应用目标的”Build Phases”中,创建一个新的”Run Script Phase”,并将
bash "${BUILT_PRODUCTS_DIR}/${FRAMEWORKS_FOLDER_PATH}/Realm.framework/strip-frameworks.sh"
这条脚本复制到文本框中。 因为要绕过APP商店提交的bug,这一步在打包通用设备的二进制发布版本时是必须的。
- 安装CocoaPods 0.37.1 或者更高版本;
- 在您的Podfile中,添加
pod 'Realm'
到您的 app 目标中,添加pod 'Realm/Headers'
到您的测试目标中; - 在终端运行
pod install
; - 采用 CocoaPods 生成的
.xcworkspace
来运行工程!
- 安装 Carthage 0.7.5 或者更高版;
- 在Carthage 中添加
github "realm/realm-cocoa"
; - 运行
carthage update
; - 从
Carthage/Build/
目录下对应平台文件夹中,将Realm.framework
拖曳到您 Xcode 工程”General”设置项的”Embedded Binaries”选项卡中; -
iOS/watchOS/tvOS: 在您应用目标的“Build Phases”设置选项卡中,点击“+”按钮并选择“New Run Script Phase”。在新建的Run Script中,填写:
/usr/local/bin/carthage copy-frameworks
在“Input Files”内添加您想要使用的框架路径,例如:
$(SRCROOT)/Carthage/Build/iOS/Realm.framework
因为要绕过APP商店提交的bug,这一步在打包通用设备的二进制发布版本时是必须的。
- 下载 Realm 的最新版本并解压;
- 将
Realm.framework
从ios/static/
文件夹拖曳到您 Xcode 项目中的文件导航器当中。确保 Copy items if needed 选中然后单击 Finish; - 在 Xcode 文件导航器中选择您的项目,然后选择您的应用目标,进入到** Build Phases** 选项卡中。在 Link Binary with Libraries 中单击 + 号然后添加 libc++.dylib;
- 如果你在用 Swift 来使用 Realm,那么将位于
Swift/RLMSupport.swift
的文件拖曳进您 Xcode 项目中的文件导航器当中,确保 Copy items if needed 选中。
tvOS
由于在 tvOS 中向 Doucments 目录写入数据是被禁止的,因此默认的 Realm 路径将被设置为 NSCachesDirectory
。然而,要注意的是 tvOS 会随时清除 Caches 目录下的文件,因此我们建议您将 Realm 数据库作为一个新的缓存机制使用,而不是用其来存储重要的用户数据。
您同样可以在您应用中加入预构建的 Realm 文件。不过,一定要确保遵循 App Store 应用上架指南,保证应用大小在 200 MB 以内。
您可以浏览我们的 tvOS 示例项目 ,以此来展示简单 tvOS 应用是如何使用 Realm 进行离线缓存以及预加载数据的。
Realm浏览器/数据库管理器
如果您已经在使用 Realm Objective‑C 0.96 及其之后版本的话,在 Realm Browser GitHub 页存有一个兼容的 浏览器版本。
我们还提供了一个名为 Realm Browser 的独立的Mac应用以便 对.realm数据库进行读取和编辑。
您可以使用菜单中的Tools(工具) > Generate demo database(生成演示数据库)来生成一个有样本数据的测试数据库。
如果您需要寻找您应用的Realm文件,请查看StackOverflow上的这个答案来获取详细信息。
您可以从Mac App Store安装Realm Browser。
Xcode 插件
我们的Xcode插件令 Realm 模型的创建更加方便。
安装 Realm 插件的最简单方式是通过点击”RealmPlugin”文件夹下的Alcatraz。您也可以手动进行安装:打开release zip 中的plugin/RealmPlugin.xcodeproj
并进行编译,重启 Xcode之后插件即可生效。如果您使用 Xcode 菜单来建立一个新文件(File > New > File… — or ⌘N) ,您就可以看到有一个新建Realm模型的选项。
API手册
您能查询我们的 完整版API手册 ,里面包含了所有类和方法等信息。
示例
您可以在release zip中的examples/
目录下查看 iOS 和 OS X 版本的示例程序。它们演示了Realm的很多功能和特性,例如数据库迁移(migration)、如何与UITableViewController’s一起使用、加密(encryption)、命令行工具等等。
获得帮助
- 编码过程中遇到了问题? 在 StackOverflow 上提问,我们会经常在上面查看以及回答问题!
- 发现了 BUG? 可以直接在GitHub repo提交给我们。如果可以的话,请给我们提供您所使用的 Realm 版本号、完整的日志记录、Realm 文件以及您的当前项目,以便我们能够重现您所发现的问题。
- 希望我们新增功能? 可以直接在GitHub repo提交给我们。请告诉我们需要实现何种功能和特性,以及新增这些功能的理由。
如果您在使用崩溃检测 SDK (诸如 Crashlytics 或者 HockeyApp) 的话,请确保开启了日志收集功能。Realm 会在抛出异常以及发生致命错误的时候会记录下元数据信息(不是用户数据),这些信息可以在出现问题的时候有效地帮助您进行解决。
数据模型(Model)
Realm数据模型是基于标准 Objective‑C 类来进行定义的,使用属性来完成模型的具体定义。
通过简单的继承 RLMObject 或者一个已经存在的模型类,您就可以创建一个新的 Realm 数据模型对象。
Realm模型对象在形式上基本上与其他 Objective‑C 对象相同 - 您可以给它们添加您自己的方法(method)和协议(protocol),和在其他对象中使用类似。
主要的限制是某个对象只能在其被创建的那个线程中使用, 并且您无法访问任何存储属性的实例变量(ivar)。
如果您安装了我们的Xcode插件 ,那么可在”New File…“对话框中会有一个很漂亮的模板,可用来创建接口(interface)和执行(implementation)文件。
您只需要为对象的类型列表添加目标类型的属性,或者RLMArray
,就可以创建数据关系(relationship)和嵌套数据结构(nested data structure)。
#import <Realm/Realm.h>
@class Person;
// 狗狗的数据模型
@interface Dog : RLMObject
@property NSString *name;
@property Person *owner;
@end
RLM_ARRAY_TYPE(Dog) // 定义RLMArray<Dog>
// 狗狗主人的数据模型
@interface Person : RLMObject
@property NSString *name;
@property NSDate *birthdate;
@property RLMArray<Dog> *dogs;
@end
RLM_ARRAY_TYPE(Person) // 定义RLMArray<Person>
// 实现文件
@implementation Dog
@end // 暂时没用
@implementation Person
@end // 暂时没用
由于在代码启动时 Realm 所有模型就需要被定义好,所以即使代码中没有调用,它们都需要被初始化。
在 Swift 中使用 Realm 的时候,Swift.reflect(_:)
函数可用于确定您模型中的信息,这需要确保 init()
已被成功调用。这意味着所有非可选的属性必须添加一个默认值。
通过RLMObject 可查看更多细节。
支持的类型
Realm支持以下的属性类型:BOOL
、bool
、int
、NSInteger
、long
、long long
、float
、double
、NSString
、NSDate
(精度为秒)、NSData
以及 被特殊类型标记的 NSNumber
。
CGFloat
属性的支持被取消了,因为它的类型不依赖于平台。
您可以使用RLMArray<Object *><Object>
和 RLMObject
的子类来建立诸如一对多、一对一之类的关系模型。
在 Xcode 7 以及之后的版本中,RLMArray
支持编译时的 Objective‑C 泛型(generics)。下面是不同属性定义方法的意义以及用途:
RLMArray
: 属性类型。<Object *>
: 属性的特别化(generic specialization),这可以阻止在编译时使用错误对象类型的数组。<Object>
: 此RLMArray
遵守的协议,可以让 Realm 知晓如何在运行时确定数据模型的架构。
关系(Relationships)
RLMObject 能够借助 RLMObject 以及 RLMArray属性来和另一个 RLMObject 建立联系。 RLMArray 表面上和NSArray非常类似,在 RLMArray 中的对象能够使用索引下标(indexed subscripting)来进行访问。 和 NSArray 不同,RLMArray 的类型是固定的,其中只能存放简单的 RLMObject 子类类型。 要了解更详细的信息,请参阅 RLMArray。
假设现在您已经定义好了 Person 数据模型(见上文),让我们创建另一个名为 Dog 的数据模型:
// Dog.h
@interface Dog : RLMObject
@property NSString *name;
@end
对一(To-One)关系
对于多对一(many-to-one)或者一对一(one-to-one)关系来说,只需要声明一个 RLMObject 子类类型的属性即可:
// Dog.h
@interface Dog : RLMObject
// 其余属性声明...
@property Person *owner;
@end
您可以非常简单的通过这个属性完成关系的绑定:
Person *jim = [[Person alloc] init];
Dog *rex = [[Dog alloc] init];
rex.owner = jim;
当使用 RLMObject 属性的时候,您可以通过正常的属性访问语法来访问嵌套属性。比如说,rex.owner?.address.country
会依次读取对象的属性,然后自动从 Relam 中匹配所需的每一个对象。
对多(To-Many)关系
通过 RLMArray 类型的属性您可以定义一个对多关系。RLMArray中可以包含简单类型的RLMObject,表面上和NSMutableArray非常类似。
如果打算给我们的 Person 数据模型添加一个“dogs”属性,以便能够和多个“dogs”建立关系,也就是表明一个 Person 可以有多个 Dog,那么我们首先需要定义一个 RLMArray<Dog>
类型。通过对应数据模型接口文件下的宏命令即可完成:
//Dog.h
@interface Dog : RLMObject
// 属性声明...
@end
RLM_ARRAY_TYPE(Dog) // 定义一个 RLMArray<Dog> 类型
RLM_ARRAY_TYPE
宏创建了一个协议,从而允许 RLMArray<Dog>
语法的使用。如果该宏没有放置在模型接口的底部的话,您或许需要提前声明该模型类。
接下来您就能定义RLMArray<Dog>
类型的属性了:
// Person.h
@interface Person : RLMObject
// 其余的属性声明...
@property RLMArray<Dog *><Dog> *dogs;
@end
您可以和之前一样,对 RLMArray 属性进行访问和赋值:
// ZhangSan是大黄(dahuang)以及所有名字叫“小白”的狗狗的主人
RLMResults<Dog> *someDogs = [Dog objectsWhere:@"name contains '小白'"];
[ZhangSan.dogs addObjects:someDogs];
[ZhangSan.dogs addObject:dahuang];
注意,虽然可以给 RLMArray
属性赋值为 nil
,但是这仅用于“清空”数组,而不是用以移除数组。这意味着您总是可以向一个 RLMArray
属性中添加对象,即使其被置为了 nil
。
反向关系(Inverse Relationship)
链接是单向性的。因此,对于对多属性 Person.dogs
来说,其会链接到一个 Dog
实例,对于对一属性 Dog.owner
来说也会链接到 Person
实例。这些链接相互之间是互相独立的。为 Person
实例的 dogs
属性添加一个新的 Dog
,并不会自动将狗狗的 owner
属性设置为该 Person
。由于手动同步关系对会很容易出错,并且还会让内容变得复杂、冗余,Realm 提供了一个 API 来取得反向链接(backlink),如下所述。
通过反向关系(也被称为反向链接(backlink)),您可以通过一个特定的属性获取和给定对象有关系的所有对象。比如说,对 Dog
的实例调用-linkingObjectsOfClass:forProperty:
会返回对应的与被调用实例所指定属性的类有关系的所有对象。通过在 Dog
中定义一个只读(计算)属性 owners
可以简化这个操作:
@interface Dog : RLMObject
@property NSString *name;
@property NSInteger age;
@property (readonly) NSArray *owners; // Realm 并不会存储这个属性,因为这个属性是只读的
@end
@implementation Dog
// 定义“owners”,和 Person.dogs 建立反向关系
- (NSArray *)owners {
return [self linkingObjectsOfClass:@"Person" forProperty:@"dogs"];
}
@end
可选属性(Optional Properties)
通常情况下,NSString
、NSData
以及 NSDate
属性可以设置为 nil
。如果你不需要实现此功能,你可以重写您的 RLMObject
子类的 +requiredProperties
方法。
比如对于以下的模型定义来说,如果尝试给 name 属性设置为 nil
将会抛出一个异常,但是将 birthday 属性设置为 nil
却是允许的:
@interface Person : RLMObject
@property NSString *name;
@property NSDate *birthday;
@end
@implementation Person
+ (NSArray *)requiredProperties {
return @[@"name"];
}
@end
存储可空数字目前已经可以通过 NSNumber
属性完成。
由于 Realm 对不同类型的数字采取了不同的存储格式,因此设置可空的数字属性必须是 RLMInt
、RLMFloat
、RLMDouble
或者 RLMBool
其中一个类型。
赋给属性的所有值都会被转换为指定的类型。
比如说,如果我们存储一个用户的年龄(age)而不是存储他们的生日,同时还允许当你不知道该用户的年龄的时候允许将 age 属性设置为 nil
:
@interface Person : RLMObject
@property NSString *name;
@property NSNumber<RLMInt> *age;
@end
@implementation Person
+ (NSArray *)requiredProperties {
return @[@"name"];
}
@end
RLMProperty
的子类属性始终都可以为 nil
,因此这些类型不能够放在 requiredProperties
中,并且 RLMArray
不支持存储 nil
值。
属性特性(attributes)
注意由于 Realm 在自己的引擎内部有很好的语义解释系统,所以Objective‑C的许多属性特性将被忽略,如nonatomic
, atomic
, strong
, copy
和weak
等。 因此为了避免误解,我们推荐您在编写数据模型的时候不要使用任何的属性特性。 当然,如果您已经设置了,在有RLMObject
对象被写入realm数据库前,这些特性会一直生效。 无论是否有RLMObject
对象存在于 realm 中,您为getter和setter自定义的名字都能正常工作。
索引属性(Indexed Properties)
重写 +indexedProperties
方法可以为数据模型中需要添加索引的属性建立索引:
@interface Book : RLMObject
@property float price;
@property NSString *title;
@end
@implementation Book
+ (NSArray *)indexedProperties {
return @[@"title"];
}
@end
目前只有字符串和数值允许索引。
对属性进行索引可以以极小的插入花费加快比较检索的速度(比如说=
以及 IN
操作符)。
属性默认值
重写+defaultPropertyValues
可以在每次对象创建之后为其提供默认值。
@interface Book : RLMObject
@property float price;
@property NSString *title;
@end
@implementation Book
+ (NSDictionary *)defaultPropertyValues {
return @{@"price" : @0, @"title": @""};
}
@end
主键(Primary Keys)
重写 +primaryKey
可以设置模型的主键。声明主键之后,对象将被允许查询,更新速度更加高效,并且要求每个对象保持唯一性。 一旦带有主键的对象被添加到 Realm 之后,该对象的主键将不可修改。
@interface Person : RLMObject
@property NSInteger id;
@property NSString *name;
@end
@implementation Person
+ (NSString *)primaryKey {
return @"id";
}
@end
忽略属性(Ignored Properties)
重写 +ignoredProperties
可以防止 Realm 存储数据模型的某个属性。Realm 将不会干涉这些属性的常规操作,它们将由成员变量(ivar)提供支持,并且您能够轻易重写它们的 setter 和 getter。
@interface Person : RLMObject
@property NSInteger tmpID;
@property (readonly) NSString *name; // 只读属性将被自动忽略
@property NSString *firstName;
@property NSString *lastName;
@end
@implementation Person
+ (NSArray *)ignoredProperties {
return @[@"tmpID"];
}
- (NSString *)name {
return [NSString stringWithFormat:@"%@ %@", self.firstName, self.lastName];
}
@end
对象存储
对对象的所有更改(添加,修改和删除)都必须通过写入事务(transaction)完成。
Rrealm的对象可以被实例化并且作为独立
对象使用(也就是未存储在 Realm 数据库中),和其他常规Objective‑C对象无异。
如果您想要在多个线程中共享对象,或者在应用重启后重复使用对象,那么您必须将其存储到Realm数据库中——这个操作必须在写入事务中完成。
因为写入事务将会产生不可忽略的性能消耗,因此你应当检视你的代码以确保减少写入事务的次数。
Because write transactions could potentially fail like any other disk IO operations, both -[RLMRealm transactionWithBlock:]
& -[RLMRealm commitWriteTransaction]
optionally take an NSError
pointer parameter so you can handle and recover from failures like running out of disk space. There are no other recoverable errors. For brevity, our code samples don’t handle these errors but you certainly should in your production applications.
由于写入事务像其余硬盘读写操作一样,会出现失败的情况,因此 -[RLMRealm transactionWithBlock:]
以及 -[RLMRealm commitWriteTransaction]
可以选择加上 NSError
指针参数 因此你可以处理和恢复诸如硬盘空间溢出之类的错误。此外,其他的错误都无法进行恢复。简单起见,我们的代码示例并不会处理这些错误,但是您应当在您应用当中注意到这些问题。
创建对象
当定义完数据模型之后,您可以将您的 RLMObject 子类实例化,然后向 Realm 中添加新的实例。我们以下面这个简单的模型为例:
// 狗狗的数据模型
@interface Dog : RLMObject
@property NSString *name;
@property NSInteger age;
@end
// 实现文件
@implementation Dog
@end
我们可以用多种方法创建一个新的对象:
// (1) 创建一个狗狗对象,然后设置其属性
Dog *myDog = [[Dog alloc] init];
myDog.name = @"大黄";
myDog.age = 10;
// (2) 通过字典创建狗狗对象
Dog *myOtherDog = [[Dog alloc] initWithValue:@{@"name" : @"豆豆", @"age" : @3}];
// (3) 通过数组创建狗狗对象
Dog *myThirdDog = [[Dog alloc] initWithValue:@[@"豆豆", @3]];
- 使用指定初始化器(designated initializer)创建对象是最简单的方式。请注意,所有的必需属性都必须在对象添加到 Realm 前被赋值。
- 通过使用恰当的键值,对象还可以通过字典完成创建。
- 最后,RLMObject 子类还可以通过数组完成实例化,数组中的值必须和数据模型中对应属性的次序相同。
嵌套属性(Nested Object)
如果某个对象中有 RLMObject 或者 RLMArray 类型的属性,那么通过使用嵌套的数组或者字典便可以对这些属性递归地进行设置。您只需要简单的用表示其属性的字典或者数组替换每个对象即可:
// 这里我们就可以使用已存在的狗狗对象来完成初始化
Person *aPerson = [[Person alloc] initWithValue:@[@"李四", @30, @[aDog, anotherDog]]];
// 还可以使用多重嵌套
Person *aPerson = [[Person alloc] initWithValue:@[@"李四", @30, @[@[@"小黑", @5],
@[@"旺财", @6]]]];
即使是数组以及字典的多重嵌套,Realm 也能够轻松完成对象的创建。注意 RLMArray 只能够包含 RLMObject
类型,不能包含诸如NSString
之类的基础类型。
添加数据
向 Realm 中添加数据的步骤如下:
// 创建对象
Person *author = [[Person alloc] init];
author.name = @"大卫·福斯特·华莱士";
// 获取默认的 Realm 实例
RLMRealm *realm = [RLMRealm defaultRealm];
// 每个线程只需要使用一次即可
// 通过事务将数据添加到 Realm 中
[realm beginWriteTransaction];
[realm addObject:author];
[realm commitWriteTransaction];
等您将某个对象添加到 Realm 数据库之后,您可以继续使用它,并且您对其做的任何更改都会被保存(必须在一个写入事务当中完成)。当写入事务提交之后,使用相同 Realm 数据源的其他线程才能够对这个对象进行更改。
请注意,如果在进程中存在多个写入操作的话,那么单个写入操作将会阻塞其余的写入操作,并且还会锁定该操作所在的当前线程。
这个特性与其他持久化解决方案类似,我们建议您使用该方案的通常做法,也就是将写入操作转移到一个独立的线程中执行。
由于 Realm 采用了 MVCC 设计架构,读取操作 并不会 因为写入事务正在进行而受到影响。除非您需要立即使用多个线程来同时执行写入操作,不然您应当采用批量化的写入事务,而不是采用多次少量的写入事务。
更新数据
Realm 提供了一系列用以更新数据的方式,这些方式都有着各自所适应的情景。请选择最符合您当前需求的方式来使用:
内容直接更新
您可以在写入事务中通过设置某个对象的属性从而完成对象的更新操作。
// 在一个事务中更新对象
[realm beginWriteTransaction];
author.name = @"托马斯·品钦";
[realm commitWriteTransaction];
通过主键更新
如果您的数据模型中设置了主键的话,那么您可以使用+[RLMObject createOrUpdateInRealm:withValue:]
来更新对象,或者当对象不存在时插入新的对象。
// 创建一个带有主键的“书籍”对象,作为事先存储的书籍
Book *cheeseBook = [[Book alloc] init];
cheeseBook.title = @"奶酪食谱";
cheeseBook.price = @9000;
cheeseBook.id = @1;
// 通过 id = 1 更新该书籍
[realm beginWriteTransaction];
[Book createOrUpdateInRealm:realm withValue:cheeseBook];
[realm commitWriteTransaction];
如果主键 id 为1的书籍在数据库中不存在,那么这个操作将会创建一个新的书籍。
您同时通过传递您想要更新值的集合,从而更新带有主键的某个对象的部分值,比如说如下所示:
// 假设带有主键值 `1` 的“书籍”对象已经存在
[realm beginWriteTransaction];
[Book createOrUpdateInRealm:realm withValue:@{@"id": @1, @"price": @9000.0f}];
// 这本书的`title`属性不会被改变
[realm commitWriteTransaction];
键值编码
RLMObject、RLMResult 以及 RLMArray 都遵守键值编码(Key-Value Coding)(KVC)机制。 当您在运行时才能决定哪个属性需要更新的时候,这个方法是最有用的。
将 KVC 应用在集合当中是大量更新对象的极佳方式,这样就可以不用经常遍历集合,为每个项目创建一个访问器了。
RLMResults *persons = [Person allObjects];
[[RLMRealm defaultRealm] transactionWithBlock:^{
[[persons firstObject] setValue:@YES forKeyPath:@"isFirst"];
// 将每个人的 planet 属性设置为“地球”
[persons setValue:@"地球" forKeyPath:@"planet"];
}];
删除数据
通过在写入事务中将要删除的对象传递给 -[RLMRealm deleteObject:]
方法,即可完成删除操作。
Book *cheeseBook = ... // 存储在 Realm 中的 Book 对象
// 在事务中删除一个对象
[realm beginWriteTransaction];
[realm deleteObject:cheeseBook];
[realm commitWriteTransaction];
您也能够删除存储在 Realm 中的所有数据。注意,Realm 文件的大小不会被改变,因为它会保留空间以供日后快速存储数据。
// 从 Realm 中删除所有数据
[realm beginWriteTransaction];
[realm deleteAllObjects];
[realm commitWriteTransaction];
查询
通过查询操作,Realm 将会返回包含 RLMObject 集合的RLMResults
实例。RLMResults 的表现和 NSArray 十分相似,并且包含在 RLMResults 中的对象能够通过索引下标进行访问。和 NSArray 不同,RLMResults 需要指定类型,并且其当中只能包含RLMObject 子类类型的属性。
所有的查询(包括查询和属性访问)在 Realm 中都是延迟加载的,只有当属性被访问时,才能够读取相应的数据。
查询结果并不是数据的拷贝:修改查询结果(在写入事务中)会直接修改硬盘上的数据。同样地,您可以直接通过包含在RLMResults中的RLMObject对象完成遍历关系图的操作。
从 Realm 中检索对象的最基本方法是+[RLMObject allObjects]
,这个方法将会返回带有所有RLMObject的RLMResults实例,并且这个实例的类型将是默认 Realm 数据库中被查询的子类类型。
// 查询默认的 Realm 数据库
RLMResults *dogs = [Dog allObjects]; // 从默认的 Realm 数据库中,检索所有狗狗
// 查询指定的 Realm 数据库
RLMRealm *petsRealm = [RLMRealm realmWithPath:@"pets.realm"]; // 获得一个指定的 Realm 数据库
RLMResults *otherDogs = [Dog allObjectsInRealm:petsRealm]; // 从该 Realm 数据库中,检索所有狗狗
条件查询(Filtering)
如果您熟悉NSPredicate的话,那么您就能很容易掌握其在 Realm 中的查询方法。RLMObjects、RLMRealm、RLMArray 以及 RLMResults 都提供了方法,允许您通过简单地传递一个 NSPredicate 实例、断言字符串或者断言格式化字符串来完成查询这顶RLMObject实例的操作,正如您在 NSArray 中执行查询的哪样。
比如说,下面的例子就展示了如何通过从默认的 Realm 数据库中调用 [RLMObject objectsWhere:]
方法来检索所有棕黄色,并且以“大”开头命名的狗狗的:
// 使用断言字符串查询
RLMResults *tanDogs = [Dog objectsWhere:@"color = '棕黄色' AND name BEGINSWITH '大'"];
// 使用 NSPredicate 查询
NSPredicate *pred = [NSPredicate predicateWithFormat:@"color = %@ AND name BEGINSWITH %@",
@"棕黄色", @"大"];
tanDogs = [Dog objectsWithPredicate:pred];
查看苹果的断言编程指南来获取更多关于断言查询和NSPredicate Cheatsheet的使用信息。 Realm 支持许多常见的断言:
- 比较操作数(comparison operand)可以是属性名称或者某个常量,但至少有一个操作数必须是属性名称;
- 比较操作符 ==、<=、<、>=、>、!=, 以及 BETWEEN 支持 int, long, long long, float, double, 以及 NSDate 属性类型的比较,比如说 age == 45;
- 相等比较 ==以及!=,比如说[Employee objectsWhere:@”company == %@”, company]
- 比较操作符 == and != 支持布尔属性;
- 对于 NSString 和 NSData 属性来说,我们支持 ==、!=、BEGINSWITH、CONTAINS 以及 ENDSWITH 操作符,比如说 name CONTAINS ‘Ja’;
- 字符串支持忽略大小写的比较方式,比如说 name CONTAINS[c] ‘Ja’ ,注意到其中字符的大小写将被忽略;
- Realm 支持以下复合操作符:“AND”、“OR” 以及 “NOT”。比如说 name BEGINSWITH ‘J’ AND age >= 32;
- 包含操作符 IN,比如说 name IN {‘Lisa’, ‘Spike’, ‘Hachi’};
- ==、!=支持与 nil 比较,比如说
[Company objectsWhere:@"ceo == nil"]
。注意到这只适用于有关系的对象,这里 ceo 是 Company 模型的一个属性。 - 通过 ==, != 进行空值比较,比如说
[Company objectsWhere:@"ceo == nil"]
; 注意,Realm 将nil
视为一个特殊的值而不是“缺失值”,不像 SQL 那样nil
等于自身。 - ANY 比较,比如说 ANY student.age < 21
- RLMArray 属性支持集合表达式:
@count
、@min
、@max
、@sum
以及@avg
,
要了解关于断言的更多详情,请查看[RLMObject objectsWhere:]
的详细信息。
排序
RLMResults 允许您指定一个排序标准,从而可以根据一个或多个属性进行排序。比如说,下列代码将上面例子中返回的狗狗根据名字升序进行排序:
// 排序名字以“大”开头的棕黄色狗狗
RLMResults *sortedDogs = [[Dog objectsWhere:@"color = '棕黄色' AND name BEGINSWITH '大'"]
sortedResultsUsingProperty:@"name" ascending:YES];
关于排序的更多信息,请查看 [RLMResults sortedResultsUsingProperty:ascending:]
的详细信息。
链式查询
Realm 查询引擎一个特性就是它能够通过非常小的事务开销来执行链式查询(chain queries),而不需要像传统数据库那样为每个成功的查询创建一个不同的数据库服务器访问。
比如说,如果我们想获得获得棕黄色狗狗的查询结果,并且在这个查询结果的基础上再获得名字以“大”开头的棕黄色狗狗,那么您可以像下列方式那样将两个查询链接起来:
RLMResults *tanDogs = [Dog objectsWhere:@"color = '棕黄色'"];
RLMResults *tanDogsWithBNames = [tanDogs objectsWhere:@"name BEGINSWITH '大'"];
自动更新
RLMResults 是底层数据的动态表现,其会进行自动更新,这意味着检索到的结果不能进行重复检索。修改影响查询结果的对象会立即在结果中反映出来。
RLMResults<Dog *> *puppies = [Dog objectsInRealm:realm where:@"age < 2"];
puppies.count; // => 0
[realm transactionWithBlock:^{
[Dog createInRealm:realm withValue:@{@"name": @"大黄", @"age": @1}];
}];
puppies.count; // => 1
这对所有的 RLMResults 都有影响,不管其是匹配查询的还是链式查询所检索出来的。
RLMResults 的这个特性不仅让 Realm 保证速度和效率,它同时还让代码更加简洁、更为灵活。比如说,如果您的视图控制器是基于查询结果而现实的,您可以将 RLMResults 存储为一个属性,这样就无需在每次访问前都要刷新数据以确保数据最新了。
您也可以查看 Realm 通知 一节以确认 Realm 数据何时被更新,比如说由此来决定应用 UI 何时需要被更新,这样就无必重新检索 RLMResults 了。
由于检索结果是自动更新的,因此不要迷信 index 以及 count 会一直保持静止。RLMResults 数据不变的唯一情况就是处于快速枚举的过程当中,这允许修改匹配检索的对象:
[realm beginWriteTransaction];
for (Person *person in [Person objectsInRealm:realm where:@"age == 10"]) {
person.age++;
}
[realm commitWriteTransaction];
此外,还可以使用 键值编码 来对 RLMResults 执行操作。
Realm 数据库
默认的 Realm 数据库
您可能很早就已经注意到,我们总是通过调用 [RLMRealm defaultRealm]
来初始化以及访问我们的 realm
变量。这个方法将会返回一个 RLMRealm对象,并指向您应用的 Documents (iOS) 或者 Application Support (OS X)文件夹下的一个名为“default.realm”的文件。
许多 Realm API 中的方法都支持两种默认的数据库访问方式,一种是RLMRealm
实例,另一种是访问默认 Realm 数据库的便捷版本。例如 [RLMObject allObjects]
等同于 [RLMObject allObjectsInRealm:[RLMRealm defaultRealm]]
。
Realm 配置
通过RLMRealmConfiguration您可以配置诸如 Realm 文件在何处存储之类的信息。
配置同时也可以在每次您需要使用 Realm 实例的时候传递给[RLMRealm realmWithConfiguration:config error:&err]
,或者您也可以通过 [RLMRealmConfiguration setDefaultConfiguration:config]
来为默认的 Realm 数据库进行配置。
比如说,假设有这样一个应用,用户必须登录到您的网站后台才能够使用,然后您希望这个应用支持快速帐号切换功能。 您可以为每个帐号创建一个特有的 Realm 文件,通过对默认配置进行更改,就可以直接使用默认的 Realm 数据库来直接访问了,如下所示:
+ (void)setDefaultRealmForUser:(NSString *)username {
RLMRealmConfiguration *config = [RLMRealmConfiguration defaultConfiguration];
// 使用默认的目录,但是使用用户名来替换默认的文件名
config.path = [[[config.path stringByDeletingLastPathComponent]
stringByAppendingPathComponent:username]
stringByAppendingPathExtension:@"realm"];
// 将这个配置应用到默认的 Realm 数据库当中
[RLMRealmConfiguration setDefaultConfiguration:config];
}
其他的realm数据库
有的时候,在不同位置存储多个 Realm 数据库是十分有用的。 例如,如果您需要将您应用的某些数据打包到一个 Realm 文件中,作为主要 Realm 数据库的扩展。 您可以像以下代码这样做:
RLMRealmConfiguration *config = [RLMRealmConfiguration defaultConfiguration];
// 获取需要打包文件的路径
config.path = [[NSBundle mainBundle] pathForResource:@"MyBundledData" ofType:@"realm"];
// 以只读模式打开文件,因为应用数据包并不可写
config.readOnly = YES;
// 通过配置打开 Realm 数据库
RLMRealm *realm = [RLMRealm realmWithConfiguration:config];
// 从打包的 Realm 数据库中读取某些数据
RLMResults<Dog *> *dogs = [Dog objectsInRealm:realm where:@"age > 5"];
请注意,使用自定义路径来初始化 Realm 数据库需要拥有路径所在位置的写入权限。 通常存储可写 Realm 文件的地方是位于 iOS 上的“Documents”文件夹以及位于 OS X 上的“Application Support”文件夹。 具体情况,请遵循苹果的 iOS 数据存储指南, 它推荐将文件存储在<Application_Home>/Library/Caches
目录下。
内存数据库
通常情况下,Realm 数据库是存储在硬盘中的,但是您能够通过设置 inMemoryIdentifier
而不是设置RLMRealmConfiguration中的 path
属性,以创建一个完全在内存中运行的数据库。
RLMRealmConfiguration *config = [RLMRealmConfiguration defaultConfiguration];
config.inMemoryIdentifier = @"MyInMemoryRealm";
RLMRealm *realm = [RLMRealm realmWithConfiguration:config];
内存数据库在每次程序运行期间都不会保存数据。但是,这不会妨碍到 Realm 的其他功能,包括查询、关系以及线程安全。 假如您需要灵活的数据读写但又不想储存数据的话,那么内存数据库对您来说一定是一个不错的选择。
内存数据库会在临时文件夹中创建多个文件,用来协调处理诸如跨进程通知之类的事务。 实际上没有任何的数据会被写入到这些文件当中,除非操作系统由于内存过满需要清除磁盘上的多余空间。
注意: 如果某个内存 Realm 数据库实例没有被引用,那么所有的数据就会被释放。强烈建议您在应用的生命周期内保持对Realm内存数据库的强引用,以避免不期望的数据丢失。
错误处理
和所有硬盘读写操作一样,当资源受限的时候创建一个 RLMRealm 实例可能会出现失败的情况。在实际情况中,这只会当首次在指定线程中创建 Realm 对象的时候发生。从相同线程中后续访问 Realm 数据库将会重复使用缓存的实例,不会导致失败。
要处理在指定线程中初次 Realm 数据库导致的错误, 给 error
参数提供一个 NSError
指针:
RLMRealmConfiguration *config = [RLMRealmConfiguration defaultConfiguration];
RLMRealm *realm = [RLMRealm realmWithConfiguration:config error:&error];
if (!realm) {
// 错误处理
}
在Realm数据库间拷贝数据
拷贝 Realm 对象到另一个 Realm 数据库十分简单,只需将原始对象传递给+[RLMObject createInRealm:withValue:]
。例如, [MyRLMObjectSubclass createInRealm:otherRealm withValue:originalObjectInstance]
.
查找 Realm 文件
如果您不知道如何寻找应用的 Realm 文件,那么请查看这个StackOverflow回答来获取详细信息.
附加的 Realm 文件
除了标准的 .realm
文件之外,Realm 同样还会为其内部操作生成和维护一些额外的文件。
.realm.lock
- 对资源进行锁定的文件.realm.log_a
,.realm.log_b
- 事务日志记录文件.realm.note
- 用于通知的命名管道
这些文件对 .realm
数据库文件本身不会造成任何影响,即时它们所依赖的数据库文件被删除或者被替换掉也不会引发任何错误。
在 提交 Realm 问题 的时候,请一定要确保提交这些附加文件,因为它们包含了诸多的有效信息可以帮助我们进行 Debug。
在应用中建立 Realm 数据库
为了能够使您的用户在应用第一次启动时就能够直接使用一些初始数据,一种通常的做法就是为应用配置初始化数据。具体步骤是:
- 首先,定位 realm 的所在位置。您应该使用与最终版本相同的数据模型来创建 Realm 数据库,并将您想要打包的数据放置到您的应用当中。由于 Realm 文件是跨平台的,因此您能够测试您的OS X app (查看我们的JSONImport example)或者在模拟器中运行您的 iOS app;
- 在生成 Realm 文件的代码处,您需要结尾对文件进行压缩拷贝(参见
-[RLMRealm writeCopyToPath:error:]
)。 这有助于减少 Realm 的文件体积,让您发布的应用体积更小; - 将您最终的 Realm 文件的压缩拷贝拖懂到您最终应用的Xcode项目导航栏中;
- 前往您应用的Xcode Build Phase 选项卡,添加 Realm 文件到”Copy Bundle Resources”当中;
- 这样,您就能够在您的应用中使用这个打包好的 Realm 数据库了。 您能通过使用
[[NSBundle mainBundle] pathForResource:ofType:]
来得到数据库路径; - 如果打包的 Realm 文件包含有您不想修改的固定数据,您也能通过为RLMRealmConfiguration 对象设置
readOnly = true
选项,这样就可以将其从包路径直接打开了。 如果您打算修改初始数据的话,您可以通过[[NSFileManager defaultManager] copyItemAtPath:toPath:error:]
,将这个打包的文件拷贝到应用的 Document 文件夹下。
您能够参考我们的迁移例程应用来学习如何使用打包好的 Realm 文件。
类的子集(Class Subsets)
在某些情况下,您可能想要对哪个类能够存储在指定 Realm 数据库中做出限制。 例如,如果有两个团队分别负责开发您应用中的不同部分,并且同时在应用内部使用了 Realm 数据库,那么您肯定不希望为它们协调进行数据迁移。 您可以通过设置您的 RLMRealmConfiguration 的 objectClasses
属性来对类做出限制。
RLMRealmConfiguration *config = [RLMRealmConfiguration defaultConfiguration];
config.objectClasses = @[MyClass.class, MyOtherClass.class];
RLMRealm *realm = [RLMRealm realmWithConfiguration:config];
线程(Threading)
在单个线程中,你只需要将所有东西看做是普通的对象即可,无需考虑并行或者多线程处理的问题。线程锁定、资源协调访问都是不需要的(即时它们同时被其他线程所修改),唯一的修改操作就是包含在写事务中的操作。
Realm 通过确保每个线程始终拥有 Realm 的一个快照,以便让并发运行变得十分轻松。你可以同时有任意数目的线程访问同一个 Realm 文件,并且由于每个线程都有对应的快照,因此线程之间绝不会产生影响。
您唯一需要注意的一件事情就是不能让多个线程都持有同一个 Realm 对象的 实例 。如果多个线程需要访问同一个对象,那么它们分别会获取自己所需要的实例(否则在一个线程上发生的更改就会造成其他线程得到不完整或者不一致的数据)。
检视其他线程上的变化
在主 UI 线程中(或者任何一个位于 runloop 中的线程),对象会在 runloop 的每次循环过程中自行获取其他线程造成的更改。其余时候您只能够对快照进行操作,因此单个方法中得到的数据将始终不变,无需担心其他线程会对其造成影响。
当您第一次打开 Realm 数据库的时候,它会根据最近成功的写事务提交操作来更新当前状态,并且在刷新之前都将一直保持在当前版本。Realm 会自每个 runloop 循环的开始自动进行刷新,除非 RLMRealm 的 autorefresh
属性设置为 NO
。如果某个线程没有 runloop 的话(通常是因为它们被放到了后台进程当中),那么 -[RLMRealm refresh]
方法必须手动调用,以确保让事务维持在最新的状态当中。
Realm 同样也会在写入事务提交(-[RLMRealm commitWriteTransaction]
)的时候刷新。
如果定期刷新 Realm 失败的话,就可能会导致某些事务的版本变为“锁定(pinned)”状态,阻止 Realm 重用该版本的硬盘空间,从而导致文件尺寸变大。查看我们的 当前的限制以获取关于此情况的更多信息。
跨线程传递实例
RLMObject 的单例(未保存的)表现的和正常的 NSObject 子类相同,可以安全地跨线程传递。
RLMRealm、RLMObject、RLMResults 或者 RLMArray 已保存的实例只能够在它们被创建的线程上使用,否则就会抛出异常*。这是 Realm 强制事务版本隔离的一种方法。否则,在不同事务版本中的线程间,通过潜在泛关系图(potentially extensive relationship graph)来确定何时传递对象将不可能实现。
相反,还有很多可以安全在线程间传递实例的方法。比如说,一个拥有主键的对象可以通过其主键值来进行传递;还有 RLMResults 可以通过其 NSPredicate
或者检索语句来进行传递;还有 RLMRealm 可以通过其 RLMRealmConfiguration 来进行传递。目标线程可以通过使用其线程安全的表达方式来重新捕获这些 RLMRealm、RLMObject、RLMResults 以及 RLMArray。记住重新捕获可能会得到当前线程所拥有的实例版本,和先前线程的版本可能会有所不同。
* 这些类型的某些属性和方法可以在任意线程中进行访问:
- RLMRealm: 所有的属性、类方法和构造器;all properties, class methods, and initializers.
- RLMObject:
invalidated
、objectSchema
、realm
,以及所有的类方法和构造器; - RLMResults:
objectClassName
和realm
; - RLMArray:
invalidated
、objectClassName
和realm
。
跨线程使用数据库
为了在不同的线程中使用同一个 Realm 文件,您需要为您应用的每一个线程初始化一个新的Realm 实例。 只要您指定的配置是相同的,那么所有的 Realm 实例都将会指向硬盘上的同一个文件。
我们还 不支持 跨线程共享Realm 实例。 Realm 实例要访问相同的 Realm 文件还必须使用相同的 RLMRealmConfiguration。
当写入大量数据的时候,在一个单独事务中通过批量执行多次写入操作是非常高效的。事务也可以使用Grand Central Dispatch(GCD)在后台运行,以防止阻塞主线程。 RLMRealm 对象并不是线程安全的,并且它也不能够跨线程共享,因此您必须要为每一个您想要执行读取或者写入操作的线程或者dispatch队列创建一个 Realm 实例。 这里有一个在后台队列中插入百万数据的例子:
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" : [self randomString],
@"birthdate" : [self randomDate]}];
}
// 提交写入事务以确保数据在其他线程可用
[realm commitWriteTransaction];
}
}
});
通知(Notification)
Realm 实例将会在每次写入事务提交后,给其他线程上的 Realm 实例发送通知。 这些通知可以通过注册一个闭包来获取到:
// 获取 Realm 通知
self.token = [realm addNotificationBlock:^(NSString *note, RLMRealm * realm) {
[myViewController updateUI];
}];
只要有任何的引用指向这个返回的通知 token ,那么这个通知将始终可用。 在这个注册更新的类里,您需要有一个强引用来钳制这个token,因为当通知 token 被释放后,通知也会自动被注销。
关于通知的更多信息,请参阅[RLMRealm addNotificationBlock:]
and [RLMRealm removeNotificationBlock:]
。
键值观察(Key-Value Observation, KVO)
Realm 对象的大多数属性都遵从 KVO 机制。 所有 RLMObject 子类的持久化(persisted)存储(未被忽略)的属性都是遵循 KVO 机制的,并且 RLMObject 以及 RLMArray 中 无效的(invalidated)
属性也同样遵循。
观察 RLMObject 子类的单个实例的属性的方法就如同观察其他 NSObject 子类 一样,不过要注意的是,当观察者(observer)存在的时候,您不能够使用诸如 [realm addObject:obj]
此类的方法向 Realm 数据库中添加对象。
观察持久化对象属性的方法有些许不同。 对于持久化对象来说,有三种能够改变其属性值的方法:直接赋值修改;调用 [realm refresh]
方法或者当另一个线程提交了写入事务之后,Realm 自行进行了更新;当另一个线程发生了改变,但还未刷新的时候,在当前进程调用 [realm beginWriteTransaction]
。
在后面两种情况中,所有在另一个线程的写入事务中进行的修改都将会立即实现,并且会立刻发送 KVO 通知。 所有的中间过程都会被抛弃掉,因此如果在写入事务中您将一个属性从 1 递增到 10,那么在主线程您只会得到一个属性从 1 直接变到 10 的通知。 由于属性的值可以不在写入事务中发生改变,甚至还可以作为写入事务开始的一部分,因此我们不推荐在 -observeValueForKeyPath:ofObject:change:context:
中尝试修改持久化 Realm 对象的值。
和 NSMutableArray
属性不同,观察 RLMArray 属性值的改变并不需要使用 -mutableArrayValueForKey:
方法,虽然这个方法适合未写入 Realm 数据库中的数据。 相反,您可以直接调用 RLMArray 中的修改方法,任何观察该属性的对象都将会得到通知。
在我们的例程应用ReactiveCocoa from Objective‑C 和 ReactKit from Swift中,您可以找到关于使用 Realm KVO机制的简要例子。
数据迁移(Migration)
当您使用任意一个数据库时,您随时都可能打算修改您的数据模型。 由于 Realm 的数据模型是以标准的 Objective‑C 类来定义的,这使得修改模型就像修改其他的 Objective‑C 类一样方便。 例如,假设我们有如下 Person
模型:
@interface Person : RLMObject
@property NSString *firstName;
@property NSString *lastName;
@property int age;
@end
假如我们想要更新数据模型,给它添加一个 fullname
属性, 而不是将“姓”和“名”分离开来。 为此我们只需要改变一下代码即可,范例如下:
@interface Person : RLMObject
@property NSString *fullName;
@property int age;
@end
在这个时候如果您在数据模型更新之前就已经保存了数据的话,那么 Realm 就会注意到代码和硬盘上数据不匹配。 每当这时,您必须进行数据迁移,否则当您试图打开这个文件的话 Realm 就会抛出错误。
注意在进行迁移的时候, default property values 并不适用于新的对象。我们认为这是一个 Bug,我们会在 #1793 对其保持关注。
进行迁移
通过设置 RLMRealmConfiguration.schemaVersion
以及 RLMRealmConfiguration.migrationBlock
可以定义一个迁移操作以及与之关联的架构版本。 迁移闭包将会提供提供相应的逻辑操作,以让数据模型从之前的架构转换到新的架构中来。 每当通过配置创建完一个 RLMRealm 之后,迁移闭包将会在迁移需要的时候,将给定的架构版本应用到更新 RLMRealm 操作中。
例如,假设我们想要把上面所声明 Person
数据模型进行迁移。如下所示是最简单的数据迁移的必需流程:
// 在 [AppDelegate didFinishLaunchingWithOptions:] 中进行配置
RLMRealmConfiguration *config = [RLMRealmConfiguration defaultConfiguration];
// 设置新的架构版本。这个版本号必须高于之前所用的版本号(如果您之前从未设置过架构版本,那么这个版本号设置为 0)
config.schemaVersion = 1;
// 设置闭包,这个闭包将会在打开低于上面所设置版本号的 Realm 数据库的时候被自动调用
config.migrationBlock = ^(RLMMigration *migration, uint64_t oldSchemaVersion) {
// 目前我们还未进行数据迁移,因此 oldSchemaVersion == 0
if (oldSchemaVersion < 1) {
// 什么都不要做!Realm 会自行检测新增和需要移除的属性,然后自动更新硬盘上的数据库架构
}
};
// 告诉 Realm 为默认的 Realm 数据库使用这个新的配置对象
[RLMRealmConfiguration setDefaultConfiguration:config];
// 现在我们已经告诉了 Realm 如何处理架构的变化,打开文件之后将会自动执行迁移
[RLMRealm defaultRealm];
我们最起码需要做的,是使用一个空的闭包来更新版本,以表明这个架构已经被 Realm 升级(自动)完毕。 虽然这个迁移操作是最精简的了,但是我们需要让这个闭包能够自行计算新的属性(这里指的是 fullName
),这样才有意义。 在迁移闭包中,我们能够调用[RLMMigration enumerateObjects:block:]
来枚举特定类型的每个 RLMObject 对象,然后执行必要的迁移逻辑。注意,对枚举中每个已存在的 RLMObject 实例来说,应该是通过访问 oldObject
对象进行访问,而更新之后的实例应该通过 newObject
进行访问:
// 在 [AppDelegate didFinishLaunchingWithOptions:] 中进行配置
RLMRealmConfiguration *config = [RLMRealmConfiguration defaultConfiguration];
config.schemaVersion = 1;
config.migrationBlock = ^(RLMMigration *migration, uint64_t oldSchemaVersion) {
// 目前我们还未进行数据迁移,因此 oldSchemaVersion == 0
if (oldSchemaVersion < 1) {
// enumerateObjects:block: 方法遍历了存储在 Realm 文件中的每一个“Person”对象
[migration enumerateObjects:Person.className
block:^(RLMObject *oldObject, RLMObject *newObject) {
// 将名字进行合并,存放在 fullName 域中
newObject[@"fullName"] = [NSString stringWithFormat:@"%@ %@",
oldObject[@"firstName"],
oldObject[@"lastName"]];
}];
}
};
[RLMRealmConfiguration setDefaultConfiguration:config];
一旦迁移成功结束,Realm 文件和其中的所有对象都可被您的应用正常访问。
添加更多版本
假如说现在我们有两个先前版本的 Person
类:
// v0
@interface Person : RLMObject
@property NSString *firstName;
@property NSString *lastName;
@property int age;
@end
// v1
@interface Person : RLMObject
@property NSString *fullName; // 新属性
@property int age;
@end
// v2
@interface Person : RLMObject
@property NSString *fullName;
@property NSString *email; // 新属性
@property int age;
@end
我们的迁移闭包里面的逻辑大致如下:
RLMRealmConfiguration *config = [RLMRealmConfiguration defaultConfiguration];
config.schemaVersion = 2;
config.migrationBlock = ^(RLMMigration *migration, uint64_t oldSchemaVersion) {
// enumerateObjects:block: 遍历了存储在 Realm 文件中的每一个“Person”对象
[migration enumerateObjects:Person.className
block:^(RLMObject *oldObject, RLMObject *newObject) {
// 只有当 Realm 数据库的架构版本为 0 的时候,才添加 “fullName” 属性
if (oldSchemaVersion < 1) {
newObject[@"fullName"] = [NSString stringWithFormat:@"%@ %@",
oldObject[@"firstName"],
oldObject[@"lastName"]];
}
// 只有当 Realm 数据库的架构版本为 0 或者 1 的时候,才添加“email”属性
if (oldSchemaVersion < 2) {
newObject[@"email"] = @"";
}
}];
};
[RLMRealmConfiguration setDefaultConfiguration:config];
// 现在我们已经成功更新了架构版本并且提供了迁移闭包,打开旧有的 Realm 数据库会自动执行此数据迁移,然后成功进行访问
[RLMRealm defaultRealm];
要了解关于数据架构迁移如何实现的更完整信息,请参考我们的迁移例程应用。
线性迁移(Linear Migrations)
假如说,我们的应用有两个用户: JP和Tim。
JP经常更新应用,但Tim却经常跳过某些版本。
所以JP可能下载过这个应用的每一个版本,并且一步一步地跟着更新构架:第一次下载更新后,数据库架构从v0更新到v1;第二次架构从v1更新到v2……以此类推,井然有序。
相反,Tim很有可能直接从v0版本直接跳到了v2版本。
因此,您应该使用非嵌套的 if (oldSchemaVersion < X)
结构来构造您的数据库迁移模块,以确保无论用户在使用哪个版本的架构,都能完成必需的更新。
当您的用户不按套路出牌,跳过有些更新版本的时候,另一种情况也会发生。
假如您在v2里删掉了一个“email”属性,然后在v3里又把它重新引进了。假如有个用户从v1直接跳到v3,那 Realm 不会自动检测到v2的这个删除操作,因为存储的数据构架和代码中的构架吻合。
这会导致Tim的Person对象有一个v3的email property,但里面的内容却是v1的。
这个看起来没什么大问题,但是假如两者的内部存储类型不同(比如说: 从ISO email 标准格式变成了自定义格式),那麻烦就大了。为了避免这种不必要的麻烦,我们推荐您在 if (oldSchemaVersion < 3)
语句中,清空所有的email属性。
加密(Encryption)
Realm 的加密 API 目前支持 iOS、OS X 以及 WatchKit 平台,但 不支持 watchOS 平台,因为 Realm 加密机制使用的 <mach/mach.h>
以及 <mach/exc.h> API 被标记为
__WATCHOS_PROHIBITED` 了。
我们针对这个问题提交了一个 radar 请求:rdar://22063654
Please take note of the Export Compliance section of our LICENSE, as it places restrictions against the usage of Realm if you are located in countries with an export restriction or embargo from the United States.
在iOS平台中,通过使用 NSFileProtection
API 就可以花费极小的代价完成 Realm 的文件加密。加密操作需要注意两点:1)加密后的 Realm文件不能跨平台使用(因为NSFileProtection
只有 iOS 才可以使用),2)Realm 文件不能在没有密码保护的 iOS 设备中进行加密。为了避免这些问题(或者您想构建一个 OS X 的应用),您可以使用 Realm 提供的加密方法。
Realm 支持在创建 Realm 数据库时采用64位的密钥对数据库文件进行 AES-256+SHA2 加密。
// 产生随机密钥
NSMutableData *key = [NSMutableData dataWithLength:64];
SecRandomCopyBytes(kSecRandomDefault, key.length, (uint8_t *)key.mutableBytes);
// 打开加密文件
RLMRealmConfiguration *config = [RLMRealm defaultConfiguration];
config.encryptionKey = key;
NSError *error;
RLMRealm *realm = [RLMRealm realmWithConfiguration:config error:&error];
if (!realm) {
// 如果密钥错误,`error` 会提示数据库不可访问
NSLog(@"Error opening realm: %@", error);
return;
}
// 和往常一样使用 Realm 即可
RLMResults *dogs = [[Dog objectsInRealm:realm where:@"name contains 'Fido'"]];
这样硬盘上的数据都能都采用AES-256来进行加密和解密,并用 SHA-2 HMAC 来进行验证。 每次您要获取一个 Realm 实例时,您都需要提供一次相同的密钥。
我们的加密例程应用展示了如何产生密钥并将其安全地存放到钥匙串当中,然后用其加密 Realm。
加密过的 Realm 只会带来很少的额外资源占用(通常最多只会比平常慢10%)。
请注意,如果您使用第三方的崩溃记录工具(crashlytics,plcrashreporter等)的话,应该在首次打开加密 Realm 数据库之前完成注册,不然您将得到无数的崩溃报告,而实际上您的应用并未崩溃。
调试
调试您的 Realm 应用是十分简单的,您可以借助 LLDB 以及 Realm浏览器 的帮助来实时查看您应用中的数据。
我们的 Xcode 插件带有 LLDB 脚本,可以在 Xcode 的用户界面中检查持久化的 RLMObject、RLMResult 以及 RLMArray 对象,而不只是简单地显示 nil
或者 0
。
注意: 这一功能只支持Objective‑C。Swift的支持版本仍在计划中。
调试加密的Realm
使用加密的 Realm 数据库将 LLDB 时域(session)附加到进程上此功能目前仍未支持。 在某些情况下,您可能会通过在开发环境中设置 REALM_DISABLE_ENCRYPTION=YES
来激活此项功能;这个变量将强制让所有进行加密的 API 方法以未加密的方式运行,这样您就无需改变您在应用中调用的 Realm API 方法代码,便可以进行调试。处于显而易见的安全原因,当您试图访问一个已存在并已被加密的 .realm
文件时,将无法使用此项功能(这会导致一个异常:File::AccessError
)。不过当您的应用在创建以及访问新的 Realm 文件的时候,这个功能是非常有用的。
测试
配置默认的 Realm 数据库
使用和测试 Realm 应用的最简单方法就是使用 默认的 Realm 数据库了。 为了避免在测试中覆盖了应用数据或者泄露,您只需要为每项测试将默认的 Realm 数据库设置为新文件即可。
// 一个基本的测试类,每个使用 Realm 进行的测试都应当继承自该类,而不是直接继承自 XCTestCase 类
@interface TestCaseBase : XCTestCase
@end
@implementation TestCaseBase
- (void)setUp {
[super setUp];
// 使用当前测试名标识的内存 Realm 数据库。
// 这确保了每个测试都不会从别的测试或者应用本身中访问或者修改数据,并且由于它们是内存数据库,因此无需对其进行清理。
RLMRealmConfiguration *config = [RLMRealmConfiguration defaultConfiguration];
config.inMemoryIdentifier = self.name;
[RLMRealmConfiguration setDefaultConfiguration:config];
}
@end
注入(injecting) Realm 实例
另一种测试使用 Realm 代码的方式是让所有您打算进行测试的方法以参数的方式获取 RLMRealm 实例,这样您就可以在应用运行和测试时将不同的 Realm 文件传递进去。 例如,假设您的应用拥有一个从 JSON API 中获取用户配置文件的 GET
方法,然后您想要对其进行测试,确保本地配置文件能够正常创建:
// 应用中使用的代码
+ (void)updateUserFromServer {
NSURL *url = [NSURL URLWithString:@"http://myapi.example.com/user"];
[[[NSURLSession sharedSession] dataTaskWithURL:url
completionHandler:^(NSData *data,
NSURLResponse *response,
NSError *error) {
[self createOrUpdateUserInRealm:[RLMRealm defaultRealm] withData:data];
}] resume];
}
+ (void)createOrUpdateUserInRealm:(RLMRealm *)realm withData:(NSData *)data {
id object = [NSJSONSerialization JSONObjectWithData:data options:nil error:nil];
[realm transactionWithBlock:^{
[User createOrUpdateInRealm:realm withValue:object];
}];
}
// 测试中使用的代码
- (void)testThatUserIsUpdatedFromServer {
RLMRealm *testRealm = [RLMRealm realmWithPath:kTestRealmPath];
NSData *jsonData = [@"{\"email\": \"help@realm.io\"}"
dataUsingEncoding:NSUTF8StringEncoding];
[ClassBeingTested createOrUpdateUserInRealm:testRealm withData:jsonData];
User *expectedUser = [User new];
expectedUser.email = @"help@realm.io";
XCTAssertEqualObjects([User allObjectsInRealm:testRealm][0],
expectedUser,
@"用户信息未能从服务器正常更新");
}
避免在测试目标中将 Realm 数据库和测试代码相链接
如果您以动态框架的方式运行 Realm 的话,那么 您需要确保单元测试目标能否正确识别 Realm。您可以通过向您单元测试的 “Framework Search Paths” 中添加 Realm.framework
的上级目录来完成此功能。
如果您的测试失败提示消息为 "Object type '...' not persisted in Realm"
,那么这很可能是因为您直接将 Realm 框架直接链接到单元测试目标上了,这恰恰是应该避免的。从您的测试目标中解除 Realm 的链接就可以解决这个问题。
同样地,您应当确保您的数据模型类文件只在您的应用或者框架目标中进行了编译,千万不要将它们置入到您的单元测试目标当中。否则,这些类会在测试的过程中被复制,这往往会为调试问题时带来麻烦(可参考此问题 了解更多信息)。
您也要确保所有您需要进行测试的代码能够供单元测试目标访问(对于 Swift 1.2 来说,需要使用 public
访问控制;对于 Swift 2.0 及其之后版本来说,需要使用 @testable
标识符)。
REST APIs
Realm 简单地整合了 REST API 中的缓存功能,提供了几个关键的优点:
- 在 Realm 中进行对 REST 数据进行存储将允许您使用缓存信息,即使您的设备处于离线也能进行访问,这将带来良好的用户体验;
- 通过在 Realm 中提前加载您的数据集,您可以提升它的显示速度,从而提供更快的查询和显示,这恰恰是单独的 REST 无法实现的功能;
- 在 Realm 中对数据进行本地存储可以有效地减少服务端的负荷,因为它仅在需要检索新数据或者改变数据的时候才会访问服务器。
最佳实践
- 异步请求 — 网络请求和其他在闭包中进行的操作不能放到主进程中运行,它会阻塞用户界面的更新,影响用户交互。同理,Realm数据库中插入和修改大量数据的时候,应该避免在主线程中进行,最好选择在后台进程中进行。您可以使用通知,从而在数据提交的时候能够得到 Realm 的通知以响应数据更新;
- 缓存大量的数据集 — 我们建议您对可能使用的数据进行预处理并且存储到本地 Realm 数据库中。 这么做可以让您在数据集中进行完整的本地查询,从而充分利用 Realm 提供的高效率。
- “插入或更新” — 如果您的数据集有一个特有的标识符,例如说主键,您可以通过它来轻松实现使用
+[RLMObject createOrUpdateInRealm:withValue:]
进行的“插入或更新”的逻辑操作:从 REST API 中获取新的数据。这些方法会自行检查每条记录是否存在,如果记录存在的话就执行更新操作,否则的话就执行插入操作。
当前版本的限制
Realm 现在还是 beta 版本。我们还在为 1.0 版本的发布一直不断地添加新特性以及修复bug。在正式版发布之前,我们整理了一些常见的限制情况。
如果您想要查看完整的问题列表,请参阅 GitHub issues。
基本的限制
Realm 致力于平衡数据库读取的灵活性和性能。为了实现这个目标,在 Realm 中所存储的信息的各个方面都有基本的限制。例如:
- 类名称的长度必须在 0 和 63 字节之间。支持 UTF8 字符。如果超出了这个限制,应用程序的初始化将抛出异常。
- 属性名称的长度必须在 0 和 63 字节之间。支持 UTF8 字符。如果超出了这个限制,应用程序的初始化将抛出异常。
- NSData 不能保存超过 16 MB 大小的数据。如果要存储大量的数据,可通过将其分解为16MB 大小的块,或者直接存储在文件系统中,然后将文件路径存储在 Realm 中。如果您的应用试图存储一个大于 16MB 的单一属性,系统将在运行时抛出异常。
- NSDate 的存储精度只能到秒。参考NSDate 当前的局限可查看更多相关信息。
- 任何一个 Realm 文件的大小也不能超过 iOS 应用所允许的内存空间总量。这个值会根据不同设备和当时的内存使用情况有所不同(这里又一个关于相关方面的讨论: rdar://17119975)。如果您想存储更大的数据,请将数据映射到多个文件中。
暂时不支持细粒化(fine-grained)通知
虽然当 Realm 数据库发生变化时可以接收到通知(参见 通知),但是目前我们还不能从通知当中获取哪个东西是被添加、删除、移动还是被更新。 我们会在之后的更新中尽快完善这个功能。 键值观察KVO能够获取每个对象变化的通知消息,包括指定的对象被删除的消息。
NSDate 的精度只能到秒
一个包含非整数秒的 NSDate
对象存入 Realm 后,会在秒的地方截断。我们正在修复这个问题。 可参考 GitHub issue #875获取此问题的更多信息。同时,您可以存储无损的 NSTimeInterval
格式来获取精密的时间。
Realm对象的 Setters & Getters 不能被重载
因为 Realm 在底层数据库中重写了 setters 和 getters 方法,所以您不可以在您的对象上再对其进行重写。 一个简单的替代方法就是:创建一个新的 realm-ignored 属性,该属性的访问起可以被重写, 并且可以调用其他的 getter 和 setter 方法。
文件大小 & 版本跟踪
一般来说 Realm 数据库比 SQLite 数据库在硬盘上占用的空间更少。如果您的 Realm 文件大小超出了您的想象,这可能是因为您数据库中的 RLMRealm
中包含了旧版本数据。
为了使您的数据有相同的显示方式,Realm 只在循环迭代开始的时候才更新数据版本。这意味着,如果您从 Realm 读取了一些数据并进行了在一个锁定的线程中进行长时间的运行,然后在其他线程进行读写 Realm 数据库的话,那么版本将不会被更新,Realm 将保存中间版本的数据,但是这些数据已经没有用了,这导致了文件大小的增长。这部分空间会在下次写入操作时被重复利用。这些操作可以通过调用 writeCopyToPath:error:
来实现。
为了避免这个问题,您可以调用invalidate
,来告诉 Realm 您不再需要那些拷贝到 Realm 的数据了。这可以使我们不必跟踪这些对象的中间版本。在下次出现新版本时,再进行版本更新。
您可能在 Realm 使用Grand Central Dispatch时也发现了这个问题。在 dispatch 结束后自动释放调度队列(dispatch queue)时,调度队列(dispatch queue)没有随着程序释放。这造成了直到 RLMRealm
对象被释放后,Realm 中间版本的数据空间才会被再利用。为了避免这个问题,您应该在 dispatch 队列中,使用一个显式的自动调度队列(dispatch queue)。
问答时间
Realm 库文件有多大?
一旦您的应用以发布模式编译完成后, Realm 的库文件应该只有1 MB 左右的大小。 我们发布的那个可能有点大,这时因为它们还包含了对 iOS、watchOS 以及 tvOS 模拟器的支持库、某些调试符号以及某些当编译应用时会被 Xcode 自动排除的中间代码。
我应该在正式产品中使用realm吗?
自2012年起, Realm 就已经开始被用于正式的商业产品中了。
您应当期待我们的 Objective‑C & Swift API 会随着社区的反馈不断的完善和进化。同时,您也应该期待 Realm 带给您更多的新特性和版本修复。
我要付费使用 Realm 吗?
不要, Realm的彻底免费的,哪怕您用于商业软件。
你们计划怎么赚钱?
其实,我们靠着我们的技术,已经开始赚钱啦!这些钱来自于我们销售企业级产品的利润。如果您想要比普通发行版本或者realm-cocoa获得更多的支持, 我们很高兴和您发邮件聊聊。 我们一直致力于开发开源的(基于Apache 2.0 开源协议)、免费的realm-cocoa。
我看到你们在代码里有“core”出现, 那是个什么?
Core是我们内部的C++存储引擎的名称。Core 现在还没有开源,假如我们有时间来进行清理、重命名以及确定其当中的主要功能的话,但我们的确想将其开源(依旧使用Apache2.0开源协议)。同时,它的二进制发行版本在 Realm Core(TightDB)Binary License里面可以找到。
在我运行应用的时候发现了一个名为 Mixpanel 的网络调用,这是什么?
当您的应用处于调试模式的时候,Realm 将会收集匿名的统计信息。这些信息都是匿名的,收集 Realm、iOS 、OS X 的版本信息、您使用的语言,以及您目前使用的 Xcode 版本信息可以帮助我们更好地改进产品。这个调用不会在实际应用上运行,也不会在您的用户设备上运行 - 只有在模拟器中或者处于调试状态下时,才会运行。您可以看到我们是如何进行收集的,也可以查看我们所收集的内容。其原理可以在我们的源码中查看。
疑难解答
崩溃报告
我们建议您在应用中使用崩溃报告。大多数 Realm 操作都可能会在运行时发生崩溃(就如同其他硬盘 IO 操作一样),因此从应用中收集这些崩溃报告可以帮助您(或者我们)发现出错的具体位置,从而进行错误处理以及修复问题。
绝大多数商用的崩溃报告都有收集日志的选项。我们强烈建议您开启此功能。Realm 在抛出异常或者处于无法恢复的状况时将会记录元数据信息(但不会记录用户数据),这些信息在出错的时候对调试有很大帮助。
报告 Realm 错误
如果您发现了 Realm 中的任何错误,请 在 Github 上提交一个 issue,也可以给 help@realm.io 我们发邮件信息。尽可能给我们发送更多的信息,可以帮助我们更好的理解并解决您提出的问题。
下面信息对我们来说将十分有用:
- 您的目的
- 您所期望的结果
- 实际的结果
- 产生此结果的步骤
- 突出此问题的代码示例 (完整的 Xcode 项目的话我们可以自行编译,更好理解)
- Realm / Xcode / OS X 的版本
- 依赖库管理器的版本(CocoaPods / Carthage)
- 出现 Bug 的平台, OS 版本及架构(例如 64-bit iOS 8.1)
- 崩溃日志以及堆栈轨迹,参见上方的 崩溃报告 了解更多信息。