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.
  • 程序支持Objective‑C, Swift 1.2 & Swift 2.0。

安装

注意:动态框架与 iOS 7 不兼容,要支持 iOS 7 的话请查看“静态框架”。

  1. 下载最新的Realm发行版本,并解压;
  2. 前往Xcode 工程的”General”设置项中,从’ios/dynamic/’、’osx/’或者watchos/中将’Realm.framework’拖曳到”Embedded Binaries”选项中。确认Copy items if needed被选中后,点击Finish按钮;
  3. 在单元测试目标的”Build Settings”中,在”Framework Search Paths”中添加Realm.framework的上级目录;
  4. 如果希望使用 Swift 加载 Realm,请拖动Swift/RLMSupport.swift文件到 Xcode 工程的文件导航栏中并选中Copy items if needed
  5. 如果在 iOS 项目中使用 Realm,请在您应用目标的”Build Phases”中,创建一个新的”Run Script Phase”,并将

    bash "${BUILT_PRODUCTS_DIR}/${FRAMEWORKS_FOLDER_PATH}/Realm.framework/strip-frameworks.sh"

    这条脚本复制到文本框中。 因为要绕过APP商店提交的bug,这一步在打包通用设备的二进制发布版本时是必须的。

  1. 安装CocoaPods 0.37.1 或者更高版本
  2. 在您的Podfile中,添加pod 'Realm'到您的 app 目标中,添加pod 'Realm/Headers'到您的测试目标中;
  3. 在终端运行pod install
  4. 采用 CocoaPods 生成的.xcworkspace来运行工程!
  1. 安装 Carthage 0.7.5 或者更高版
  2. 在Carthage 中添加github "realm/realm-cocoa"
  3. 运行carthage update
  4. iOS:Carthage/Build/iOS/文件夹中拖拽Realm.framework 到您 Xcode 工程”General”设置项的”Linked Frameworks and Libraries”选项卡中;

    OS X:Carthage/Build/Mac/文件夹中拖拽 Realm.framework到您 Xcode 工程”General”设置项的”Embedded Binaries”选项卡中;

    watchOS:Carthage/Build/watchOS/文件夹中拖拽 Realm.framework到您 Xcode 工程”General”设置项的”Embedded Binaries”选项卡中。

  5. iOS: 在您应用目标的“Build Phases”设置选项卡中,点击“+”按钮并选择“New Run Script Phase”。在新建的Run Script中,填写:

    /usr/local/bin/carthage copy-frameworks

    在“Input Files”内添加您想要使用的框架路径,例如:

    $(SRCROOT)/Carthage/Build/iOS/Realm.framework

    因为要绕过APP商店提交的bug,这一步在打包通用设备的二进制发布版本时是必须的。

  1. 下载 Realm 的最新版本并解压;
  2. Realm.frameworkios/static/ 文件夹拖曳到您 Xcode 项目中的文件导航器当中。确保 Copy items if needed 选中然后单击 Finish
  3. 在 Xcode 文件导航器中选择您的项目,然后选择您的应用目标,进入到** Build Phases** 选项卡中。在 Link Binary with Libraries 中单击 + 号然后添加 libc++.dylib
  4. 如果你在用 Swift 来使用 Realm,那么将位于 Swift/RLMSupport.swift 的文件拖曳进您 Xcode 项目中的文件导航器当中,确保 Copy items if needed 选中。

tvOS

尽管 tvOS 仍处于测试状态,但是我们现在已经正在评估 Realm 在此平台应该如何运作。如果你想要参与评测 tvOS 的 Realm 早期版本,那么可以前往 PR #2506 以获取更多内容。这个版本只适用于开发测试,不要应用在正式产品当中!

Realm浏览器/数据库管理器

我们还提供了一个名为 Realm Browser 的独立的Mac应用以便 对.realm数据库进行读取和编辑。

Realm Browser

您可以使用菜单中的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提交给我们。请告诉我们需要实现何种功能和特性,以及新增这些功能的理由。

数据模型(Model)

Realm数据模型是基于传统的 Objective‑C 类来进行定义的,使用属性来完成模型的具体定义。 通过简单的继承 RLMObject 或者一个已经存在的模型类,您就可以创建一个新的 Realm 数据模型对象。 Realm模型对象在形式上基本上与其他 Objective‑C 对象相同 - 您可以给它们添加您自己的方法(method)和协议(protocol),和在其他对象中使用类似。 主要的限制就是某个对象只能在其被创建的那个线程中使用,并且您不能在任何存储属性中直接访问其实例变量。

如果您安装了我们的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 所有模型就需要被定义好,所以即使代码中没有调用,它们都需要被初始化。

通过RLMObject 可查看更多细节。

支持的类型

Realm支持以下的属性类型:BOOLboolintNSIntegerlonglong longfloatdoubleNSStringNSDate精度为秒)、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> 类型

接下来您就能定义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];

反向关系(Inverse Relationship)

通过反向关系(也被称为反向链接(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)

通常情况下,NSStringNSData 以及 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 对不同类型的数字采取了不同的存储格式,因此设置可空的数字属性必须是 RLMIntRLMFloatRLMDouble 或者 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, copyweak等。 因此为了避免误解,我们推荐您在编写数据模型的时候不要使用任何的属性特性。 当然,如果您已经设置了,在有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可以在每次对象创建之后为其提供默认值。因为 Swift 已经提供了一种通过属性定义的方法来定义属性默认值,因此在 Swift 中您不应当使用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的对象可以被实例化并且被单独使用,和其他常规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]];
  1. 使用 alloc-init(Objective‑C)或者指定初始化器(designated initializer)(Swift)创建对象是最简单的方式。请注意,所有的属性都必须在对象添加到 Realm 前被赋值。
  2. 通过使用恰当的键值,对象还可以通过字典完成创建。
  3. 最后,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 设计架构,读取操作 并不会 因为写入事务正在进行而受到影响。除非您需要立即使用多个线程来同时执行写入操作,不然您应当采用批量化的写入事务,而不是采用多次少量的写入事务。

查看RLMRealmRLMObject来获得更多内容。

更新数据

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 属性来说,我们支持 ==!=BEGINSWITHCONTAINS 以及 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];

关于排序的更多信息,请查看 [RLMObject objectsWhere:] and [RLMResults sortedResultsUsingProperty:ascending:] 的详细信息。

链式查询

Realm 查询引擎一个特性就是它能够通过非常小的事务开销来执行链式查询(chain queries),而不需要像传统数据库那样为每个成功的查询创建一个不同的数据库服务器访问。

比如说,如果我们想获得获得棕黄色狗狗的查询结果,并且在这个查询结果的基础上再获得名字以“大”开头的棕黄色狗狗,那么您可以像下列方式那样将两个查询链接起来:

RLMResults *tanDogs = [Dog objectsWhere:@"color = '棕黄色'"];
RLMResults *tanDogsWithBNames = [tanDogs objectsWhere:@"name BEGINSWITH '大'"];

Realm 数据库

默认的 Realm 数据库

您可能很早就已经注意到,我们总是通过调用 [RLMRealm defaultRealm] 来初始化以及访问我们的 realm 变量。这个方法将会返回一个 RLMRealm对象,并指向您应用的 Documents 文件夹下的一个名为“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 (!error) {
  // 错误处理
}

在Realm数据库间拷贝数据

拷贝 Realm 对象到另一个 Realm 数据库十分简单,只需将原始对象传递给+[RLMObject createInRealm:withValue:]。例如, [MyRLMObjectSubclass createInRealm:otherRealm withValue:originalObjectInstance].

查找 Realm 文件

如果您不知道如何寻找应用的 Realm 文件,那么请查看这个StackOverflow回答来获取详细信息.

在应用中建立 Realm 数据库

为了能够使您的用户在应用第一次启动时就能够直接使用一些初始数据,一种通常的做法就是为应用配置初始化数据。具体步骤是:

  1. 首先,定位 realm 的所在位置。您应该使用与最终版本相同的数据模型来创建 Realm 数据库,并将您想要打包的数据放置到您的应用当中。由于 Realm 文件是跨平台的,因此您能够测试您的OS X app (查看我们的JSONImport example)或者在模拟器中运行您的 iOS app;
  2. 在生成 Realm 文件的代码处,您需要结尾对文件进行压缩拷贝(参见 -[RLMRealm writeCopyToPath:error:])。 这有助于减少 Realm 的文件体积,让您发布的应用体积更小;
  3. 将您最终的 Realm 文件的压缩拷贝拖懂到您最终应用的Xcode项目导航栏中;
  4. 前往您应用的Xcode Build Phase 选项卡,添加 Realm 文件到”Copy Bundle Resources”当中;
  5. 这样,您就能够在您的应用中使用这个打包好的 Realm 数据库了。 您能通过使用[[NSBundle mainBundle] pathForResource:ofType:]来得到数据库路径;
  6. 如果打包的 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: invalidatedobjectSchemarealm,以及所有的类方法和构造器;
  • RLMResults: objectClassNamerealm
  • RLMArray: invalidatedobjectClassNamerealm

跨线程使用数据库

为了在不同的线程中使用同一个 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:]

数据迁移(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 就会抛出错误。

进行迁移

通过设置 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 数据库之前完成注册,不然您将得到无数的崩溃报告,而实际上您的应用并未崩溃。

键值观察(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‑CReactKit from Swift中,您可以找到关于使用 Realm KVO机制的简要例子。

调试

调试您的 Realm 应用是十分简单的,您可以借助 LLDB 以及 Realm浏览器 的帮助来实时查看您应用中的数据。

我们的 Xcode 插件带有 LLDB 脚本,可以在 Xcode 的用户界面中检查持久化的 RLMObject、RLMResult 以及 RLMArray 对象,而不只是简单地显示 nil 或者 0

Xcode截图

注意: 这一功能只支持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 的链接就可以解决这个问题。

同样地,您应当确保您的数据模型类文件只链接到了您的应用或者框架目标上,千万不要链接到您的单元测试目标当中。否则,这些类会在测试的过程中被复制,这往往会为调试问题时带来麻烦(可参考 https://github.com/realm/realm-cocoa/issues/1350 了解更多信息)。

您也要确保所有您需要进行测试的代码能够供单元测试目标访问(对于 Swift 1.2 来说,需要使用 public 访问控制;对于 Swift 2.0 来说,需要使用 @testable标识符)。

REST APIs

Realm 简单地整合了 REST API,并且在几个方面胜过了没有本地缓存的 REST API:

  • Realm 中的缓存数据允许您进行离线访问,而 REST API 则一直需要网络连接才能使用;
  • 通过在 Realm 中缓存整个数据集,您就可以执行本地查询并且提供更好的搜索和搜索体验,这恰恰是单独的 REST 无法实现的功能;
  • 在 Realm 中存储您的数据可以减轻服务器端的负荷,这需要在检索新的数据或者改变数据的时候才访问服务器。

最佳实践

  1. 异步请求 — 网络请求和其他在闭包中进行的操作应该放到后台进程中运行,以免影响用户交互。同理,Realm数据库中插入和修改大量数据的时候,应该在后台进程中进行。您可以使用通知来响应后台操作中的变化;
  2. 缓存大量的数据集 — 我们建议您对可能使用的数据进行预处理并且存储到本地 Realm 数据库中。 这么做可以让您在数据集中进行完整的本地查询。
  3. “插入或更新” — 如果您的数据集有一个特有的标识符,例如说主键,您可以通过它来轻松实现使用 +[RLMObject createOrUpdateInRealm:withValue:] 进行的“插入或更新”的逻辑操作:当从 REST API 中得到响应之后,这些方法会自行检查每条记录是否存在,如果记录存在的话就执行更新操作,否则的话就执行插入操作。

范例

以下是如何使用带有 REST API 功能的 Realm 数据库示例。在这个示例里,我们将从 foursquare API 那里获取一组 JSON 格式的数据,然后将它以 Realm Objects 的形式储存到默认的 Realm 数据库里。

如果您想参考类似示例的实际操作,请观看我们的视频demo

首先我们要创建一个默认的 Realm 数据库实例,用于存储数据以及从 API 检索数据集。为了更简单易读,我们在这个例子里面运用了[NSData initWithContentsOfURL]

// 调用API
NSData *response = [[NSData alloc] initWithContentsOfURL:
                    [NSURL URLWithString:@"https://api.foursquare.com/v2/venues/search?near=San%20Francisco&limit=50"]];

// 对 JSON 的回应数据进行反序列化操作
NSDictionary *json = [[NSJSONSerialization
                       JSONObjectWithData:response
                                  options:kNilOptions
                                    error:&error] objectForKey:@"response"];

这条回应数据包含了一个 JSON 数组,形式类似于:

{
  "venues": [
    {
      "id": "4c82f252d92ea09323185072",
      "name": "杭州西湖",
      "contact": {
        "phone": "0571-xxxxxxxx"
      },
      "location": {
        "lat": 30.885,
        "lng": 120.510,
        "postalCode": "94103",
        "cc": "CN",
        "state": "浙江",
        "country": "中华人民共和国"          
      }
    }
  ]
}

要想把JSON数据导入Realm中我们有很多办法。您可以读取 NSDictionary 然后通过自定义的插入函数将其手动映射到一个 RLMObject 上。为了确保演示效果,在这个示例里,我们将直接把 NSDictionary 插入到Realm中,然后让 Realm 自行快速地将其映射到 RLMObject 上。为了确保示例能够成功,我们需要一个所有属性完全匹配 JSON 键结构 的RLMObject 结构体。如果 JSON 的键结构不匹配 RLMObject 结构体属性结构的话,那么就会在插入时被忽略。因此,下面的 RLMObject 定义将是有效的:

// Contact.h
@interface Contact : RLMObject
@property NSString *phone;
@end

@implementation Contact
+ (NSString)primaryKey {
    return @"phone";
}
@end
RLM_ARRAY_TYPE(Contact)

// Location.h
@interface Location : RLMObject
@property double lat; // 纬度
@property double lng; // 经度
@property NSString *postalCode;
@property NSString *cc;
@property NSString *state;
@property NSString *country;
@end

@implementation Location
@end
RLM_ARRAY_TYPE(Location)

// Venue.h
@interface Venue : RLMObject
@property NSString *id;
@property NSString *name;
@property Contact  *contact;
@property Location *location;
@end

@implementation Venue
+ (NSString)primaryKey {
    return @"id";
}
@end
RLM_ARRAY_TYPE(Venue)

因为结果集是以数组的形式给我们的,我们要调用 [Venue createInDefaultRealmWithValue:] 方法来为每个元素创建一个对象。这里会创建 Venue 对象和一个 JSON 格式的子对象,并将这些新建的对象加入到默认的 Realm 数据库中:

// 从回应数据中提取 venues 数组
NSArray *venues = json[@"venues"];

RLMRealm *realm = [RLMRealm defaultRealm];
[realm beginWriteTransaction];
// 为数组中的每个元素保存一个对象(以及其依赖对象)
for (NSDictionary *venue in venues) {
    [Venue createOrUpdateInDefaultRealmWithValue:venue];
}
[realm commitWriteTransaction];

当前版本的限制

Realm 现在还是 beta 版本。我们还在为 1.0 版本的发布一直不断地添加新特性以及修复bug。在正式版发布之前,我们整理了一些常见的限制情况。

如果您想要查看完整的问题列表,请参阅 GitHub issues

基本的限制

Realm 致力于平衡数据库读取的灵活性和性能。为了实现这个目标,在 Realm 中所存储的信息的各个方面都有基本的限制。例如:

  1. 类名称的长度必须在 0 和 63 字节之间。支持 UTF8 字符。如果超出了这个限制,应用程序的初始化将抛出异常。
  2. 属性名称的长度必须在 0 和 63 字节之间。支持 UTF8 字符。如果超出了这个限制,应用程序的初始化将抛出异常。
  3. NSData 不能保存超过 16 MB 大小的数据。如果要存储大量的数据,可通过将其分解为16MB 大小的块,或者直接存储在文件系统中,然后将文件路径存储在 Realm 中。如果您的应用试图存储一个大于 16MB 的单一属性,系统将在运行时抛出异常。
  4. NSDate 的存储精度只能到秒。参考NSDate 当前的局限可查看更多相关信息。
  5. 任何一个 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 模拟器的支持库、某些调试符号以及某些当编译应用时会被 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里面可以找到。