You are reading the documentation for an older version of Realm. You can view the latest documentation instead.

即刻开始

下载 Objective‑C 版本的 Realm

或者,从 Github 上的 realm-cocoa 库下载源代码。

如果您的应用中只打算使用纯 Swift 进行开发,那么您应当考虑使用 Realm 的 Swift 版本。 注意:不能同时使用 Objective‑C 版本和 Swift 版本的 Realm,两者是不可互操作的。

Realm Objective‑C 能够让您以安全、稳定、迅速的方式,来高效编写引用的数据模型层。如下例所示:

// 定义模型的做法和定义常规 Objective‑C 类的做法类似
@interface Dog : RLMObject
@property NSString *name;
@property NSData   *picture;
@property NSInteger age;
@end
@implementation Dog
@end
RLM_ARRAY_TYPE(Dog)
@interface Person : RLMObject
@property NSString             *name;
@property RLMArray<Dog *><Dog> *dogs;
@end
@implementation Person
@end

// 使用的方法和常规 Objective‑C 对象的使用方法类似
Dog *mydog = [[Dog alloc] init];
mydog.name = @"Rex";
mydog.age = 1;
mydog.picture = nil; // 该属性是可空的
NSLog(@"Name of dog: %@", mydog.name);

// 检索 Realm 数据库,找到小于 2 岁 的所有狗狗
RLMResults<Dog *> *puppies = [Dog objectsWhere:@"age < 2"];
puppies.count; // => 0 因为目前还没有任何狗狗被添加到了 Realm 数据库中

// 存储数据十分简单
RLMRealm *realm = [RLMRealm defaultRealm];
[realm transactionWithBlock:^{
    [realm addObject:mydog];
}];

// 检索结果会实时更新
puppies.count; // => 1

// 可以在任何一个线程中执行检索、更新操作
dispatch_async(dispatch_queue_create("background", 0), ^{
    @autoreleasepool {
        Dog *theDog = [[Dog objectsWhere:@"age == 1"] firstObject];
        RLMRealm *realm = [RLMRealm defaultRealm];
        [realm beginWriteTransaction];
        theDog.age = 3;
        [realm commitWriteTransaction];
    }
});

准备工作

  • Xcode 8.0 或者更高版本
  • 构建目标 (target):iOS 8 及其以上版本、macOS 10.9 及其以上版本,此外支持任意版本的 tvOS 和 watchOS

安装

  1. 下载 Realm 的最新发布版本,并解压;
  2. 前往 Xcode 工程的 “General” 设置选项卡中,从 ios/dynamic/osx/tvos/ 或者 watchos/ 目录中,将 Realm.framework 拖曳到 “Embedded Binaries” 部分内。请确保勾选了 Copy items if needed(除非项目中有多个平台都需要使用 Realm ),然后单击 Finish 按钮;
  3. 在单元测试目标的 “Build Settings” 中,将 Realm.framework 的父目录添加到 “Framework Search Paths” 部分中;
  4. 如果使用了 Realm Swift,请将 Swift/RLMSupport.swift 文件拖曳到 Xcode 工程的文件导航栏中,请确保选中了 Copy items if needed 选择框;
  5. 如果在 iOS、watchOS 或者 tvOS 工程中使用 Realm,请在应用目标的 “Build Phases” 中创建一条新的 “Run Script Phase”,然后将下面这段代码粘贴到脚本文本框内:

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

    因为要绕过 App Store 出现的提交 bug, 因此这一步在打包通用二进制文件时是必须的。

  1. 安装 CocoaPods 1.1.0 或者更高版本
  2. 执行 pod repo update,从而让 CocoaPods 更新至目前最新可用的 Realm 版本;
  3. 在您的 Podfile 中,将 pod 'Realm' 添加到应用目标中,并将 pod 'Realm/Headers' 添加到测试目标中;
  4. 在命令行中执行 pod install
  5. 使用由 CocoaPods 生成的 .xcworkspace 文件来编写工程;
  6. 如果使用了 Realm Swift,请将 Swift/RLMSupport.swift 文件拖曳到 Xcode 工程的文件导航栏中,请确保选中了 Copy items if needed 选择框。
  1. 安装 Carthage 0.17.0 或者更高版本
  2. github "realm/realm-cocoa" 添加到 Cartfile 中;
  3. 执行 carthage update
  4. Carthage/Build 目录中选择适合您项目的平台目录,将 Realm.framework 拖曳到 Xcode 工程的 “General” 设置选项卡的 “Linked Frameworks and Libraries” 部分内;

iOS、tvOS、watchOS:前往应用目标的 “Build Phases” 设置选项卡中,单击 “+” 图标,然后选择 “New Run Script Phase”。创建一个 Run Script,并输入以下内容:

/usr/local/bin/carthage copy-frameworks

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

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

这段脚本用来绕过在打包通用二进制文件时,App Store 出现的提交 bug

  1. 下载 Realm 的最新发布版本,并解压;
  2. 前往 Xcode 工程的 “General” 设置选项卡中,从 ios/static/ 目录中将 Realm.framework 拖曳到 Xcode 工程的文件导航器内。请确保勾选了 Copy items if needed,然后单击 Finish 按钮;
  3. 在 Xcode 文件导航器中选中工程。然后选择应用目标,前往 Build Phases 选项卡。在 Link Binary with Libraries 部分中单击 + 按钮,然后添加 libc++.tbdlibz.tbd
  4. 如果使用了 Realm Swift,请将 Swift/RLMSupport.swift 文件拖曳到 Xcode 工程的文件导航栏中,请确保选中了 Copy items if needed 选择框。

Realm Studio

Realm Studio is our premiere developer tool, built so you can easily manage the Realm Database and Realm Platform. With Realm Studio, you can open and edit local and synced Realms, and administer any Realm Object Server instance. It supports Mac, Windows and Linux.

Realm Studio

可以使用 Tools > Generate demo database 菜单项,来生成一个带有样本数据的测试数据库。

如果您不知道何处寻找应用的 Realm 文件,请查看这个 StackOverflow 回答 来了解详细步骤。

示例

您可以在压缩包examples/ 目录下,找到 iOS 以及 OS X 的示例应用,其演示了很多 Realm 的功能,例如迁移、如何在 UITableViewController 中使用 Realm、加密、命令行工具等等。

使用 Realm 框架

在 Objective-C 源文件的顶部,使用 #import <Realm/Realm.h> 来导入 Realm Objective-C,从而让其能够在代码中使用。在 Swift 源文件的顶部(如果有的话),使用 import Realm。这样一切就准备妥当了!

在 Swift 当中使用 Realm Objective-C

如果您打算使用完全由 Swift 编写的 Realm 库,那么请考虑使用 Realm Swift

Realm Objective-C 旨在能够在 Objective‑C 和 Swift 混编的工程中完好使用。在 Swift 中可以正常使用 Realm Objective-C 的所有功能,比如说定义模型,以及使用 Realm 的 Objective-C API。不过,与在纯 Objective‑C 项目中使用相比,您还应当注意某些微小的区别:

RLMSupport.swift

我们建议您将 Swift/RLMSupport.swift 文件一同编译进去(这个文件同样可以在我们提供的压缩包中找到)。这个文件为 Realm Objective‑C 的集合类型添加了 Sequence 实现,并且重新暴露了某些不能被 Swift 原生访问的 Objective‑C 方法,比如说包含可变参数的方法等等。

Realm Objective‑C 默认情况下不会包含此文件,因为这会强制让所有使用 Realm Objective‑C 的用户去嵌入多余的 Swift 动态库,而不管他们是否在应用中使用了 Swift!

RLMArray 属性

在 Objective‑C 中,我们需要依靠协议一致性 (protocol conformance),才能够让 Realm 知晓 RLMArray 对多关系 中的内含对象类型。在 Swift 中,这种形式的语法是无法实现的。因此,您应当使用以下语法来声明 RLMArray 属性:

class Person: Object {
    @objc dynamic var dogs = RLMArray(objectClassName: Dog.className())
}

这等同于 Objective‑C 中的如下形式:

@interface Person : RLMObject
@property RLMArray<Dog *><Dog> *dogs;
@end

tvOS

由于 tvOS 禁止向 “Documents” 目录中写入数据,因此默认的 Realm 路径将被设置为 NSCachesDirectory。然而,要注意的是,tvOS 会随时清理 “Caches” 目录下的文件,因此我们建议您将 Realm 视为一种全新的缓存机制,而不是用来存储重要的用户 数据。

如果您想要在 tvOS 应用和 TV 服务扩展 (services extension)(例如 Top Shelf 扩展)之间共享 Realm 文件的话,那么您必须要使用应用程序组 (application group) 共享容器 (shared container) 当中的 Library/Caches/ 目录。

// end declarations
RLMRealmConfiguration *configuration = [RLMRealmConfiguration defaultConfiguration];
configuration.fileURL = [[[NSFileManager defaultManager]
    containerURLForSecurityApplicationGroupIdentifier:@"group.io.realm.examples.extension"]
    URLByAppendingPathComponent:@"Library/Caches/default.realm"];

您同样可以向应用中加入预构建的 Realm 文件。不过,一定要遵循 App Store 的相关规定,保证应用大小在 200MB 以内。您可以查看我们的 tvOS 范例,它提供了一个 tvOS 示例应用,展示如何使用 Realm 的离线缓存功能,以及预载入数据的 Realm 数据库。

使用带有后台应用刷新功能的 Realm

在 iOS 8 及其以上平台中,当设备锁定之后,应用内的文件会自动被 NSFileProtection 所加密。如果您的应用试图在设备锁定、且 Realm 文件的 NSFileProtection 属性被设置为“允许加密”(默认配置)的时候,去执行任何涉及 Realm 操作的话,那么就会抛出一个 open() failed: Operation not permitted 异常。

为了解决这个问题,请确保将 Realm 文件本身以及其辅助文件两者的文件保护属性降级,比如说 NSFileProtectionCompleteUntilFirstUserAuthentication,这样即便设备被锁定,文件仍然允许被访问。

如果您选择以这种方式摈弃完整的 iOS 文件加密机制的话,那么我们建议您使用Realm 内置的加密机制,从而确保您的数据仍能够得到正确保护。

由于辅助文件有些时候可能会延迟创建,或者在中间操作中被删除,因此我们建议您将文件保护属性应用到包含这些 Realm 文件的父文件夹中。这将确保所有相关的 Realm 文件无论是何时所创建的,都能够应用此属性配置。

RLMRealm *realm = [RLMRealm defaultRealm];

// 获取 Realm 文件的父目录
NSString *folderPath = realm.configuration.fileURL.URLByDeletingLastPathComponent.path;

// 禁用此目录的文件保护
[[NSFileManager defaultManager] setAttributes:@{NSFileProtectionKey: NSFileProtectionNone}
                                 ofItemAtPath:folderPath error:nil];

Realm 数据库

Realm 数据库是 Realm 移动端数据库容器的一个实例。Realm 数据库可以是本地化的,也可以是可同步的

可同步 Realm 数据库 使用 Realm 对象服务器 (Realm Object Server) 来实现其内容与其他设备之间的同步,这整个过程是透明的。实际上,任何一种类型的 Realm 数据库的使用方式是完全相同的,虽然可同步 Realm 数据库需要一个用户对象才能打开,该用户对象需要得到对象服务器的认证,并且获取打开该 Realm 数据库的授权。当应用在使用可同步 Realm 数据库的过程中,其他具备该 Realm 数据库写入权限的设备可能会将 Realm 中的数据给更新掉。

欲了解更多关于 Realm 数据库的详细信息,请参阅 Realm 数据模型

打开 Realm 数据库

要打开一个 Realm 数据库,首先需要初始化一个新的 RLMRealm 对象:

RLMRealm *realm = [RLMRealm defaultRealm];

[realm transactionWithBlock:^{
    [realm addObject:mydog];
}];

这将会初始化出一个默认 Realm 数据库

配置 Realm 数据库

在打开 Realm 数据库之前,可以对其进行配置。通过创建一个 RLMRealmConfiguration 的对象实例,然后配置相应的属性。通过创建并自定义相关的配置值,使得您可以实现个性化的设置,包括如下方面:

  • 对于本地 Realm 数据库而言,可以配置 Realm 文件在磁盘上的路径;
  • 对于可同步 Realm 数据库而言,可以配置管理该 Realm 数据库的用户,以及 Realm 数据库在 Realm 对象服务器上的远程路径;
  • 对于架构版本之间发生变化的 Realm 数据库而言,可以通过迁移功能来控制旧架构的 Realm 数据该如何更新到最新的架构。
  • 对于存储的数据量过大、或者数据频繁发生变化的 Realm 数据库而言,可以通过压缩功能来控制 Realm 文件该如何实现压缩,从而确保能高效地利用磁盘空间。

要应用配置,可以在每次需要获取 Realm 实例的时候,通过向 +[RLMRealm realmWithConfiguration:config error:&err] 方法传递该配置对象,或者通过 [RLMRealmConfiguration setDefaultConfiguration:config] 方法,将默认 Realm 数据库的默认配置设置为我们所需的配置。

例如,假设有一个应用要求用户必须要登录到 Web 后端服务器中,并且需要支持账户快速切换功能的话。 那么您可以通过以下代码,来为每个账户提供一个独立的 Realm 数据库,并且当前账户所使用的数据库将作为默认 Realm 数据库来使用:

@implementation SomeClass
+ (void)setDefaultRealmForUser:(NSString *)username {
    RLMRealmConfiguration *config = [RLMRealmConfiguration defaultConfiguration];

    // 使用默认的目录,但是请将文件名替换为用户名
    config.fileURL = [[[config.fileURL URLByDeletingLastPathComponent]
                            URLByAppendingPathComponent:username]
                            URLByAppendingPathExtension:@"realm"];

    // 将该配置设置为默认 Realm 配置
    [RLMRealmConfiguration setDefaultConfiguration:config];
}
@end

您可以创建多个配置对象,这样便可以单独控制各个 Realm 数据库的版本、架构以及位置。

RLMRealmConfiguration *config = [RLMRealmConfiguration defaultConfiguration];

// 获取预植数据库文件的 URL
config.fileURL = [[NSBundle mainBundle] URLForResource:@"MyBundledData" withExtension:@"realm"];
// 以只读模式打开该文件,这是因为应用的预植数据库是不可写的
config.readOnly = YES;

// 使用该配置来打开 Realm 数据库
RLMRealm *realm = [RLMRealm realmWithConfiguration:config error:nil];

// 从预植 Realm 数据库中读取某些数据
RLMResults<Dog *> *dogs = [Dog objectsInRealm:realm where:@"age > 5"];

可写 Realm 文件的最常见存储位置是 iOS 上的 “Documents” 文件夹,以及 macOS 上的 “Application Support” 文件夹。请遵守 Apple’s iOS Data Storage Guidelines 的相关规定,因此我们建议,如果文件是可以被应用重新生成的话,那么请将其存储在 <Application_Home>/Library/Caches 目录下。如果使用自定义 URL 来初始化 Realm 数据库,那么所描述的位置必须具备写入权限。

默认 Realm 数据库

您或许已经注意到,我们是通过调用 [RLMRealm defaultRealm] 来初始化 realm 变量并访问的。这个方法会返回一个 RLMRealm 对象,该对象映射到应用 Documents 文件夹(iOS)或者 Application Support 文件夹(macOS)中的 default.realm 文件。

Realm API 中的许多方法都存在一个接受 RLMRealm 实例为参数的版本,以及另一个使用默认 Realm 数据库的便利版本。例如,[RLMObject allObjects] 等同于 [RLMObject allObjectsInRealm:[RLMRealm defaultRealm]]

请注意,默认的 Realm 构造方法和默认的 Realm 便利方法均不允许进行错误处理;只有在初始化 Realm 数据库不可能失败的情况下,才去使用它们。欲了解更多详情,请参见文档的错误处理部分。

打开可同步 Realm 数据库

Realm 对象服务器上的 Realm 数据库同样也可以使用 RLMRealmConfiguration 和相关的工厂方法进行配置,这与之前创建本地 Realm 数据库的做法基本类似,只不过在 RLMRealmConfiguration 中,需要将 syncConfiguration 属性设置为 RLMSyncConfiguration。可同步 Realm 数据库 (synchronized Realm) 可通过 URL 地址来进行定位。

RLMSyncUser *user = [RLMSyncUser currentUser];

// 创建配置
NSURL *syncServerURL = [NSURL URLWithString: @"realm://localhost:9080/~/userRealm"];
RLMRealmConfiguration *config = [RLMRealmConfiguration defaultConfiguration];
config.syncConfiguration = [[RLMSyncConfiguration alloc] initWithUser:user realmURL:syncServerURL];

// 打开远程 Realm 数据库
RLMRealm *realm = [RLMRealm realmWithConfiguration:config error:nil];
// 任何对此 Realm 数据库所做的操作,都会同步到所有设备上!

如果 Realm 数据库设定为只读权限,那么您必须使用异步打开 Realm 数据库一节中所述的 asyncOpen API。如果不使用 asyncOpen 来打开只读 Realm 数据库,那么就会出现错误。

对于可同步 Realm 数据库而言,无法对 inMemoryIdentifier 或者 fileURL 选项进行配置。设置这两个属性会自动导致 syncConfiguration 被置为 nil(反之亦然)。本框架将自行负责可同步 Realm 数据库是如何缓存或者存储在磁盘上的。

异步打开 Realm 数据库

如果打开 Realm 数据库的操作需要耗费大量时间的话,比如说需要执行迁移压缩 或者需要从可同步 Realm 数据库下载远程内容,那么建议使用 asyncOpen API。这使得您可以在调度到指定队列之前,在后台线程中执行任意的初始化工作。当可同步 Realm 数据库只能以只读权限打开的时候,那么必须使用 asyncOpen

RLMRealmConfiguration *config = [RLMRealmConfiguration defaultConfiguration];
config.schemaVersion = 1;
config.migrationBlock = ^(RLMMigration *migration, uint64_t oldSchemaVersion) {
    // 可能会进行冗长的数据迁移操作
};
[RLMRealm asyncOpenWithConfiguration:config
                       callbackQueue:dispatch_get_main_queue()
                            callback:^(RLMRealm *realm, NSError *error) {
    if (realm) {
        // 成功打开 Realm 数据库,迁移操作在后台线程中进行
    } else if (error) {
        // 处理在打开 Realm 数据库期间所出现的错误
    }
}];

完全下载

在某些情况下,除非所有的远程数据均已下载完毕,否则的话您不希望将 Realm 数据库打开。例如,假设您需要向用户展示所有可用的邮政编码列表。这是 asyncOpen API 的另一种使用方法。这将在后台中下载 Realm 数据库,直到下载完毕后才会进行通知:

RLMRealmConfiguration *config = [RLMRealmConfiguration defaultConfiguration];
config.syncConfiguration = [[RLMSyncConfiguration alloc] initWithUser:user realmURL:realmURL];
[RLMRealm asyncOpenWithConfiguration:config
                       callbackQueue:dispatch_get_main_queue()
                            callback:^(RLMRealm *realm, NSError *error) {
    if (realm) {
        // 成功打开 Realm 数据库,所有远程数据均已下载完毕
    } else if (error) {
        // 处理在打开或者下载 Realm 数据库期间所出现的错误
    }
}];

内存中 Realm 数据库

通过配置 RLMRealmConfiguration 中的 inMemoryIdentifier 属性,而不是 fileURL 属性,这样就能够创建一个完全在内存中运行的 Realm 数据库 (in-memory Realm),它将不会存储在磁盘当中。设置 inMemoryIdentifier 会将 fileURL 置为 nil(反之亦然)。

RLMRealmConfiguration *config = [RLMRealmConfiguration defaultConfiguration];
config.inMemoryIdentifier = @"MyInMemoryRealm";
RLMRealm *realm = [RLMRealm realmWithConfiguration:config error:nil];

内存中 Realm 数据库无法在应用启动期间存储数据,但是 Realm 数据库的其他功能都能正常使用,包括查询、关系以及线程安全。 如果您需要提供一种灵活的数据访问方式,而不占用磁盘空间的话,那么这是一个很有用的选择。

内存中 Realm 数据库可以在临时目录中创建多个文件,以用于处理诸如跨进程通知之类的协调工作。实际上并不会有任何数据会被写入到磁盘文件当中,除非由于内存占用过高,操作系统进行了内存交换。

注意: 对于具备特定标识符的内存中 Realm 数据库而言,如果所有相关的实例引用均被移除的话,那么该 Realm 数据库当中的所有数据都会被删除。我们建议您在应用生命周期内对内存中 Realm 数据库进行强引用。(对于磁盘 Realm 数据库而言,这个操作是不必要的。)

错误处理

与任何磁盘 I/O 操作类似,如果资源受到限制,那么创建 RLMRealm 实例有可能会失败。实际上,只有在指定线程中第一次创建 Realm 实例时才可能会发生这种情况。在同一个线程中继续访问 Realm 数据库将会重用缓存的实例,这个操作是不可能失败的。

为了处理在指定线程中第一次创建 Realm 数据库时所发生的错误,我们提供了一个 NSError 指针类型的 error 参数:

NSError *error = nil;

RLMRealmConfiguration *config = [RLMRealmConfiguration defaultConfiguration];
RLMRealm *realm = [RLMRealm realmWithConfiguration:config error:&error];
if (!realm) {
    // 错误处理
}

Realm 辅助文件

出于内部操作等因素的考量,除了正常的 .realm 文件之外,Realm 还会生成和维护一些额外的文件和目录。

  • .realm.lock - 资源锁定文件;
  • .realm.management - 存放进程锁文件的目录;
  • .realm.note - 用于通知的命名管道。

这些文件不会对 .realm 数据库文件造成任何影响,即便所依赖的数据库文件被删除或者被替换掉,也不会引发任何异常行为。

报告 Realm 问题的时候,请将这些辅助文件 (auxiliary Realm) 连同主要的 .realm 文件一同提交,因为它们很可能会包含某些对调试问题有用的信息。

预植 Realm 数据库

为应用提供一些初始数据的做法非常常见,这样就让用户在首次启动时进行访问。具体做法是:

  1. 首先,向 Realm 数据库中植入数据。所使用的数据模型应当与最终发布应用时所使用的 Realm 数据模型相同,然后向数据库中写入所需要的初始数据。由于 Realm 文件是跨平台的,因此您可以使用 macOS 应用(参见我们的 JSONImport 示例),或者运行在模拟器中的 iOS 应用来完成数据的植入;
  2. 在生成此 Realm 文件的代码中,最后您应当制作一个此数据库的压缩版本(参见 -[RLMRealm writeCopyToPath:error:]))。这可以减少 Realm 文件的大小,使您的应用体积更小,便于用户下载。
  3. 将您 Realm 文件的压缩版本拖曳到应用的 Xcode 项目导航栏中;
  4. 前往 Xcode 中应用目标的 “Build Phases” 选项卡,将 Realm 文件添加到 “Copy Bundle Resources” 构建阶段中。
  5. 此时,应用已经可以访问该预植 Realm 文件 (bundled Realm) 了。您可以使用 [[NSBundle mainBundle] pathForResource:ofType:] 来获取路径;
  6. 如果预植 Realm 数据库中的数据是固定不变、不需要修改的,那么您可以在 RLMRealmConfiguration 对象中,通过设置 readOnly = true 来直接从该路径中打开此文件。否则,如果要对初始数据进行更改的话,您需要使用 [[NSFileManager defaultManager] copyItemAtPath:toPath:error:] 将预植文件复制到应用的 Documents 目录下。

关于如何使用预植 Realm 文件,您可以参考我们的迁移示例应用

类的子集限定

在某些情况下,您可能想要限制某些类只能够存储在指定 Realm 数据库中。例如,假如有两只团队分别负责应用的不同部分,两者都在内部使用了 Realm 数据库,而又不想协调两个团队之间可能会出现的数据迁移。那么您可以通过设置 RLMRealmConfigurationobjectClasses 属性来实现这一点:

RLMRealmConfiguration *config = [RLMRealmConfiguration defaultConfiguration];
config.objectClasses = @[MyClass.class, MyOtherClass.class];
RLMRealm *realm = [RLMRealm realmWithConfiguration:config error:nil];

压缩 Realm 数据库

由于 Realm 的工作原理所致,使得 Realm 的文件尺寸总是比存储在其中的数据总量要大。至于为什么这种架构可以实现 Realm 卓越的高性能、高并发和安全性,可以参见线程方面的文档。

为了避免系统调用带来的性能浪费,Realm 文件在运行时很少会减少自身的体积。相反,Realm 文件将会以特定的大小进行增长,从而新的数据可以写入到文件内部未使用的空间。为了解决这个问题,您可以在 Realm 的配置对象上对 shouldCompactOnLaunch 属性进行配置,来决定首次打开该 Realm 文件时是否对其进行压缩。例如:

RLMRealmConfiguration *config = [RLMRealmConfiguration defaultConfiguration];
config.shouldCompactOnLaunch = ^BOOL(NSUInteger totalBytes, NSUInteger usedBytes){
    // totalBytes 指的是硬盘上文件的大小(以字节为单位)(数据 + 可用空间)
    // usedBytes 指的是文件中数据所使用的字节数

    // 如果文件的大小超过 100 MB且已用空间低于 50%时,进行压缩
    NSUInteger oneHundredMB = 100 * 1024 * 1024;
    return (totalBytes > oneHundredMB) && (usedBytes / totalBytes) < 0.5;
};

NSError *error = nil;
// 如果配置条件满足,那么 Realm 就会在首次打开时被压缩
RLMRealm *realm = [RLMRealm realmWithConfiguration:config error:&error];
if (error) {
    // 处理打开 Realm 或者压缩时产生的错误
}

压缩操作将会读取 Realm 文件的全部内容,然后在另一个地方重新写成一个新的文件,最后将原文件进行替换。根据文件中数据量的大小,因此这很可能是一个极度耗费时间的操作。

我们建议您多尝试几次,从而找出执行压缩以及 Realm 文件增长过大之间的良好平衡。

最后,如果另一个进程正在访问 Realm 数据库,即便配置条件得到满足,压缩操作也会被跳过。这是因为在 Realm 被访问期间,无法安全地执行压缩操作。

可同步 Realm 数据库不支持配置 shouldCompactOnLaunch。这是因为压缩不会保留事务日志,而同步操作必须使用事务日志方可进行。

删除 Realm 文件

在某些情况下,例如清除缓存、或者重置整个数据集之类的操作,那么就可能需要从磁盘中将 Realm 文件给完全删除掉。

除非必要,否则 Realm 数据库会避免将数据复制到内存中,因此由 Realm 管理的所有对象都会包含磁盘上文件的引用,并且必须在文件被安全删除之前完成释放。这包括从 Realm 读取到(或者添加到 Realm 中)的所有对象,包括所有 RLMArrayRLMResultsRLMThreadSafeReference 对象,以及 RLMRealm 本身。

实际上,这意味着对 Realm 文件的删除,要么在应用启动时、在打开 Realm 数据库之前完成,要么只在显式声明的自动释放池 中打开 Realm 数据库,然后在自动释放池后面进行删除。这样才能够确保所有的 Realm 对象都能够成功释放。

最后,尽管不是必须的,不过您应当将 Realm 辅助文件连同 Realm 文件一起删除,以完全清除所有的相关文件。

@autoreleasepool {
    // 在这里进行所有的 Realm 操作
}
NSFileManager *manager = [NSFileManager defaultManager];
RLMRealmConfiguration *config = [RLMRealmConfiguration defaultConfiguration];
NSArray<NSURL *> *realmFileURLs = @[
    config.fileURL,
    [config.fileURL URLByAppendingPathExtension:@"lock"],
    [config.fileURL URLByAppendingPathExtension:@"note"],
    [config.fileURL URLByAppendingPathExtension:@"management"]
];
for (NSURL *URL in realmFileURLs) {
    NSError *error = nil;
    [manager removeItemAtURL:URL error:&error];
    if (error) {
        // 错误处理
    }
}

数据模型

Realm 的数据模型由标准的 Objective‑C 类所定义,其中具备标准的属性。要创建一个数据模型, 只需要继承 RLMObject 或者某个已存在的 Realm 数据模型类。 Realm 模型对象的绝大部分功能与其他 Objective‑C 对象相同。您可以在其中自定义相关的方法,或者实现协议,以及实现其他对象中的功能。 不过主要的限制在于,您只能在对象被创建的线程中使用该对象,并且您也无法直接访问其实例变量来获取存储属性。一旦对这两个属性进行配置,都会自行导致

对于关系与嵌套数据结构,可以通过添加对应类型的 RLMArrays 对象列表来完成构建。RLMArray 实例同样也可以作为基础类型的集合(比如说:字符数组或者整数数组)。

#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 *><Dog> *dogs;
@end
RLM_ARRAY_TYPE(Person) // 定义 RLMArray<Person>

// Implementations
@implementation Dog
@end // none needed

@implementation Person
@end // none needed

由于 Realm 会在启动后将所有代码中定义的模型进行解析,因此即便这些模型没有使用过,都需要符合相关规则。

在 Swift 使用 Realm 的时候,Swift.reflect(_:) 函数可用来读取模型中的相关信息,而这需要 init() 被成功调用。这意味着所有不可空的属性都必须配置一个默认值。

您可以参见 RLMObject 的 API 文档 来获取详细信息。

支持的属性类型

Realm 支持下述属性类型:BOOLboolintNSIntegerlonglong longfloatdoubleNSStringNSDateNSData 以及 被特殊类型标记的 NSNumber

CGFloat 属性被取消了,因为它不具备平台独立性。

您可以使用 RLMArray<Object *><Object>RLMObject 的相关子类来构建关系模型,诸如一对多、一对一等。

RLMArray 支持 Objective-C 泛型。下面是不同类型的属性定义含义,以及相关用途:

  • RLMArray:属性类型。
  • <Object *>:泛型特化。这可以在编译时防止错误对象类型数组的使用。
  • <Object>RLMArray 所遵守的协议。可以让 Realm 知晓如何在运行时确定该模型的架构。

必需属性

通常情况下,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 均使用了不同的存储格式,因此该属性必须由 RLMIntRLMFloatRLMDouble 或者 RLMBool 所标记。所有赋给该属性的值都会被转换为指定的类型。

请注意,NSDecimalNumber 值只能够分配给 RLMDouble 属性,并且 Realm 会存储双进度浮点数的近似值,而不是基础的十进制数值。

如果我们打算存储某人的年龄,而不是存储生日的话,那么还要允许不知道用户年龄的时候,可以将其设置为 nil

@interface Person : RLMObject
@property NSString *name;
@property NSNumber<RLMInt> *age;
@end

@implementation Person
+ (NSArray *)requiredProperties {
    return @[@"name"];
}
@end

RLMProperty 子类属性始终可以为 nil,因此无法包含在 requiredProperties 当中,此外 RLMArray 不支持存储 nil

主键

重写 +primaryKey 可以设置模型的主键。声明主键允许对象的查询和更新更加高效,并且会强制要求每个值保持唯一性。一旦将带有主键的对象添加到 Realm 数据库,那么该对象的主键将无法更改。

@interface Person : RLMObject
@property NSInteger id;
@property NSString *name;
@end

@implementation Person
+ (NSString *)primaryKey {
    return @"id";
}
@end

索引属性

要为某个属性建立索引,那么重写 +indexedProperties 即可。与主键类似,索引会稍微减慢写入速度,但是使用比较运算符进行查询的速度将会更快(它同样会造成 Realm 文件体积的增大,因为需要存储索引。)当您需要为某些特定情况优化读取性能的时候,那么最好添加索引。

@interface Book : RLMObject
@property float price;
@property NSString *title;
@end

@implementation Book
+ (NSArray *)indexedProperties {
    return @[@"title"];
}
@end

Realm 支持为字符串、整型、布尔值以及 NSDate 属性建立索引。

被忽略属性

如果您不想将模型中的某些字段保存在 Realm 数据库中,那么可以重写 +ignoredProperties。Realm 不会干涉这些属性的正常操作;它们被成员变量所持有,并且可以随意重写它们的 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

被忽略属性的行为与正常属性完全相同。不过它们不支持 Realm 属性所特有的功能(例如:无法在查询中使用,也无法触发通知)。这些属性仍能够使用 KVO 进行观察。

默认属性值

重写 +defaultPropertyValues, 可以在每次创建对象时为属性提供默认值。

@interface Book : RLMObject
@property float price;
@property NSString *title;
@end

@implementation Book
+ (NSDictionary *)defaultPropertyValues {
    return @{@"price" : @0, @"title": @""};
}
@end

属性特性

Realm 将会忽略诸如 nonatomicatomicstrongcopyweak 之类的 Objective-C 属性特性。这些特性对于 Realm 存储机制而言并没有意义。Realm 有自己优化过的存储语义。所以为了避免有人对代码产生误解,我们建议您在编写模型时不要附加任何属性特性。不过,如果您切实设置了属性特性,那么在有 RLMObject 被写入到 Realm 数据库之前,这些属性特性都会一直生效。

无论该 RLMObject 对象是否被 Realm 数据库所管理,Getter 和 Setter 的自定名称仍然可以正常使用。

由于未被管理的 Realm 对象(即不被 Realm 数据库所管理的 Realm 模型类实例)只是单纯的 NSObject 子类,因此其中的属性特性可以像其他 NSObject 对象一样被观察到。

如果在 Swift 中使用 Realm Objective-C,那么模型属性需要添加 @objc dynamic var 特性,才能使这些属性能够访问到底层数据库的数据。(您同样可以用 objcMembers 来声明类,然后使用 dynamic var 来声明模型属性。)

属性备忘单

这个表格提供了声明模型属性的简易参考:

类型 非可空值形式 可空值形式
Bool @property BOOL value; @property NSNumber<RLMBool> *value;
Int @property int value; @property NSNumber<RLMInt> *value;
Float @property float value; @property NSNumber<RLMFloat> *value;
Double @property double value; @property NSNumber<RLMDouble> *value;
String @property NSString *value; 1 @property NSString *value;
Data @property NSData *value; 1 @property NSData *value;
Date @property NSDate *value; 1 @property NSDate *value;
Object 不存在:必须是可空值 @property Object *value;
List @property RLMArray<Class *><Class> *value; 不存在:必须是非可空值
LinkingObjects @property (readonly) RLMLinkingObjects<Object *> *value; 2 不存在:必须是非可空值

Objective-C 引用类型的必需属性必须结合重写 +requiredProperties 方法来实现:

@implementation MyModel
+ (NSArray *)requiredProperties {
    // The array must contain the names of all required properties.
    return @[@"value"];
}
@end

操作 Realm 对象

对象的自更新

RLMObject 实例是底层数据的动态体现,会自动进行更新;因此这意味着无需去刷新对象的当前状态。修改某个对象的属性,会立即影响到所有指向该对象的其他实例。

Dog *myDog = [[Dog alloc] init];
myDog.name = @"Fido";
myDog.age = 1;

[realm transactionWithBlock:^{
    [realm addObject:myDog];
}];

Dog *myPuppy = [[Dog objectsWhere:@"age == 1"] firstObject];
[realm transactionWithBlock:^{
    myPuppy.age = 2;
}];

myDog.age; // => 2

这不仅使得 Realm 保证高速和高效,同时还让代码更为简洁、更为灵活。如果您的 UI 代码基于某个特定的 Realm 对象来实现,那么在触发 UI 重绘以前,您根本无需进行数据刷新或者重新检索。

您同样也可以订阅 Realm 通知,从而知道 Realm 对象当中的数据何时进行了更新,从而决定应用的 UI 何时进行刷新。

模型继承

Realm 允许对模型进行多级继承,从而允许跨模型实现代码复用,但是某些 Cocoa 特性是没有办法使用的,比如说那些支撑运行时类的多态性的特性。下面是可以实现的操作:

  • 父类当中的类方法、实例方法和属性可以被子类继承;
  • 子类可以使用以父类为参数的方法和函数。

下列操作目前是无法实现的:

  • 多态类之间的强制转换(例如:子类转换为另一个子类,子类转换为父类,父类转换成子类,等等);
  • 同时对多个类进行检索;
  • 包含多个类的容器(RLMArray 以及 RLMResults)。

目前正在尝试向 Realm 中增加此类功能。与此同时,我们提供了一些代码示例,展示了对一些常见模式的处理方法。

此外,如果您的代码实现允许的话,我们建议您使用下述模式,即使用类组合模式来构建子类,从而将其他类当中的逻辑给包含进去:

// Base Model
@interface Animal : RLMObject
@property NSInteger age;
@end
@implementation Animal
@end

// 与 Animal 一并组合的模型
@interface Duck : RLMObject
@property Animal *animal;
@property NSString *name;
@end
@implementation Duck
@end

@interface Frog : RLMObject
@property Animal *animal;
@property NSDate *dateProp;
@end
@implementation Frog
@end

// Usage
Duck *duck =  [[Duck alloc] initWithValue:@{@"animal" : @{@"age" : @(3)}, @"name" : @"Gustav" }];

集合

Realm 拥有许多能够表示一组对象的类型,称之为 “Realm 集合”:

  1. RLMResults 类,表示检索所返回的对象集合。
  2. RLMArray 类,表示模型之间的对多关系
  3. RLMLinkingObjects 类,表示模型之间的双向关系](#inverse-relationships)。
  4. RLMCollection 协议,定义了所有 Realm 集合的常用接口。

Realm 集合类型均实现了 RLMCollection 协议,这确保 它们的行为均保持一致。这个协议继承自 NSFastEnumeration,因此它的使用方式 与 Foundation 内的集合相同。这个协议也同样声明了其他常用的 Realm 集合 API, 比如说检索、排序、聚合操作等等。RLMArray 还存在一些额外的修改操作, 这些操作没有在协议接口中定义,比如说添加或者删除对象。

使用 RLMCollection 协议, 您可以编写能够对任意 Realm 集合进行操作的泛型代码:

@implementation MyObject
- (void)operateOnCollection:(id<RLMCollection>)collection {
    // collection 既可以是 RLMResults,也可以是 RLMArray
    NSLog(@"operating on collection of %@s", collection.objectClassName);
}
@end

在 Realm 数据库间复制对象

将 Realm 对象复制到另一个 Realm 数据库非常简单,只需要将初始对象传递给 +[RLMObject createInRealm:withValue:] 方法即可。例如,[MyRLMObjectSubclass createInRealm:otherRealm withValue:originalObjectInstance]。要记住的是,Realm 对象 只能在首次所创建的线程中访问,因此这个复制操作只能够在 相同线程上的 Realm 数据库之间进行。

请注意,+[RLMObject createInRealm:withValue:] 不支持处理循环对象图。 请不要将包含双向关系(不管是直接关联还是间接关联)的 对象传递进来。

关系

您可以将任意两个 Realm 对象关联在一起。在 Realm 中构建关系非常简单:访问关联关系并不会影响数据库的速度,也不会造成内存方面的压力。让我们来探索 Realm 在两个对象之间能够定义的各种关系。

可以通过 RLMObjectRLMArray 属性来关联 RLMObjectRLMArray 的接口与 NSArray 非常类似,并且 RLMArray 当中的对象可以通过索引下标来进行访问。与 NSArray 所不同的是,RLMArray 的类型是固定的,并且 只能存放一种 RLMObject 类型。欲了解详细信息,请参阅 RLMArray 的相关 API 文档。

假设 Person 数据模型已经定义(参见数据模型),下面创建另一个名为 Dog 的数据模型:

// Dog.h
@interface Dog : RLMObject
@property NSString *name;
@end

多对一关系

要配置多对一或者一对一关系,在数据模型当中声明一个 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 将会遍历对象图,然后自动从 Realm 中检索出每个所需的对象。

多对多关系

通过 RLMArray 属性,您可以为任意数量的对象或者所支持的原始类型之间构建关系。RLMArray 可以包含其它 RLMObject 类型,也可以包含简单类型的原始值,其接口与 NSMutableArray 非常类似。

RLMArray 所包含的 Realm 对象可能会存储多个相同 Realm 对象的引用,即便对象带有主键也是如此。例如,您或许会创建一个空的 RLMArray,然后连续三次向其中插入同一个对象;每次分别使用 0、1、2 的元素索引来访问的时候,RLMArray 都会返回同一个对象。

RLMArray 可以存储原始类型,从而代替一般的 Realm 对象。为了实现此功能, 请使用下列协议来约束 RLMArrayRLMBoolRLMIntRLMFloatRLMDoubleRLMStringRLMData 或者 RLMDate

默认情况下,包含原始类型的 RLMArray 可能也会包含空值(由 NSNull 表示)。 将数组标记为非可空(通过在数组所在的模型对象类型中重写 +requiredProperties:), 同样也会导致数组当中的值变为非可空。

让我们给 Person 模型添加一个 dogs 属性,从而让其能够与多个 Dog 对象建立关系。首先,我们需要定义 RLMArray<Dog> 类型,也就是在 Dog 模型接口定义的底部使用这条宏:

// Dog.h
@interface Dog : RLMObject
// ... property declarations
@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 属性进行访问和赋值:

// Jim 是 Rex 和所有名为 "Fido" 狗狗的主人
RLMResults<Dog *> *someDogs = [Dog objectsWhere:@"name contains 'Fido'"];
[jim.dogs addObjects:someDogs];
[jim.dogs addObject:rex];

注意,虽然可以给 RLMArray 属性赋值为 nil,但是这仅仅只会“清空”该数组,而不会将该数组给移除。这意味着您永远都可以向 RLMArray 属性中添加对象,即便其之前曾被置为 nil

RLMArray 属性会确保其内部的插入次序不会被打乱。

注意,目前暂时不支持对包含原始类型的 RLMArray 进行查询。

双向关系

关系是单向的。以 PersonDog 这两个类为例。如果 Person.dogs 连接了一个 Dog 实例,那么您可以随着该连接从 Person 访问到对应的 Dog,但是是没有办法从 Dog 访问到对应的 Person 对象的。您可以设置一个一对一属性 Dog.owner 从而连接到 Person,但是这些连接实际上仍然是互相独立的。给 Person.dogs 添加一个 Dog 对象并不会将该对象的 Dog.owner 属性设置为对应的 Person。为了解决这个问题,Realm 提供了连接对象属性,从而表示这种双向关系。

@interface Dog : RLMObject
@property NSString *name;
@property NSInteger age;
@property (readonly) RLMLinkingObjects *owners;
@end

@implementation Dog
+ (NSDictionary *)linkingObjectsProperties {
    return @{
        @"owners": [RLMPropertyDescriptor descriptorWithClass:Person.class propertyName:@"dogs"],
    };
}
@end

借助连接对象属性,可以从特定属性获取连接到指定对象的所有对象。Dog 对象可以拥有一个名为 owners 属性,它包含所有 dogs 属性有该 Dog 对象的 Person 对象。将这个 owners 属性设置为 RLMLinkingObjects 类型,然后重写 +[RLMObject linkingObjectsProperties] 来表示 ownersPerson 模型对象之间的关系。

对象存储

对象的所有更改(添加、修改和删除)都必须在写入事务内完成。

Realm 对象可以被实例化,还可作为未管理对象使用(例如,还未添加到 Realm 数据库),并且使用方式与其它正常 Objective‑C 对象无异。然而,如果要在线程之间共享对象,或者在应用启动后反复使用,那么您必须将这些对象添加到 Realm 数据库中。向 Realm 数据库中添加对象必须在写入事务内完成。由于写入事务将会产生无法忽略的性能消耗,因此您应当检视您的代码,以确保尽可能减少写入事务的数量。

Realm 的写入操作是同步以及阻塞进行的,它并不会异步执行。如果线程 A 开始进行写入操作,然后线程 B 在线程 A 结束之前,对相同的 Realm 数据库也执行了写入操作,那么线程 A 必须要在线程 B 的写入操作发生之前,结束并提交其事务。写入事务会在 beginWrite() 执行时自动刷新,因此重复写入并不会产生竞争条件。

由于写入事务与其它磁盘 IO 操作类似,可能会出现失败的情况,因此 -[RLMRealm transactionWithBlock:] 以及 -[RLMRealm commitWriteTransaction] 可能会附上 NSError 指针参数,这样您就可以对诸如磁盘空间溢出之类的错误进行处理并恢复。除此之外的错误都是无法恢复的。出于简单期间,我们的代码示例并不会处理这些错误,但是您应当在应用中注意这些错误。

创建对象

当定义完数据模型之后,就可以实例化 RLMObject 子类了, 然后还可以向 Realm 数据库中添加新的实例。以这个简单的数据模型为例:

// Dog 数据模型
@interface Dog : RLMObject
@property NSString *name;
@property NSInteger age;
@end

// Implementation
@implementation Dog
@end

创建新对象的方法有很多种:

// (1) 创建 Dog 对象,然后设置其属性
Dog *myDog = [[Dog alloc] init];
myDog.name = @"Rex";
myDog.age = 10;

// (2) 从字典中创建 Dog 对象
Dog *myOtherDog = [[Dog alloc] initWithValue:@{@"name" : @"Pluto", @"age" : @3}];

// (3) 从数组中创建 Dog 对象
Dog *myThirdDog = [[Dog alloc] initWithValue:@[@"Pluto", @3]];
  1. 使用指定初始化函数来创建对象是最直观的方式。请注意,所有非可空属性必须在对象添加到 Realm 数据库之前完成赋值。
  2. 通过恰当的键值,还可以使用字典来创建对象。
  3. 最后,RLMObject 的子类还可以使用数组来完成实例化。数组中的值必须与数据模型中对应的属性次序相同。

数组中的值对应存储在 Realm 数据库当中的属性——因此您不应当指定已忽略属性或者计算属性的值。

对象创建之后,您就可以将其添加到 Realm 数据库了:

// 获取默认的 Realm 数据库
RLMRealm *realm = [RLMRealm defaultRealm];
// (每个线程)只需执行一次

// 在事务中向 Realm 数据库中添加数据
[realm beginWriteTransaction];
[realm addObject:myDog];
[realm commitWriteTransaction];

将对象添加到 Realm 数据库之后,您仍然可以继续使用它,并且对其进行的所有更改都会被存储(必须要在写入事务当中进行)。当写入事务提交之后,其他使用同一个 Realm 数据库的线程所做的更改都可以继续进行。

请注意,写入操作会互相阻塞,并且如果正在执行多个写入操作的话,那么还会将当前线程给阻塞掉。与其它持久化解决方案类似,我们建议您在这种情况下使用通常的最佳做法:将您的写入操作载入到专门的线程中执行。

由于 Realm 数据库的 MVCC 架构,当写入事务未提交之前,读取操作是不会被阻塞的。因此除非需要立即在多个线程中同时写入数据,否则您应该编写庞大的写入事务,而不是将写入事务拆分成多个细粒度的操作。当您向 Realm 数据库提交写入事务时,Realm 数据库当中所有的实例都将收到通知,并且会被自动更新

欲了解更多信息,请参见 RLMRealmRLMObject

嵌套属性

如果对象中存在 RLMObject 或者 RLMArray 类型的属性,那么借助嵌套数组或者嵌套字典,便可以递归地设置这些属性。只需要用表示该属性的字典或者数组将原对象给替换掉即可:

// 与其使用已存在的对象...
Person *person1 = [[Person alloc] initWithValue:@[@"Jane", @30, @[aDog, anotherDog]]];

// ...我们可以通过内联的方式来创建
Person *person2 = [[Person alloc] initWithValue:@[@"Jane", @30, @[@[@"Buster", @5],
                                                                  @[@"Buddy", @6]]]];

对于嵌套数组以及字典的任意组合而言,这个操作均是有效的。请注意, RLMArray 只能够包含 RLMObject 类型,诸如 NSString 之类的基础类型是无法包含在内的。

更新对象

Realm 提供了一系列更新对象的方法,根据使用场景的不同, 每个方法都有各自的优缺点。

直接更新

您可以在写入事务中,通过设置对象的属性从而完成更新。

// 在事务中更新对象
[realm beginWriteTransaction];
author.name = @"Thomas Pynchon";
[realm commitWriteTransaction];

键值编码

RLMObjectRLMResultRLMArray 均允许使用 键值编码(KVC)。 当您需要在运行时决定何种属性需要进行更新的时候, 这个方法就非常有用了。

批量更新对象时,为集合实现 KVC 是一个很好的做法, 这样就不用承受遍历集合时为每个项目创建访问器 所带来的性能损耗。

RLMResults<Person *> *persons = [Person allObjects];
[[RLMRealm defaultRealm] transactionWithBlock:^{
    [[persons firstObject] setValue:@YES forKeyPath:@"isFirst"];
    // 将每个 person 对象的 planet 属性设置为 "Earth"
    [persons setValue:@"Earth" forKeyPath:@"planet"];
}];

通过主键更新

如果数据模型类中包含了主键,那么 可以使用 -[RLMRealm addOrUpdateObject:],从而让 Realm 基于主键来自动更新或者添加对象。

// 创建一个 book 对象,其主键与之前存储的 book 对象相同
Book *cheeseBook = [[Book alloc] init];
cheeseBook.title = @"Cheese recipes";
cheeseBook.price = @9000;
cheeseBook.id = @1;

// 更新这个 id = 1 的 book
[realm beginWriteTransaction];
[realm addOrUpdateObject:cheeseBook];
[realm commitWriteTransaction];

如果这个主键值为 “1” 的 Book 对象已经存在于数据库当中 ,那么该对象只会进行更新。如果不存在的话, 那么一个全新的 Book 对象就会被创建出来,并被添加到数据库当中。

您可以通过传递一个子集,其中只包含打算更新的值, 从而对带有主键的对象进行部分更新:

// 假设主键为 `1` 的 "Book" 对象已经存在
[realm beginWriteTransaction];
[Book createOrUpdateInRealm:realm withValue:@{@"id": @1, @"price": @9000.0f}];
// book 对象的 `title` 属性仍旧保持不变
[realm commitWriteTransaction];

如果没有定义主键,那么最好不要对这类对象调用本节中所示的方法(也就是这些以 OrUpdate 结尾的方法)。

请注意,对于可空属性 而言, 在更新对象的时候,NSNull 仍会被视为有效值。如果您提供了一个属性值存在 NSNull 的字典,那么这个设定会被应用到应用当中,并且这些属性值也会被清空。 为了确保不会出现意外的数据丢失, 在使用此方法之前请再三确认, 只提供了想要进行更新的属性值。

删除对象

在写入事务中,将要删除的对象传递给 -[RLMRealm deleteObject:] 方法。

// cheeseBook 存储在 Realm 数据库中

// 在事务中删除对象
[realm beginWriteTransaction];
[realm deleteObject:cheeseBook];
[realm commitWriteTransaction];

您同样也可以删除存储在 Realm 数据库当中的所有数据。请注意,Realm 文件会保留在磁盘上所占用的空间,从而为以后的对象预留足够的空间,从而实现快速存储。

// 从 Realm 数据库中删除所有对象
[realm beginWriteTransaction];
[realm deleteAllObjects];
[realm commitWriteTransaction];

查询

查询将会返回一个 RLMResults 实例,其中包含了一组 RLMObject 对象。RLMResults 的接口与 NSArray 基本相同,并且可以使用索引下标来访问包含在 RLMResults 当中的对象。与 NSArray 所不同的是,RLMResults 中元素的类型是固定的,并且只能持有一个 RLMObject 子类类型。

所有的查询操作(包括检索和属性访问)在 Realm 中都是延迟加载的。只有当属性被访问时,数据才会被读取。

查询结果并不是数据的拷贝:(在写入事务中)修改查询结果会直接修改磁盘上的数据。与之类似,您可以从 RLMResults 当中的 RLMObject 来直接遍历关系图。

除非对结果进行了访问,否则查询的执行将会被推迟。这意味着 将多个临时 RLMResults 关联在一起,然后对数据进行排序和条件检索的操作, 并不会执行中间状态处理之类的额外工作。

一旦执行了查询,或者添加了通知模块, 那么 RLMResults 将时刻与 Realm 数据库当中的数据保持一致, 如有可能,会在后台线程中执行再一次查询操作。

从 Realm 数据库中检索对象的最基本方法是 +[RLMObject allObjects],这个方法将会返回 RLMObject 子类类型在默认 Realm 数据库当中的查询到的所有数据,并以 RLMResults 实例的形式返回。

RLMResults<Dog *> *dogs = [Dog allObjects]; // 从默认的 Realm 数据库中遍历所有 Dog 对象

条件查询

如果您对 NSPredicate 有所了解的话,那么您就已经掌握了在 Realm 中进行查询的方法了。RLMObjectsRLMRealmRLMArrayRLMResults 均提供了相关的方法,从而只需传递 NSPredicate 实例、断言字符串、或者断言格式化字符串来查询特定的 RLMObject 实例,这与对 NSArray 进行查询所类似。

例如,下面这个例子通过调用 [RLMObject objectsWhere:] 方法,从默认 Realm 数据库中遍历出所有棕黄色、名字以 “B” 开头的狗狗:

// 使用断言字符串来查询
RLMResults<Dog *> *tanDogs = [Dog objectsWhere:@"color = 'tan' AND name BEGINSWITH 'B'"];

// 使用 NSPredicate 来查询
NSPredicate *pred = [NSPredicate predicateWithFormat:@"color = %@ AND name BEGINSWITH %@",
                                                     @"tan", @"B"];
tanDogs = [Dog objectsWithPredicate:pred];

参见 Apple 的断言编程指南来获取更多关于构建断言的信息,此外还可以使用我们的 NSPredicate Cheatsheet。Realm 支持大多数常见的断言:

  • 比较操作数可以是属性名,也可以是常量。但至少要有一个操作数是属性名;
  • 比较操作符 ==<=<>=>!=BETWEEN 支持 intlonglong longfloatdouble 以及 NSDate 这几种属性类型,例如 age == 45
  • 比较是否相同:==!=,例如,[Employee objectsWhere:@"company == %@", company]
  • 比较操作符 ==!= 支持布尔属性;
  • 对于 NSStringNSData 属性而言,支持使用 ==!=BEGINSWITHCONTAINSENDSWITH 操作符,例如 name CONTAINS 'Ja'
  • 对于 NSString 属性而言,LIKE 操作符可以用来比较左端属性和右端表达式:?* 可用作通配符,其中 ? 可以匹配任意一个字符,* 匹配 0 个及其以上的字符。例如:value LIKE '?bc*' 可以匹配到诸如 “abcde” 和 “cbc” 之类的字符串;
  • 字符串的比较忽略大小写,例如 name CONTAINS[c] 'Ja'。请注意,只有 “A-Z” 和 “a-z” 之间的字符大小写会被忽略。[c] 修饰符可以与 [d] 修饰符结合使用;
  • 字符串的比较忽略变音符号,例如 name BEGINSWITH[d] 'e' 能够匹配到 étoile。这个修饰符可以与 [c] 修饰符结合使用。(这个修饰符只能够用于 Realm 所支持的字符串子集:参见当前的限制一节来了解详细信息。)
  • Realm 支持以下组合操作符:“AND”“OR”“NOT”,例如 name BEGINSWITH 'J' AND age >= 32
  • 包含操作符:IN,例如 name IN {'Lisa', 'Spike', 'Hachi'}
  • 空值比较:==!=,例如 [Company objectsWhere:@"ceo == nil"]。请注意,Realm 将 nil 视为一种特殊值,而不是某种缺失值;这与 SQL 不同,nil 等同于自身;
  • ANY 比较,例如 ANY student.age < 21
  • RLMArrayRLMResults 属性支持聚集表达式:@count@min@max@sum@avg,例如 [Company objectsWhere:@"[email protected] > 5"] 可用以检索所有拥有 5 名以上雇员的公司。
  • 支持子查询,不过存在以下限制:
    • @count 是唯一一个能在 SUBQUERY 表达式当中使用的操作符;
    • SUBQUERY(…)[email protected] 表达式只能与常量相比较;
    • 目前仍不支持关联子查询。

参见 [RLMObject objectsWhere:]

排序

RLMResults 允许您指定一个排序标准,然后基于关键路径、属性或者多个排序描述符来进行排序。例如,下列代码让上述示例中返回的 Dog 对象按名字进行升序排序:

// 对颜色为棕黄色、名字以 "B" 开头的狗狗进行排序
RLMResults<Dog *> *sortedDogs = [[Dog objectsWhere:@"color = 'tan' AND name BEGINSWITH 'B'"]
                                    sortedResultsUsingKeyPath:@"name" ascending:YES];

关键路径同样也可以是某个多对一关系属性。

RLMResults<Person *> *dogOwners = [Person allObjects];
RLMResults<Person *> *ownersByDogAge = [dogOwners sortedResultsUsingKeyPath:@"dog.age" ascending:YES];

请注意,sortedResultsUsingKeyPath:sortedResultsUsingProperty: 不支持 将多个属性用作排序基准,此外也无法链式排序(只有最后一个 sortedResults... 调用会被使用)。 如果要对多个属性进行排序,请使用 sortedResultsUsingDescriptors:,然后向其中输入多个 RLMSortDescriptor` 对象。

欲了解更多信息,参见:

注意,在对查询进行排序的时候,只能保证 Results 的次序不变。 出于性能考量,插入次序将无法保证。 如果您希望维护插入次序, 那么可以在这里查看解决方案。

链式查询

与传统数据库相比,Realm 查询引擎的一个独特特性就是:它能够用很小的事务开销来实现链式查询,而不是每条查询都要接二连三地分别去单独访问数据库服务器。

如果您需要获取一个棕黄色狗狗的结果集,然后在此基础上再获取名字以 ‘B’ 开头的棕黄色狗狗,那么您可以像这样将这两个查询连接起来:

RLMResults<Dog *> *tanDogs = [Dog objectsWhere:@"color = 'tan'"];
RLMResults<Dog *> *tanDogsWithBNames = [tanDogs objectsWhere:@"name BEGINSWITH 'B'"];

结果的自更新

RLMObject 实例是底层数据的动态体现,其会自动进行更新,这意味着您无需去重新检索结果。它们会直接映射出 Realm 数据库在当前线程中的状态,包括当前线程上的写入事务。唯一的例外是,在使用 for...in 枚举时,它会将刚开始遍历时满足匹配条件的所有对象给遍历完,即使在遍历过程中有对象被过滤器修改或者删除。

RLMResults<Dog *> *puppies = [Dog objectsInRealm:realm where:@"age < 2"];
puppies.count; // => 0

[realm transactionWithBlock:^{
    [Dog createInRealm:realm withValue:@{@"name": @"Fido", @"age": @1}];
}];

puppies.count; // => 1

所有的 RLMResults 对象均有此特性,无论是匹配查询出来的还是链式查询出来的。

RLMResults 属性不仅让 Realm 数据库保证高速和高效,同时还让代码更为简洁、更加灵活。例如,如果视图控制器基于查询结果来实现,那么您可以将 RLMResults 存储在属性当中,这样每次访问就不需要刷新以确保数据最新了。

您可以订阅 Realm 通知,以了解 Realm 数据何时发生了更新,比如说可以决定应用 UI 何时进行刷新,而无需重新检索 RLMResults

由于结果是自动更新的,因此不要迷信下标索引和总数会保持不变。RLMResults 不变的唯一情况是在快速枚举的时候,这样就可以在枚举过程中,对匹配条件的对象进行修改。

[realm beginWriteTransaction];
for (Person *person in [Person objectsInRealm:realm where:@"age == 10"]) {
    person.age++;
}
[realm commitWriteTransaction];

此外,还可以使用键值编码 来对 RLMResults 执行相关操作。

限制查询结果

大多数其他数据库技术都提供了从检索中对结果进行“分页”的能力(例如 SQLite 中的 “LIMIT” 关键字)。这通常是很有必要的,可以避免一次性从硬盘中读取太多的数据,或者将太多查询结果加载到内存当中。

由于 Realm 中的检索是惰性的,因此这行这种分页行为是没有必要的。因为 Realm 只会在检索到的结果被明确访问时,才会从其中加载对象。

如果由于 UI 相关或者其他代码实现相关的原因导致您需要从检索中获取一个特定的对象子集,这和获取 RLMResults 对象一样简单,只需要读出您所需要的对象即可。

// 循环读取出前 5 个 Dog 对象
// 从而限制从磁盘中读取的对象数量
RLMResults<Dog *> *dogs = [Dog allObjects];
for (NSInteger i = 0; i < 5; i++) {
    Dog *dog = dogs[i];
    // ...
}

数据迁移

当您使用任意一个数据库时,您随时都可能打算修改您的数据模型。由于 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 就会抛出错误。

注意在进行迁移的时候,默认属性值 既不适用于新的对象,也不适用于既有对象的新属性。我们认为这是一个 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) {

        // 将两个 name 合并到 fullName 当中
        newObject[@"fullName"] = [NSString stringWithFormat:@"%@ %@",
                                      oldObject[@"firstName"],
                                      oldObject[@"lastName"]];
        }];
    }
};
[RLMRealmConfiguration setDefaultConfiguration:config];

一旦迁移成功结束,Realm 文件和其中的所有对象都可被您的应用正常访问。

属性重命名

在迁移过程中对类中某个属性进行重命名操作, 比起拷贝值和保留关系来说要更为高效。

要在迁移过程中对某个属性就进行重命名的话,请确保您的新模型当中的这个属性是一个全新的名字, 它的名字不能和原有模型当中的名字重合。

如果新的属性拥有不同的可空性或者索引设置的话, 这些配置会在重命名操作期间生效。

下面是一个例子,展示了您该如何将 PersonyearsSinceBirth 属性重命名为 age 属性:

// 此段代码位于 [AppDelegate didFinishLaunchingWithOptions:]

RLMRealmConfiguration *config = [RLMRealmConfiguration defaultConfiguration];
config.schemaVersion = 1;
config.migrationBlock = ^(RLMMigration *migration, uint64_t oldSchemaVersion) {
    // 我们目前还未执行过迁移,因此 oldSchemaVersion == 0
    if (oldSchemaVersion < 1) {
        // 重命名操作必须要在 `enumerateObjects:` 调用之外进行
        [migration renamePropertyForClass:Person.className oldName:@"yearsSinceBirth" newName:@"age"];
    }
};
[RLMRealmConfiguration setDefaultConfiguration:config];

线性迁移

假如说,我们的应用有两个用户: 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 属性,但里面的内容却是 v1 的。这个看起来没什么大问题,但是假如两者的内部存储类型不同(比如说: 从 ISO email 标准格式变成了自定义格式),那麻烦就大了。为了避免这种不必要的麻烦,我们推荐您在 if (oldSchemaVersion < 3) 语句中,清空所有的 email 属性。

迁移同步

当 Realm 数据库与 Realm 对象服务器同步时,迁移过程会有所不同——在很多情况下,其实会更加简单。下面是您所需要知晓的全部内容:

  • 无需设置架构版本(尽管您可以这样做);
  • 新增内容的更改会自动进行,例如添加类或者向类中添加字段;
  • 从架构中将某个字段移除并不会从数据库中删除该字段,而是指示 Realm 忽略该字段。新的对象创建的时候仍然会使用这些属性,但是它们都将会被设置为 null。不可空的字段将被恰当地设置为零/空值:数字字段将被置为 0,字符串属性将被置为空字符串,等等。
  • 不能添加迁移模块。

假设您的应用中有一个 Dog 类:

@interface Dog : RLMObject
@property NSString *name;
@end

现在您需要添加 Person 类,并建立一个到 Dogowner 关系。除了添加类和相关属性之外,在同步之前您无需执行任何操作:

@interface Dog : RLMObject
@property NSString *name;
@property Person   *owner;
@end
RLM_ARRAY_TYPE(Dog)

@interface Person : RLMObject
@property NSString *name;
@property NSDate   *birthdate;
@end
RLM_ARRAY_TYPE(Person)

// Objecitve-C 引用类型的非可空属性
// 必须以这种形式进行声明:
@implementation Person
+ (NSArray *)requiredProperties {
    return @[@"name"];
}
@end

NSURL *syncServerURL = [NSURL URLWithString:@"http://localhost:9080/Dogs"];
RLMRealmConfiguration *config = [RLMRealmConfiguration defaultConfiguration];
config.syncConfiguration = [[RLMSyncConfiguration alloc] initWithUser:user realmURL:syncServerURL];

RLMRealm *realm = [RLMRealm realmWithConfiguration:config error:nil];

由于可同步 Realm 数据库不支持迁移模块,因此迁移当中的破坏性更改——例如主键更改、既有字段的字段类型更改(同时保留相同的名称),以及将属性从可空更改为非可空,诸如此类的操作,都需要用另外的方式来进行处理。创建一个新的具备新架构的可同步 Realm 数据库,然后将数据从旧的 Realm 数据库复制到新的 Realm 数据库:

@interface Dog : RLMObject
@property NSString *name;
@property Person *owner;
@end
RLM_ARRAY_TYPE(Dog)

@interface Person : RLMObject
@property NSString *name;
@end
RLM_ARRAY_TYPE(Person)

@implementation Person
+ (NSArray *)requiredProperties {
    return @[@"name"];
}

@interface PersonV2 : RLMObject
@property NSString *name;
@end
RLM_ARRAY_TYPE(PersonV2)

@implementation PersonV2
+ (NSArray *)requiredProperties {
    return @[];
}

NSURL *syncServerURL = [NSURL URLWithString: @"realm://localhost:9080/Dogs"];
RLMRealmConfiguration *config = [RLMRealmConfiguration defaultConfiguration];
config.syncConfiguration = [[RLMSyncConfiguration alloc] initWithUser:user realmURL:syncServerURL];
// 限制初始对象类型
config.objectClasses = @[Dog.class, Person.class];

RLMRealm *initialRealm = [RLMRealm realmWithConfiguration:config error:nil];

syncServerURL = [NSURL URLWithString: @"realm://localhost:9080/DogsV2"];
config = [RLMRealmConfiguration defaultConfiguration];
config.syncConfiguration = [[RLMSyncConfiguration alloc] initWithUser:user realmURL:syncServerURL];
// 限制新对象类型
config.objectClasses = @[Dog.class, PersonV2.class];

RLMRealm *newRealm = [RLMRealm realmWithConfiguration:config error:nil];

此外,对于可同步 Realm 数据库而言,还可以在客户端上编写一个通知处理器,或者使用 Node.js SDK 在服务器上编写一段 JavaScript 函数(如果您所使用的对象服务器版本支持的话),来执行自定义迁移。但是,如果迁移过程中出现了破坏性更改,那么 Realm 将停止与 Realm 对象服务器进行同步,并产生 Bad changeset received 错误。

通知

可以注册一个监听器,从而在 Realm 或者其实体发生变更时接收相应的通知。当整个 Realm 数据库发生变化时,就会发送 Realm 通知;如果只有个别对象被修改、添加或者删除,那么就会发送集合通知

如果有引用持有所返回的通知令牌,那么就会对其传递通知。您应当在负责监听的类中保持该令牌的强引用,因为一旦通知令牌被释放,通知也会自动取消注册。

通知只会在最初所注册的注册的线程中传递,并且该线程必须拥有一个正在运行的 Run Loop。如果您希望在主线程之外的线程中注册通知,并且该线程也没有 Run Loop 的话,那么如果您就要对该线程上的 Run Loop 进行配置和启动。

无论写入事务是在哪个线程或者进程中发生的,一旦提交了相关的写入事务,那么通知处理模块就会被异步调用。

如果某个写入事务当中包含了 Realm 的版本升级操作,那么通知处理模块很可能会被同步调用。这种情况只会在 Realm 升级到最新版本的时候发生,这时被观察的 Realm 实体会被修改或者删除,从而触发通知。这种通知会在当前写入事务的上下文中执行,这意味着在通知处理模块中再次尝试进行写入事务会导致 Realm 抛出异常。如果您的应用架构是这种方式构建的,那么可以使用 -[RLMRealm isInWriteTransaction] 来确定是否正处于写入事务当中。

由于通知的传递是通过 Run Loop 进行的,因此 Run Loop 中的其他活动可能会延迟通知的传递。如果通知无法立即发送,那么来自多个写入事务的更改可能会合并到一个通知当中。

Realm 通知

通知处理模块可以对整个 Realm 数据库进行注册。每次涉及到 Realm 的写入事务提交之后,无论写入事务发生在哪个线程还是进程中,通知处理模块都会被激活:

// 获取 Realm 通知
token = [realm addNotificationBlock:^(NSString *notification, RLMRealm * realm) {
    [myViewController updateUI];
}];

// 随后
[token invalidate];

集合通知

整个 Realm 发生了变更,那么发出的是 [Realm 通知],而如果细粒度方面的描述发生了变更,那么发出的是集合通知。细粒度方面的描述包括了自上次通知以来,所增加、移除或者修改的对象索引。集合通知是异步传递的,首先传递过来的是初始结果,然后如果有写入事务对集合中的对象作出修改(或者添加了新对象),那么还会再次触发。

可以通过传递到通知模块当中的 RLMCollectionChange 参数来访问这些变更。该对象存放了受删除 (deletions)插入 (insertions) 以及修改 (modifications) 所影响的索引信息。

对于前两个信息,也就是删除插入而言,如果有对象成为集合的一部分,或者从集合当中移除,那么就会将索引记录下来。当您向 Realm 数据库中添加对象或者从中删除对象的时候,也会触发此通知。对于 RLMResults 也同样适用,例如您执行了条件检索操作,然后有对象的值发生了变化,那么它是否还匹配该检索的通知同样也会触发。对于基于 RLMArray 以及 RLMLinkingObjects 所构建的集合来说同样适用,包括派生出来的 RLMResults,此外当关系中的对象被添加或者移除时,同样也会触发通知。

当集合当中的对象属性发生变化时,您就会收到修改通知。这对多对一关系多对多关系中发生的变更同样适用,不过双向关系就无法触发此通知了。

@interface Dog : RLMObject
@property NSString *name;
@property NSData   *picture;
@property NSInteger age;
@end

@implementation Dog
@end

RLM_ARRAY_TYPE(Dog)
@interface Person : RLMObject
@property NSString             *name;
@property RLMArray<Dog *><Dog> *dogs;
@end

@implementation Person
@end

让我们假设您正在对一组 Dog Owner 进行监听,当下述情况发生时,您会收到相匹配的 Person 对象发来的修改状态通知:

  • 修改了 Personname 属性;
  • Persondogs 属性添加或者删除了 Dog
  • 修改了属于该 Person 对象 Dogage 属性。

这使得您可以单独控制 UI 的动画和内容更新,而不是在每次通知发生的时候强行重置所有内容。

- (void)viewDidLoad {
    [super viewDidLoad];

    // 订阅 RLMResults 通知
    __weak typeof(self) weakSelf = self;
    self.notificationToken = [[Person objectsWhere:@"age > 5"] 
      addNotificationBlock:^(RLMResults<Person *> *results, RLMCollectionChange *changes, NSError *error) {
        
        if (error) {
            NSLog(@"Failed to open Realm on background worker: %@", error);
            return;
        }

        UITableView *tableView = weakSelf.tableView;
        // 初次运行查询的话,这个变化信息的值为 nil
        if (!changes) {
            [tableView reloadData];
            return;
        }

        // 检索结果发生改变,将其应用到 UITableView
        [tableView beginUpdates];
        [tableView deleteRowsAtIndexPaths:[changes deletionsInSection:0]
                         withRowAnimation:UITableViewRowAnimationAutomatic];
        [tableView insertRowsAtIndexPaths:[changes insertionsInSection:0]
                         withRowAnimation:UITableViewRowAnimationAutomatic];
        [tableView reloadRowsAtIndexPaths:[changes modificationsInSection:0]
                         withRowAnimation:UITableViewRowAnimationAutomatic];
        [tableView endUpdates];
    }];
}

- (void)dealloc {
    [self.notificationToken invalidate];
}

对象通知

Realm 支持对象级别的通知。您可以在特定的 Realm 对象上进行通知的注册,这样就可以在此对象被删除时、或者该对象所管理的属性值被修改时(同样也适用于当对象所管理的属性值被设置为既有值的时候),获取相应的通知。

只有由 Realm 数据库所管理的对象才能够进行通知注册。

对于在不同线程或者不同进程当中所值行的写操作事务而言,当管理对象的 Realm 数据库(自动)更新到新版本时,通知处理闭包将会被调用。对于本地的写操作事务而言,将在事务提交之后的某个时间被调用。

通知处理闭包拥有三个参数。第一个参数 deletedBOOL 类型,表示该对象是否已被删除。如果返回的值是 YES,那么其他参数将为 nil,并且此闭包将不会被再次调用。

第二个参数 changes 是一个包含 RLMPropertyChange 对象的 NSArray 类型。这个对象包含有被修改的属性名(以字符串的形式提供)、修改前的属性值以及当前的属性值。

第三个参数则是 NSError。如果有涉及到该对象的错误发生,那么 NSError 就会包含相关的错误信息,且 changes 参数将为空,deleted 的值将为 NO,且这个闭包将不会被再次调用。

@interface RLMStepCounter : RLMObject
@property NSInteger steps;
@end

@implementation RLMStepCounter
@end

RLMStepCounter *counter = [[RLMStepCounter alloc] init];
counter.steps = 0;
RLMRealm *realm = [RLMRealm defaultRealm];
[realm beginWriteTransaction];
[realm addObject:counter];
[realm commitWriteTransaction];

__block RLMNotificationToken *token = [counter addNotificationBlock:^(BOOL deleted,
                                                                      NSArray<RLMPropertyChange *> *changes,
                                                                      NSError *error) {
    if (deleted) {
        NSLog(@"The object was deleted.");
    } else if (error) {
        NSLog(@"An error occurred: %@", error);
    } else {
        for (RLMPropertyChange *property in changes) {
            if ([property.name isEqualToString:@"steps"] && [property.value integerValue] > 1000) {
                NSLog(@"Congratulations, you've exceeded 1000 steps.");
                [token invalidate];
                token = nil;
            }
        }

    }
}];

界面驱动更新

Realm 的通知总是以异步的方式进行传递,因此这些操作永远不会阻塞主 UI 线程,也不会导致应用卡顿。然而,当变更切实需要在主线程进行同步传递,并能够立即反映在 UI 的时候,我们也提供了相应的解决方案。我们将这些事务称之为界面驱动更新。

例如,假设用户需要向表视图当中插入一个项目。理想状况下,UI 将会将这个操作用动画表现出来,然后当用户完成动作的时候立即启动相应的操作。

然而,此插入操作的 Realm 变更通知将会延时一段时间才进行传递,它会先将一个对象添加到表视图背后所关联的集合当中,随后再尝试去在 UI 当中插入一个新的项目。这种双重插入将会导致 UI 与后台数据之间的数据不一致,而这往往会导致应用崩溃!

在执行界面驱动更新的时候,将通知模块的的通知令牌传递给 -[RLMRealm commitWriteTransactionWithoutNotifying:error:]Realm.commitWrite(withoutNotifying:),这个通知模块不应该再次对变更操作作出回应。

这项特性是非常有用的,尤其是在需要大量同步的 Realm 当中使用细粒化集合通知的时候,因为界面驱动更新依赖于控制应用何时该执行变更的完整状态。对于同步型 Realm 而言,无论 Realm 是否进行了同步,变更都能够成功应用,而这种变更往往会在应用生命周期当中的任何时候发生。

// 监听 RLMResults 通知
__weak typeof(self) weakSelf = self;
self.notificationToken = [self.collection addNotificationBlock:^(RLMResults<Item *> *results, RLMCollectionChange *changes, NSError *error) {
    if (error) {
        NSLog(@"Failed to open Realm on background worker: %@", error);
        return;
    }

    UITableView *tableView = weakSelf.tableView;
    // 初次运行查询的话,这个变化信息的值为 nil
    if (!changes) {
        [tableView reloadData];
        return;
    }

    // 检索结果发生改变,将其应用到 UITableView
    [tableView beginUpdates];
    [tableView deleteRowsAtIndexPaths:[changes deletionsInSection:0]
                     withRowAnimation:UITableViewRowAnimationAutomatic];
    [tableView insertRowsAtIndexPaths:[changes insertionsInSection:0]
                     withRowAnimation:UITableViewRowAnimationAutomatic];
    [tableView reloadRowsAtIndexPaths:[changes modificationsInSection:0]
                     withRowAnimation:UITableViewRowAnimationAutomatic];
    [tableView endUpdates];
}];

- (void)insertItem {
    // 在主线程执行界面驱动更新:
    [self.collection.realm beginWriteTransaction];
    [self.collection insertObject:[Item new] atIndex:0];
    // 随后立即将其同步到 UI 当中
    [tableView insertRowsAtIndexPaths:[NSIndexPath indexPathForRow:0 inSection:0]
                     withRowAnimation:UITableViewRowAnimationAutomatic];
    // 确保变更通知不会再次响应变更
    [self.collection.realm commitWriteTransactionWithoutNotifying:@[token]];
}

键值观察

Realm 对象的大多数属性都遵从键值观察机制。所有 RLMObject 子类的持久化存储(未被忽略)的属性都是遵循 KVO 机制的,并且 RLMObject 以及 RLMArray 中的无效属性也同样遵循(然而 RLMLinkingObjects 属性并不能使用 KVO 进行观察)。

观察 RLMObject 子类的某个未管理实例的属性的方法就如同观察其他 NSObject 子类 一样,不过要注意的是,当观察者存在的时候,您不能够使用诸如 [realm addObject:obj] 此类的方法向 Realm 数据库中添加对象。

观察已管理对象(也就是那些已经被添加到 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机制的简要例子。

加密

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.

Realm 支持在创建 Realm 数据库时采用64位的密钥对数据库文件进行 AES-256+SHA2 加密。

// 生成随机秘钥
NSMutableData *key = [NSMutableData dataWithLength:64];
(void)SecRandomCopyBytes(kSecRandomDefault, key.length, (uint8_t *)key.mutableBytes);

// 打开已加密的 Realm 文件
RLMRealmConfiguration *config = [RLMRealmConfiguration defaultConfiguration];
config.encryptionKey = key;
NSError *error = nil;
RLMRealm *realm = [RLMRealm realmWithConfiguration:config error:&error];
if (!realm) {
    // 如果秘钥错误,`error` 会提示数据库无法访问
    NSLog(@"Error opening realm: %@", error);
}

// 照常使用 Realm 数据库
RLMResults<Dog *> *dogs = [Dog objectsInRealm:realm where:@"name contains 'Fido'"];

这样硬盘上的数据都能都采用AES-256来进行加密和解密,并用 SHA-2 HMAC 来进行验证。 每次您要获取一个 Realm 实例时,您都需要提供一次相同的密钥。

我们的加密例程应用 展示了如何产生密钥并将其安全地存放到钥匙串当中,然后用其加密 Realm。

加密过的 Realm 只会带来很少的额外资源占用(通常最多只会比平常慢10%)。

用户

Realm 对象服务器当中的核心对象便是与可同步 Realm 数据库相关联的 Realm 用户 (RLMSyncUser) 了。RLMSyncUser 可以通过用户名/密码或者通过多种第三方身份验证来完成共享 Realm 数据库的身份验证。

创建用户和用户登录需要完成两件事情:

  • 需要能连接到 Realm 对象服务器的 URL;
  • 用于进行验证的证书,能够描述用户适合使用该机制(比如说:用户名/密码、访问密钥等)。

服务器 URL

认证服务器 URL 简单地由 NSURL 所表示, 它代表了 Realm 对象服务器的地址。

NSURL *serverURL = [NSURL URLWithString:@"http://my.realmServer.com:9080"];

欲了解关于服务器 URL 的更多内容,请参阅认证文档

认证

认证用于建立用户的合法身份,并完成登录操作。请参阅我们的 Realm 对象服务器认证文档,来了解 Realm 移动端平台所支持的认证方式。

某个给定用户的证书信息表示的是 RLMSyncCredentials 的值,可以通过以下几种方式来构建:

  • 提供有效的用户名/密码组合
  • 提供从受支持的第三方身份验证服务那里所获取的令牌
  • 提供令牌以及自定义的身份验证提供器(参见自定义身份认证

用户名和密码认证是完全由 Realm 对象服务器所管理的,这样可以让您完全控制应用中的用户管理操作。对于其他身份验证方法而言,您的应用将负责登录外部服务,并获取身份验证令牌。

以下是一些不同的认证方式的配置,通过配置证书来实现。

用户名/密码

RLMSyncCredentials *usernameCredentials = [RLMSyncCredentials credentialsWithUsername:@"username"
                                                                             password:@"password"
                                                                             register:NO];

注意到这个这工厂方法要获取一个布尔值参数,从而决定是否允许此新用户完成注册,或者是否允许此用户登录。如果新用户所注册的用户名与现有用户发生冲突的话,那么就会报错,如果用户登录的用户名不存在的话,那么也会记录下来。

Google

RLMSyncCredentials *googleCredentials = [RLMSyncCredentials credentialsWithGoogleToken:@"Google token"];

Facebook

RLMSyncCredentials *facebookCredentials = [RLMSyncCredentials credentialsWithFacebookToken:@"Facebook token"];

Apple CloudKit

RLMSyncCredentials *cloudKitCredentials = [RLMSyncCredentials credentialsWithCloudKitToken:@"CloudKit token"];

自定义身份认证

Realm 对象服务器支持使用第三方身份认证提供器。这允许用户能够使用遗留数据库或者 API 进行身份验证,也支持集成 Realm 对象服务器目前不支持的提供器。关于如何编写自定义身份验证提供器的相关信息,请参阅对象服务器手册的这一部分

RLMSyncCredentials *credentials = [[RLMSyncCredentials alloc] initWithCustomToken:@"custom token" provider:@"myauth" userInfo:nil];

附加信息可以通过第三个参数传递给自定义证书构造器当中。 关于这方面的详细信息,请参阅 API reference

登录

要创建用户的话,调用 +[RLMSyncUser logIn] 即可。这个工厂方法将会构造一个用户,然后通过异步操作登录到 Realm 对象服务器上,如果登录成功,那么就通过一个回调闭包提供一个可用的用户对象,以供后续使用。如果登录失败的话,那么回调闭包当中传递的就将是一个错误对象。

[RLMSyncUser logInWithCredentials:usernameCredentials
                    authServerURL:serverURL
                     onCompletion:^(RLMSyncUser *user, NSError *error) {
    if (user) {
        // can now open a synchronized RLMRealm with this user
    } else if (error) {
        // handle error
    }
}];

Realm 移动端平台允许应用同时存在多个独立用户。

试想,我们可能需要实现一个支持多账户的 e-mail 客户端应用。这样的话多个用户可以在任意指定的时间完成认证,如果要获取当前可用的所有活跃用户的话,那么可以使用 +[RLMSyncUser all] 来获取。

管理用户

currentUser 方法用于检索当前用户,也就是最后登录并且证书尚未过期的那个用户。如果找不到当前用户的话,这个方法就会返回 nil。如果存在多个已登录用户的话,将会抛出异常。

RLMSyncUser *user = [RLMSyncUser currentUser];

如果存在多个用户登录的情况,您可以使用 allUsers 来获取对应的用户对象字典。

NSDictionary<NSString *, RLMSyncUser *> *allUsers = [RLMSyncUser allUsers];

如果没有用户登录的话,返回的字典内容将为空。

对于使用内置 Realm 对象服务器用户名/密码认证类型进行身份验证的用户,可以通过调用 -[RLMSyncUser changePassword:completion:] API 来更改他们自己的密码。

这个 API 会发送一个异步请求给服务器。一旦收到服务器回应,完成代码块就会被调用,如果操作失败,那么就会返回一个错误对象,如果操作成功,则会返回 nil。

NSString *newPassword = @"swordfish";
[user changePassword:newPassword
          completion:^(NSError *error) {
    if (error) {
        // 出错
    }
    // 否则的话,密码将得以成功修改
}];

注销

如果用户想要退出他们的账户,那么您可以调用 -[RLMSyncUser logOut] 方法。所有未决的本地更改会继续与对象服务进行完全同步。随后,所有的本地已同步数据都将从设备上清除掉。

管理员用户

管理员用户是 Realm 对象服务器中定义的用户,具备对 ROS 实例上所有的 Realm 数据库的访问、管理权限。要知道用户是否是管理员用户,可以查看 RLMSyncUser.isAdmin 属性,它也能反映出用户上次成功登录时的状态。

[RLMSyncUser logInWithCredentials:usernameCredentials
                    authServerURL:serverURL
                     onCompletion:^(RLMSyncUser *user, NSError *error) {
    if (user) {
        // 现在可以使用此用户来打开可同步 Realm 数据库
        // 如果该用户是 Realm 对象服务器实例的管理员,那么则会返回 YES
        NSLog(@"User is admin: %@", @(user.isAdmin));
    } else if (error) {
        // 处理错误
    }
}];

访问控制

Realm 移动端平台提供了灵活的访问控制机制,从而可以指定哪些用户可以操作指定的可同步 Realm 数据库。这个功能可以用于多人协作类的应用当中,其中有多个用户同时向同一个 Realm 写入数据。此外也可以用于在发布者/订阅者场景当中的数据共享,其中某个用户拥有写入权限,而其他用户只拥有读取权限。

对于指定 Realm 数据库而言,有三种访问控制级别:

  • mayRead 表示允许用户从 Realm 中读取数据;
  • mayWrite 表示允许用户向 Realm 中写入数据;
  • mayManage 表示允许用户修改 Realm 架构。

虽然这三个标识是以布尔值的形式实现的,但实际上,每个级别都包含之前的级别权限:mayWrite 包含 mayReadmayManage 包含全部三种权限。(只读 Realm 数据库必须异步打开。)

除非明确指明权限,否则只有 Realm 数据库的所有者才能够访问。唯一的例外是管理员用户:他们将拥有服务器上所有 Realm 数据库的所有权限。

目前不支持只写权限(即不带 mayRead 权限的 mayWrite)。

欲了解更多关于访问控制方面的内容,请参见 Realm 对象服务器文档中的访问控制一节。

读取权限

如果要获取某个用户所有可以访问的 Realm 数据库,以及每个 Realm 数据库对应的访问级别,可以使用 -[RLMSyncUser retrievePermissionsWithCallback:] 方法。

[[RLMSyncUser currentUser] retrievePermissionsWithCallback:^(RLMResults<RLMSyncPermission *> *permissions, NSError *error) {
    if (error) {
        // 处理错误
        return;
    }
    // 成功!访问准许
}];

修改权限

要修改 Realm 文件的访问控制设置的话,可以通过以下两种方法来执行:应用/撤销权限配置提供/回应对象

授予权限

可以向其他用户应用(即授予)权限配置,从而直接增加或减少他们对 Realm 数据库的访问。

RLMSyncPermission *permission = [[RLMSyncPermission alloc] initWithRealmPath:realmPath                 // The remote Realm path on which to apply the changes
                                                                    identity:anotherUserID             // The user ID for which these permission changes should be applied
                                                                 accessLevel:RLMSyncAccessLevelWrite]; // The access level to be granted
[user applyPermission:permission callback:^(NSError *error) {
    if (error) {
        // 处理错误
        return;
    }
    // 成功应用权限
}];

如果要对用户所管理的全部 Realm 数据库执行权限更改的话,请将 realmPath 的值指定为 *。如果要对对象服务器授权的所有用户执行权限更改,请将 userID 的值指定为 *

除了根据用户在 Realm 对象服务器上的标识来授予权限外,还可以根据其在 Realm 对象服务器上的用户名来授予权限:

RLMSyncPermission *permission = [[RLMSyncPermission alloc] initWithRealmPath:realmPath
                                                                    username:@"[email protected]"
                                                                 accessLevel:RLMSyncAccessLevelWrite];
[user applyPermission:permission callback:^(NSError *error) {
    // ...
}];

撤销权限

如果要撤销权限,那么可以在授予权限的时候,将访问级别设置为 RLMSyncAccessLevelNone,也可以将带有任意权限的权限设置传递给 -[RLMSyncUser revokePermission:callback:] 方法。

权限邀请

权限邀请可以在用户之间共享 Realm 访问权限。您无需编写任何的服务端代码;权限邀请是完全通过客户端 API 所创建和处理的。

如果您想要将所管理的 Realm 数据库相关权限分享出去,那么可以创建一个权限邀请,即调用 -[RLMSyncUser createOfferForRealmAtURL:accessLevel:expiration:callback:] 方法。 这个方法将会异步创建一个权限邀请;一旦操作成功完成,那么回调模块就会被调用, 并返回一条表示权限邀请的字符串令牌。

权限邀请指定了所访问的 Realm 数据库 URL 地址,以及向接收者授予什么级别的访问权限,此外还要设置该权限邀请何时到期, 过期之后就无法重新激活权限。(已经接受邀请的用户不会失去对该 Realm 数据库的访问权限)。如果将日期设置为 nil,那么该邀请就永远不会过期。

NSURL *realmURL = [NSURL URLWithString:@"realm://realm.example.org/~/recipes"];

// 为 `offeringUser` 自己的 `recipes` Realm 数据库实例建立可读可写权限邀请
[offeringUser createOfferForRealmAtURL:realmURL
                           accessLevel:RLMSyncAccessLevelWrite
                            expiration:nil
                              callback:^(NSString *token, NSError *error) {
    if (error) {
        NSLog(@"Not able to create a permission offer! Error was: %@", error);
        return;
    }
    // (`token` 现在可以传递到闭包外部,以传递给别的用户...)
}];

随后就可以通过合适的渠道(比如说电子邮件)将这串字符串令牌传递给其他用户,然后使用 -[RLMSyncUser acceptOfferForToken:callback:] 方法来接受邀请。 这个方法也是异步执行的;回调也会被调用,并返回所邀请的 Realm 数据库 URL 地址。对于接收者来说,这个 URL 可用来 打开 Realm 数据库。

NSString *token;
// (get token...)

[receivingUser acceptOfferForToken:token callback:^(NSURL *realmURL, NSError *error) {
    if (error) {
        NSLog(@"Not able to accept a permission offer! Error was: %@", error);
        return;    
    }
    // 我们现在可以使用 `realmURL` 来打开 Realm 数据库。
    // (记住如果我们只有 .read 权限,那么必须使用 `asyncOpen` API。)
    RLMSyncConfiguration *syncConfig = [[RLMSyncConfiguration alloc] initWithUser:receivingUser realmURL:realmURL];
    RLMRealmConfiguration *config = [RLMRealmConfiguration defaultConfiguration];
    config.syncConfiguration = syncConfig;
    RLMRealm *realm = [RLMRealm realmWithConfiguration:config error:&error];
    // ...
}];

请注意,用户的设备必须要连接到 Realm 对象服务器,才能够创建和接受权限邀请。

权限邀请所授予的权限是累积的:如果用户具备某个 Realm 数据库的写入权限,并且接受了该 Realm 数据库的读取权限邀请,那么 该用户不会失去现有的写入权限。

操作可同步 Realm 数据库

同步会话

与 Realm 对象数据库的已同步 Realm 连接由 RLMSyncSession 对象所表示。会话对象表示由某个特定用户所打开的 Realm 数据库,并且可以通过使用 RLMSyncUser 用户对象的 +[RLMSyncUser allSessions] 或者 -[RLMSyncUser sessionForURL:] API 来获取得到。

基础操作

底层会话的状态可以通过 state 属性检索得到,这样便可以检查会话是处于激活状态、离线状态还是异常状态。如果会话处于激活状态的话,那么 configuration 属性当中将包含有 RLMSyncConfiguration 值,可用于打开 Realm 数据库的另一个实例(比如说在不同的线程上执行此操作)。

进度通知

会话对象允许您的应用监视向 Realm 对象服务器上传或者下载的会话状态,只需要在会话对象上注册进度通知闭包即可。

最初注册线程 Runloop 上的同步子系统将会定期调用进度通知闭包。如果没有 Runloop 存在,那么就会自行创建一个。实际上,这意味着您可以在这里 GCD 的后台队列中注册这些闭包,并且这些闭包也可以正常工作。可以在会话对象上同时注册多个所需的闭包。闭包可以配置为报告上传进度,也可以配置为报告下载进度。

每次当闭包被调用之后,它就会接收到当钱已经传输的字节数,以及可传输字节的总量(其定义为:已传输的字节数 + 等待传输的字节数)。

当闭包注册之后,注册方法便会返回一个令牌对象 (token)。在令牌上调用 -invalidate 方法可以停止对此闭包的监听和通知。如果该闭包已经被注销掉,那么调用 invalidate 方法将不会做任何事情。注意,注册方法很有可能会返回空的令牌,特别是当通知闭包永远不会再次运行的时候(例如,会话处于致命错误状态,或者进度已经完成或无法完成)。

还存在两种类型的闭包。首先,闭包可以配置为无限期通知。这些闭包将永远保持激活状态,除非用户明确声明停止这些闭包的活动,此外这些闭包将始终通知最新的可传输字节数。这类闭包可以用于控制网络指示器之类的 UI,例如在上传或者下载操作正在进行时所进行的 UI 颜色变化和 UI 出现等操作。

RLMSyncSession *session = [[RLMSyncUser currentUser] sessionForURL:realmURL];
void(^workBlock)(NSUInteger, NSUInteger) = ^(NSUInteger downloaded, NSUInteger downloadable) {
    if ((double)downloaded/(double)downloadable >= 1) {
        [viewController hideActivityIndicator];
    } else {
        [viewController showActivityIndicator];
    }
};
RLMProgressNotificationToken *token;
token = [session addProgressNotificationForDirection:RLMSyncProgressDirectionDownload
                                                mode:RLMSyncProgressModeReportIndefinitely
                                               block:workBlock];

// 完成操作后...
[token invalidate];

闭包也可以被配置为为当前未完成的工作而报告进度。这些闭包在注册之后,捕获当前已传输字节,并会还会汇报相应的进度百分比。一旦已传输字节数达到或者超过所设定的初始值,那么闭包将会自动注销自身。这种类型的闭包可以用于追踪下载进度的进度条,比如说当用户登录时下载可同步 Realm 数据库时的操作,这样就可以让用户知道本地副本更新需要耗费多少时间。

RLMSyncSession *session = [[RLMSyncUser currentUser] sessionForURL:realmURL];
RLMProgressNotificationToken *token;
void(^workBlock)(NSUInteger, NSUInteger) = ^(NSUInteger uploaded, NSUInteger uploadable) {
    double progress = ((double)uploaded/(double)uploadable);
    progress = progress > 1 ? 1 : progress;
    [viewController updateProgressBarWithFraction:progress];
    if (progress == 1) {
        [viewController hideProgressBar];
        [token invalidate];
    }
};
token = [session addProgressNotificationForDirection:RLMSyncProgressDirectionUpload
                                                mode:RLMSyncProgressModeForCurrentlyOutstandingWork
                                               block:workBlock];

日志

同步子系统支持多种日志记录级别,在应用开发过程当中是非常有帮助的。可以通过对 RLMSyncManager 单例上的 logLevel 属性进行设置,从而选择所需的日志记录详尽度:Realm 支持多种日志级别,我们可以通过同步管理器来进行配置选择,如下所示:

[[RLMSyncManager sharedManager] setLogLevel:RLMSyncLogLevelOff];

在任何可同步 Realm 数据库被打开之前,就必须设置日志级别。在首次可同步 Realm 数据库打开之后,对日志级别的修改将不起任何作用。

关于日志级别的更多信息,请参阅 Realm 对象服务器配置文档

错误报告

某些与同步相关的 API 在执行异步操作时可能会发生失败的情况。这些 API 将会包含有一个 completion 闭包,从而可以接受错误参数;如果错误参数被传递进去则说明操作失败。这样便可以通过检查错误以获取更详细的信息。

我们同时强烈建议RLMSyncManager 单例配置一个错误处理器。涉及全局同步子系统的错误或者某些特定的会话(表示所打开的用于与服务器同步的 Realm 会话)将会通过此错误处理器进行报告。当错误发生之后,错误处理器就会被调用,因此便可以通过表示错误的对象来进行处理,此外还有表示发生错误会话的 RLMSyncSession 对象(如果适用的话)。

[[RLMSyncManager sharedManager] setErrorHandler:^(NSError *error, RLMSyncSession *session) {
    // 处理错误
}];

Realm 移动端平台的错误由 NSError 对象表示,其 domain 为 RLMSyncErrorDomain。 请参考 RLMSyncError 以及 RLMSyncAuthError 的定义来查找错误代码以及其相关的含义。

客户端重置

如果 Realm 对象服务器发生了崩溃并且需要进行备份恢复的话,那么应用可能需要对给定的可同步 Realm 数据库实施客户端重置操作。 当所操作的 Realm 本地版本号比服务器上的 Realm 版本号大的时候, 那么就需要执行这个操作(例如,当 Realm 对象服务器备份之后应用执行了变更操作, 但是这些操作并没有在服务器崩溃并恢复之前同步回服务器上)。

客户端重置的步骤如下所示:创建本地 Realm 文件的备份拷贝,然后将本地的 Realm 文件删除。 接下来应用连接到 Realm 对象服务器并打开服务器上的 Realm 数据库,从而下载最新的数据库拷贝。 而在 Realm 对象服务器备份完成之后、但还未同步回服务器的数据将保留在 Realm 的备份拷贝当中, 但是当 Realm 重新下载过程当中所操作的数据将不会被保留。

是否需要进行客户端重置,将由发送给 RLMSyncManager 错误处理器中的错误所标示出来。 这个错误将由 RLMSyncErrorClientResetError 所表示。

错误对象当中还包含有两个值:客户端重置操作执行时所备份的 Realm 文件位置,以及一个令牌对象, 它能够传递给 +[RLMSyncSession immediatelyHandleError:] 方法 来手动启动客户端重置过程。

如果手动启动客户端重置过程的话,所有的 Realm 实例都必须先无效化并销毁。 请注意,RLMRealm 可能不会完全无效化,即使所有该对象的引用都会 nil,除非包含该对象的自动释放池被释放掉。 但是,这样做将会导致 Realm 在客户端重置进程完成之后被重新打开,从而使同步恢复执行。

如果没有手动启动客户端重置过程的话,那么该操作会在下一次应用启动之后自动执行,此外在第一次访问 RLMSyncManager 单例的时候也会调用。 这时候需要应用来保存备份副本的位置,以便稍后可以找到备份副本。

需要重置的 Realm 仍然可以进行读写操作,但是所有的变更都不会同步到服务器上,除非客户端重置操作已经完成并且 Realm 文件也重新下载完毕。 这一点非常重要,您的应用需要监听客户端重置所产生的错误,为了以防万一,当客户端重置触发之后,最少还需要保存所创建、修改的用户数据, 这样才能够在 Realm 的副本重新下载完毕之后将这些保存掉的数据重新写回到数据库当中。

备份副本的文件路径可以通过调用 NSError 对象的 -[NSError rlmSync_clientResetBackedUpRealmPath] 来获取得到。 此外也可以直接从 userInfo 字典中通过 kRLMSyncPathOfRealmBackupCopyKey 键来提取得到。

重置初始化令牌可以通过调用 NSError-[NSError rlmSync_errorActionToken] 来获取得到。 此外也可以通过 kRLMSyncErrorActionTokenKey 键,直接从 userInfo 字典中提取得到。

下例展示了该如何使用客户端重置 API 来实现客户端重置操作:

[[RLMSyncManager sharedManager] setErrorHandler:^(NSError *error, RLMSyncSession *session) {
    if (error.code == RLMSyncErrorClientResetError) {
        [realmManager closeRealmSafely];
        [realmManager saveBackupRealmPath:[error rlmSync_clientResetBackedUpRealmPath]];
        [RLMSyncSession immediatelyHandleError:[error rlmSync_errorActionToken]];
        return;
    }
    // 处理其他错误...
}];

关于 Realm 对象服务器是如何处理客户端重置的更多信息,请参阅我们的服务器文档

无权限错误

如果有用户尝试操作没有权限的 Realm 数据库,那么就会报出“权限拒绝”错误。例如,当用户只拥有只读权限并尝试向 Realm 数据库写入数据, 或者试图打开没有权限访问的 Realm 数据库,那么就会产生此类错误。

最重要的是要注意:如果用户只具备某个特定可同步 Realm 数据库的只读权限,那么必须要使用 +[RLMRealm asyncOpenWithConfiguration:callbackQueue:callback:] API 来异步打开 Realm 数据库。 否则的话会导致服务器返回无权限错误。

无权限错误由 RLMSyncErrorPermissionDeniedError 所表示。 该错误对象将包含一个令牌,可用于删除无权限的 Realm 文件。 可以通过对该 NSError 对象调用 -[NSError rlmSync_errorActionToken] 方法来获取此令牌。此外,还可以 从 userInfo 字典中,通过 kRLMSyncErrorActionTokenKey 键来直接获取。

无论什么情况,无权限错误均表示用户设备上的 Realm 数据库本地副本与服务器出现了冲突,无法进行同步。因此,该文件将在下次应用启动时自动删除。 此外,还可以将包含在错误对象内部的令牌传递给 +[RLMSyncSession immediatelyHandleError:] 方法, 从而立即删除 Realm 文件。

如果使用了该令牌删除了 Realm 文件,所有目标 Realm 数据库当中的实例必须先无效化并销毁。注意,RLMRealm 可能无法完全无效化, 即便所有的引用都被置为 nil 也是如此,除非耗尽了包含它的自动释放池的资源。然而,这种做法可以立即用正确的方式重新打开 Realm 数据库, 或者在进行正确的配置之后,允许与服务器进行同步以恢复数据。

部分同步

部分同步允许在打开可同步 Realm 数据库的时候,无需从服务器的远程 Realm 数据库中下载全部对象。 此外,部分同步的 Realm 数据库允许您使用查询, 来指定希望与本地副本进行同步的对象子集。

部分同步目前正处于技术预览阶段,与之相关的 API 可能会在将来版本的 Realm 数据库中发生变化。

要在部分同步模式下打开 Realm 数据库,只需在打开 Realm 数据库之前, 在 RLMSyncConfiguration 中设置 isPartial 属性即可。

部分同步 Realm 数据库在第一次创建和打开的时候,是不会包含任何对象的。要为 Realm 指定所需要检索的对象, 请调用 -[RLMRealm subscribeToObjects:where:callback:] 方法。 首先将希望检索的对象类型传递进去,然后是一串包含有效检索的字符串,最后在回调中获取所返回的数据。

回调最多只会被调用一次。如果获取的结果有问题(例如查询字符串是无效的),那么闭包当中将会返回一个错误,描述具体的错误原因。 如果成功的话,就会返回一个 RLMResults 对象,其中包含了所有匹配该检索的对象。 当远程 Realm 数据库当中有项目被添加、修改或者删除时,这个结果集合也会随之自动更新。 如果您希望观察更多的变更内容,那么请为之添加集合通知

您可以修改结果集合当中的对象,并将这些更改同步到 Realm 对象服务器。 请注意,对于冲突的解决,其与完全同步的 Realm 副本有所不同。

将 Realm 对象服务器 1.* 迁移至 2.0

如果您正在使用 Realm 对象服务器 2.0 之前的版本,并在将应用升级到 Realm 对象服务器 2.0 之后, 就必须在应用当中编写相关代码,从而处理本地可同步 Realm 数据库副本的迁移。 Realm 对象服务器 2.0 的文件格式与之前版本的 Realm 对象服务器不兼容,并且所有从旧版本的 Realm 对象服务器同步的 Realm 数据库都必须重新下载。

在打开需要进行迁移的可同步 Realm 数据库时,Realm 文件会进行备份,随后会被删除,从而能够从 Realm 对象服务器重新下载。 之后便会传入一个错误对象。

为了处理迁移,使用 +[RLMRealm realmWithConfiguration:error:] 来打开 Realm 数据库,并检查所返回的错误。 迁移错误将会由 RLMErrorIncompatibleSyncedFile 错误码来表示。该错误的 userInfo 字典的 RLMBackupRealmConfigurationErrorKey 键中包含了一个 RLMRealmConfiguration 对象,从而允许打开所备份的 Realm 数据库拷贝。随后,您可以编写代码来检查备份的 Realm 数据库拷贝, 将没有与服务器进行同步的相关数据手动写回到重新下载的 Realm 数据库当中。

请注意,通过备份配置打开的所有 Realm 数据库将视为“离线型” Realm 数据库;不会进行任何同步操作。

NSError *error = nil;
RLMRealm *realm = [RLMRealm realmWithConfiguration:configuration error:&error];
if (!realm && error.code == RLMErrorIncompatibleSyncedFile) {
    RLMRealmConfiguration *backupConfiguration = error.userInfo[RLMBackupRealmConfigurationErrorKey];
    RLMRealm *backupRealm = [RLMRealm realmWithConfiguration:backupConfiguration error:&error];
    // (...)
}
// 一旦打开过备份 Realm 数据库,我们可以尝试重新打开 Realm 数据库
realm = [RLMRealm realmWithConfiguration:configuration error:&error];

本地 Realm 数据库转换为可同步 Realm 数据库

目前,还不支持将本地(不可同步)的 Realm 数据库自动转换为可同步 Realm 数据库。我们计划在未来添加对此项功能的支持。

如果您希望将本地 Realm 数据库转换为可同步 Realm 数据库的话,您需要建立一个新的可同步 Realm 数据库,然后手动将本地 Realm 数据库当中的对象完全复制到可同步 Realm 数据库当中。

冲突处理

关于冲突处理的更多内容,请参阅Realm 对象服务器文档

管理员 API

我们提供了一系列 API,只有管理员用户(即设置了 isAdmin 标识的用户)才能够调用。

修改密码

管理员通过调用 -[RLMSyncUser changePassword:forUserID:completion:] API 来更改所有用户的密码。只需要将需要更改密码的 Realm 对象服务器用户标识传递进去即可。

检索用户信息

管理员通过调用 -[RLMSyncUser retrieveInfoForUser:identityProvider:completion:] API 来检索 Realm 对象服务器当中任何用户的信息。

第一个参数是一个字符串,表示用户标识,是身份提供服务发送给用户的。例如,如果用户使用 Realm 对象服务器的用户名/密码功能进行注册,那么这个字符串就是他的用户名。这不应该与 Realm 对象服务器发给用户的内部标识相混淆。第二个参数则是提供用户注册的服务标识。

结果将通过完成代码块异步返回。如果检索失败(比如说该用户不存在或者用户不是 Realm 对象服务器的管理员),那么将会返回一个错误,如果检索成功,那么就会返回一个包含用户信息的 RLMSyncUserInfo 对象。

NSString *targetUserIdentity = @"[email protected]";
[adminUser retrieveInfoForUser:targetUserIdentity
              identityProvider:RLMIdentityProviderUsernamePassword
                    completion:^(RLMSyncUserInfo *userInfo, NSError *error) {
    if (error) {
        // 发生了错误
    }
    // 通过 userInfo 获取用户信息...
}];

线程

Realm 读取事务的生命周期与 RLMRealm 实例的生命周期相关联。避免使用自动刷新 Realm 数据库,以及在显式自动释放池中从后台进程封装所有的 Realm API 使用,以达成“固定”旧有 Realm 事务的目的。

关于此效应的更多详细信息,请参阅我们当前的限制

在单个线程中,你只需要将所有东西看做是普通的对象即可,无需考虑并行或者多线程处理的问题。线程锁定、资源协调访问都是不需要的(即时它们同时被其他线程所修改),唯一的修改操作就是包含在写事务中的操作。

Realm 通过确保每个线程始终拥有 Realm 的一个快照,以便让并发运行变得十分轻松。你可以同时有任意数目的线程访问同一个 Realm 文件,并且由于每个线程都有对应的快照,因此线程之间绝不会产生影响。

您唯一需要注意的一件事情就是不能让多个线程都持有同一个 Realm 对象的实例。如果多个线程需要访问同一个对象,那么它们分别会获取自己所需要的实例(否则在一个线程上发生的更改就会造成其他线程得到不完整或者不一致的数据)。

检视其他线程上的变化

在主 UI 线程中(或者任何一个位于 runloop 中的线程),对象会在 runloop 的每次循环过程中自行获取其他线程造成的更改。其余时候您只能够对快照进行操作,因此单个方法中得到的数据将始终不变,无需担心其他线程会对其造成影响。

当您第一次打开 Realm 数据库的时候,它会根据最近成功的写事务提交操作来更新当前状态,并且在刷新之前都将一直保持在当前版本。Realm 会自每个 runloop 循环的开始自动进行刷新,除非 RLMRealmautorefresh 属性设置为 NO。如果某个线程没有 runloop 的话(通常是因为它们被放到了后台进程当中),那么 -[RLMRealm refresh] 方法必须手动调用,以确保让事务维持在最新的状态当中。

Realm 同样也会在写入事务提交(-[RLMRealm commitWriteTransaction])的时候刷新。

如果定期刷新 Realm 失败的话,就可能会导致某些事务的版本变为“锁定(pinned)”状态,阻止 Realm 重用该版本的硬盘空间,从而导致文件尺寸变大。

跨线程传递实例

RLMObject 的未管理实例(unmanaged) 表现的和正常的 NSObject 子类相同,可以安全地跨线程传递。

RLMRealm、RLMObjectRLMResults 或者 RLMArray 受管理实例皆受到线程的限制,这意味着它们只能够在被创建的线程上使用,否则就会抛出异常*。这是 Realm 强制事务版本隔离的一种方法。否则,在不同事务版本中的线程间,通过潜在泛关系图 (potentially extensive relationship graph) 来确定何时传递对象将不可能实现。

Realm 提供了一个机制,通过以下三个步骤来保证受到线程限制的实例能够安全传递:

  1. 通过受到线程限制的对象来构造一个 RLMThreadSafeReference
  2. 将此 RLMThreadSafeReference 传递给目标线程或者队列;
  3. 通过在目标 Realm 上调用 -[RLMRealm resolveThreadSafeReference:] 来解析此引用。
Person *person = [Person new];
person.name = @"Jane";
[realm transactionWithBlock:^{
    [realm addObject:person];
}];
RLMThreadSafeReference *personRef = [RLMThreadSafeReference
    referenceWithThreadConfined:person];

dispatch_async(queue, ^{
    @autoreleasepool {
        RLMRealm *realm = [RLMRealm realmWithConfiguration:realm.configuration
                                                     error:nil];
        Person *person = [realm resolveThreadSafeReference:personRef];
        if (!person) {
            return; // person 被删除
        }
        [realm transactionWithBlock:^{
            person.name = @"Jane Doe";
        }];
    }
});

RLMThreadSafeReference 对象最多只能够解析一次。如果 RLMThreadSafeReference 解析失败的话,将会导致 Realm 的原始版本被锁死,直到引用被释放为止。因此,RLMThreadSafeReference 的生命周期应该很短。

这些类型的某些属性和方法可以在任意线程中进行访问:

  • RLMRealm: 所有的属性、类方法和构造器;all properties, class methods, and initializers.
  • RLMObject: isInvalidatedobjectSchemarealm, 以及所有的类方法和构造器;
  • RLMResults: objectClassNamerealm
  • RLMArray: isInvalidatedobjectClassNamerealm

跨线程使用 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"      : randomString,
                                        @"birthdate" : randomDate}];
            }

          // 提交写入事务
          // 以确保数据在其他线程可用
          [realm commitWriteTransaction];
        }
    }
});

JSON

Realm 没有提供对 JSON 的直接支持,但是您可以使用 [NSJSONSerialization JSONObjectWithData:options:error:] 的输出,实现将 JSON 添加到 RLMObject 的操作。由此所产生的 KVC 兼容对象可以使用创建和更新对象的 标准 API 来添加/更新 RLMObject

// 表示城市的一个 Realm 对象
@interface City : RLMObject
@property NSString *name;
@property NSInteger cityId;
// 其余的属性...
@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 中插入或者更新 JSON 数据的时候,要注意 Realm 需要确保 JSON 的属性名和类型能够与 RLMObject 属性完全匹配。例如:

  • float 属性应当使用 float 进行初始化——您也可以选择 NSNumbers
  • NSDate and NSData 属性无法从字符串进行自动推断, 而应该在传递给 [RLMObject createOrUpdateInRealm:withValue:] 之前转换为适当的类型。
  • 如果 JSON 中的属性是 null (例如:NSNull) 提供给了一个必需属性的话,那么会抛出异常。
  • 如果某个必需属性在插入操作中没有提供的话,那么会抛出异常。
  • Realm 将会忽略 JSON 中没有以 RLMObject 定义的任何属性。

如果您的 JSON 模式和您的 Realm 对象无法完全一致的话,那么我们推荐您使用第三方的模型映射框架,这样可以对您的 JSON 进行转换。Objective‑C 有一系列积极维护的能够与 Realm 协同工作的模型映射框架,其中的一部分已经在 realm-cocoa 仓库 当中列出来了。

测试与调试

配置默认 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

注入 Realm 实例

另一种测试使用 Realm 代码的方式是让所有您打算进行测试的方法以参数的方式获取 RLMRealm 实例,这样您就可以在应用运行和测试时将不同的 Realm 文件传递进去。例如,假设您的应用拥有一个从 JSON API 中获取用户配置文件的 GET 方法,然后您想要对其进行测试,确保本地配置文件能够正常创建:

// 应用代码

@implementation ClassBeingTested
+ (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:(NSJSONReadingOptions)nil error:nil];
    [realm transactionWithBlock:^{
        [User createOrUpdateInRealm:realm withValue:object];
    }];
}
@end

// 测试代码

@implementation UnitTests
- (void)testThatUserIsUpdatedFromServer {
    RLMRealm *testRealm = [RLMRealm realmWithURL:kTestRealmURL];
    NSData *jsonData = [@"{\"email\": \"[email protected]\"}"
                        dataUsingEncoding:NSUTF8StringEncoding];
    [ClassBeingTested createOrUpdateUserInRealm:testRealm withData:jsonData];
    User *expectedUser = [User new];
    expectedUser.email = @"[email protected]";
    XCTAssertEqualObjects([User allObjectsInRealm:testRealm][0],
                          expectedUser,
                          @"User was not properly updated from server.");
}
@end

调试

调试您的 Realm 应用是非常简单的,您可以通过 Realm 浏览器来实时查看您应用中的数据。

我们的 SDK 包含有一段 LLDB 脚本,可以在 Xcode 的用户界面中检查已管理的 RLMObjectRLMResult 以及 RLMArray 对象,而不只是简单地显示 nil 或者 0

Xcode截图

注意: 这一功能只支持Objective‑C。Swift的支持版本仍在计划中。

如果您以动态框架的方式运行 Realm 的话,那么 您需要确保单元测试目标能否正确识别 Realm。 您可以通过向您单元测试的 “Framework Search Paths” 中添加 Realm.framework 的上级目录来完成此功能。

如果您的测试失败提示消息为 "Object type '...' not persisted in Realm",那么这很可能是因为您直接将 Realm 框架直接链接到单元测试目标上了,这恰恰是应该避免的。从您的测试目标中解除 Realm 的链接就可以解决这个问题。

同样地,您应当确保您的数据模型类文件只在您的应用或者框架目标中进行了编译,千万不要将它们置入到您的单元测试目标当中。否则,这些类会在测试的过程中被复制,这往往会为调试问题时带来麻烦(可参考此问题 了解更多信息)。

您也要确保所有您需要进行测试的代码能够供单元测试目标访问(需要使用 public 访问修饰符或者 @testable标识符)。关于更多信息,请参阅这个 Stack Overflow 回答。

目前的限制

这个列表列出了目前我们最常见的一些限制情况。

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

常见限制

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

  1. 类名称的长度最大只能存储 57 个 UTF8 字符。
  2. 属性名称的长度最大只能支持 63 个 UTF8 字符。
  3. NSDataNSString 属性不能保存超过 16 MB 大小的数据。如果要存储大量的数据,可通过将其分解为16MB 大小的块,或者直接存储在文件系统中,然后将文件路径存储在 Realm 中。如果您的应用试图存储一个大于 16MB 的单一属性,系统将在运行时抛出异常。
  4. 每个单独的 Realm 文件大小无法超过应用在 iOS 系统中所被允许使用的内存量——这个量对于每个设备而言都是不同的,并且还取决于当时内存空间的碎片化情况(关于此问题有一个相关的 Radar:rdar://17119975)。如果您需要存储海量数据的话,那么可以选择使用多个 Realm 文件并进行映射。
  5. 对字符串进行排序以及不区分大小写查询只支持“基础拉丁字符集”、“拉丁字符补充集”、“拉丁文扩展字符集 A” 以及”拉丁文扩展字符集 B“(UTF-8 的范围在 0~591 之间)。

线程

尽管 Realm 文件可以被多个线程同时访问,但是您不能直接跨线程传递 Realms、Realm 对象、查询和查询结果。如果您需要跨线程传递 Realm 对象的话,您可以使用 RLMThreadSafeReference API。有关 Realm 线程的更多知识,可以参阅线程一节。

模型

Setter 和 Getter:因为 Realm 在底层数据库中重写了 setters 和 getters 方法,所以您不可以在您的对象上再对其进行重写。一个简单的替代方法就是:创建一个新的 Realm 忽略属性,该属性的访问起可以被重写, 并且可以调用其他的 getter 和 setter 方法。

自动增长属性:Realm 没有线程且进程安全的自动增长属性机制,而这在其他数据库中常常用来产生主键。然而,在绝大多数情况下,对于主键来说,我们需要的是一个唯一的、自动生成的值,因此没有必要使用顺序的、连续的、整数的 ID 作为主键,因此一个独一无二的字符串主键通常就能满足需求了。一个常见的模式是将默认的属性值设置为 [[NSUUID UUID] UUIDString] 以产生一个唯一的字符串 ID。

自动增长属性另一种常见的动机是为了维持插入之后的顺序。在某些情况下,这可以通过向某个 RLMArray 中添加对象,或者使用 [NSDate date] 默认值的 createdAt 属性。

-[NSPredicate evaluateWithObject:] 无法使用 Realm 集合,因为它们是非集合对象:由于在 NSPredicate 的内部实现中存在一些过度约束的检查,因此某些 NSPredicate 的 API 与 Realm 的集合类型并不兼容。例如,在谓词子查询试图对 Realm 集合进行迭代操作时,-[NSPredicate evaluateWithObject:] 将会抛出异常。Apple 已经明确了这个问题:(rdar://31252694)。

如果您需要在应用中解决此问题的话,可以将 PR #4770 中的补丁集成进去,在执行任何谓词赋值 (predicate evaluation) 操作之前,调用一次 RLMWorkaroundRadar31252694()

文件大小

Realm 读取事务的生命周期与 RLMRealm 实例的生命周期相关联。避免使用自动刷新 Realm 数据库,以及在显式自动释放池中从后台进程封装所有的 Realm API 使用,以达成“固定”旧有 Realm 事务的目的。

一般来说 Realm 数据库比 SQLite 数据库在硬盘上占用的空间更少。如果您的 Realm 文件大小超出了您的想象,这可能是因为您数据库中的 RLMRealm 中包含了旧版本数据。

为了使您的数据有相同的显示方式,Realm 只在循环迭代开始的时候才更新数据版本。这意味着,如果您从 Realm 读取了一些数据并进行了在一个锁定的线程中进行长时间的运行,然后在其他线程进行读写 Realm 数据库的话,那么版本将不会被更新,Realm 将保存中间版本的数据,但是这些数据已经没有用了,这导致了文件大小的增长。这部分空间会在下次写入操作时被重复利用。这些操作可以通过设置 shouldCompactOnLaunch 或者通过调用 writeCopyToPath:error: 来实现。

为了避免这个问题,您可以调用invalidate,来告诉 Realm 您不再需要那些拷贝到 Realm 的数据了。这可以使我们不必跟踪这些对象的中间版本。在下次出现新版本时,再进行版本更新。您可能在 Realm 使用 Grand Central Dispatch 时也发现了这个问题。在 dispatch 结束后自动释放调度队列(dispatch queue)时,调度队列(dispatch queue)没有随着程序释放。这造成了直到 RLMRealm`` 对象被释放后,Realm 中间版本的数据空间才会被再利用。为了避免这个问题,您应该在 dispatch 队列中, 使用一个显式的自动调度队列。

使用 Realm API 初始化 Swift 属性

如果您打算编写 Swift 应用的话,那么对于 Swift 应用的类和结构体当中所定义的属性而言,其值可以使用 Realm API 来进行构造。例如:

class SomeSwiftType {
    let persons = RLMPerson.allObjects(in: RLMRealm.default())
    // ...
}

如果您使用类似的属性来定义相关的类型的话,您需要注意:如果在 Realm 配置操作完成之前,就去调用这些新构造的类型很可能会导致问题的发生。例如,如果您在 applicationDidFinishLaunching() 当中对默认的 Realm 配置操作设置了迁移代码,但是您又在 applicationDidFinishLaunching() 运行之前创建了 SomeSwiftType 的实例,这个时候您的 Realm 正在进行迁移操作,这样您就会在 Realm 被正确配置之前对原有的 Realm 进行了访问。

为了避免此类现象的发生,您可以选择:

  1. 推迟初始化任何用到 Realm API 属性的类型,直到应用完成 Realm 配置。
  2. 使用 Swift 的 lazy 关键字来定义相关属性。这允许您能够在应用生命周期的任一时段安全地构建相关的类型,只要您不要在应用完成 Realm 配置之前尝试去访问这些惰性属性即可。
  3. 仅使用明确获取用户定义配置的 Realm API 来构造属性。这样,您就能确保您所使用的配置能够在打开 Realm 之前正确完成设置。

加密的 Realm 数据库与多进程

加密的 Realm 不能同时被多个进程访问。这其中包括了 iOS 应用扩展。要解决这个问题,请使用未加密的 Realm,这样才能够跨进程共享。您可以使用 Security 和 CommonCrypto 系统框架来对 Realm 对象存储在 NSData 属性当中的数据进行加密。

我们正在跟踪以解决这方面的限制, 包括了 Realm Cocoa 问题追踪(#1693) 以及 Realm 核心问题追踪(#1845)。

小技巧

我们将一些小技巧集中在一起,展示如何使用 Realm 来完成一些特定的任务。我们将会定期添加这些小技巧,因此请时常回顾。如果您有想看到的示例的话,请在 Github 上开启一个 issue。

问答时间到!

我该如何寻找并查看 Realm 文件以及当中的内容呢?

这个 StackOverflow 回答 讲述了去哪寻找您的 Realm 文件。您可以通过我们的 Realm 浏览器 来查看内容。

Realm 基础库有多大?

一般而言,Realm 只会给应用的下载大小增加 5 到 8 MB 左右。我们的发布版本可能会更大一些,这是因为其中还包含了对 iOS、watchOS 以及 tvOS 模拟器的支持库、某些调试符号以及 bitcode,此外还有某些当编译应用时会被 Xcode 自动排除的中间代码。

Realm 开源了吗?

没错!Realm 内部 C++ 存储引擎以及其不同语言封装的 SDK 现已基于 Apache 2.0 协议开源! 不过,Realm 还包含了一个闭源的同步组件,但是如果您将 Realm 作为嵌入式数据库使用的话,这个组件是不必要的。

在应用运行的时候发现了一个名为 Mixpanel 的网络调用

当您的应用处于调试模式运行,或者在模拟器当中运行的时候,Realm 将会收集匿名的统计信息。这些分析结果都是匿名的,并通过收集 Realm、iOS 、macOS 的版本信息、您使用的语言,以及您目前使用的 Xcode 版本信息从而帮助我们更好地改进产品。这个调用不会在实际应用上运行,也不会在用户设备上运行。只有在模拟器中或者处于调试状态下时,才会运行。您可以看到我们是如何进行收集的,也可以查看我们所收集的内容。其原理可以在我们的源码中查看。

疑难解答

崩溃报告

我们建议您在应用中使用崩溃报 (crash reporter)。很多 Realm 操作都可能会在运行时发生崩溃(就如同其他硬盘 I/O 操作一样),因此从应用中收集这些崩溃报告可以帮助您(或者我们)发现出错的具体位置,从而更好地进行错误处理以及修复相关的问题。

绝大多数商用的崩溃报告都有收集日志的选项。我们强烈建议您开启此功能。Realm 在抛出异常或者处于无法恢复的状况时将会记录元数据 (metadata) 信息(但不会记录用户数据),这些信息在出错的时候对调试有很大帮助。

报告 Realm 的错误

如果您发现了 Realm 中的任何错误,请 在 Github 上提交一个 issue,也可以给 help@realm.io 发邮件信息。请尽可能多地给我们发送相关的错误信息,这样可以帮助我们更好的理解并解决您提出的问题。

下面信息对我们来说将十分有用:

  1. 您的目的
  2. 您所期望的结果
  3. 实际结果
  4. 产生此结果的步骤
  5. 突出此问题的代码示例 (完整的 Xcode 项目的话我们可以自行编译,更好理解)
  6. Realm / Xcode / macOS 的版本
  7. 依赖库管理器的版本(CocoaPods / Carthage)
  8. 出现 Bug 的平台, OS 版本及架构(例如 64-bit iOS 8.1)
  9. 崩溃日志以及堆栈轨迹 (stack traces),参见上方的 崩溃报告 了解更多信息。

依赖管理器

如果您曾经通过 CocoaPods 或者 Carthage 安装过 Realm,并且遇到了编译错误的话,那么很可能是您使用了该依赖管理器所不支持的版本,也可能是 Realm 没有成功整合到项目当中,还可能是构建工具中遗留有缓存。如果这样的话,请尝试删除依赖管理器所创建的这些文件夹,并重新安装。

此外,您还可以尝试删除 Derived Data,然后清除 Xcode 的构建文件夹;这可以修复因构建工具版本更新或者项目配置发生变更(例如增加新的 Target,跨 Target 之间共享依赖等等)而导致的问题。

要清除构建文件夹的话,按住 “Option” 键然后打开 “Product” 菜单,随后选择 “Clean Build Folder…“。您也可以在 Xcode 帮助菜单的搜索栏中键入 “Clean”,然后在搜索结果当中选中出现的 “Clean Build Folder…” 菜单项。

CocoaPods

Realm 可以使用 CocoaPods 0.39.0 或者更高版本进行安装。

如果您在使用 CocoaPods 进行集成的过程中遇到了问题,那么重置集成状态或许可以帮助您解决这个问题。为了实现此操作,只需要在终端中,进入到项目根目录中并运行以下命令:

pod cache clean Realm
pod cache clean RealmSwift
pod deintegrate || rm -rf Pods
pod install --verbose
rm -rf ~/Library/Developer/Xcode/DerivedData

您还可以使用 cocoapods-deintegrate,而不是选择直接删除 Pods 文件夹。对于 CocoaPods 1.0 而言,这属于预安装插件。如果您使用的 CocoaPods 版本较老的话,那么可以使用 gem install cocoapods-deintegrate 来安装这个插件。您可以使用 pod deintegrate 来运行这个插件。它将从您的 Xcode 项目中移除 CocoaPods 的所有痕迹。

Carthage

Realm 可以通过 Carthage 0.9.2 以及更高版本进行安装。

要从项目中移除所有由 Carthage 管理的依赖关系,只需要在终端中,进入到项目根目录中并运行以下命令:

rm -rf Carthage
rm -rf ~/Library/Developer/Xcode/DerivedData
carthage update

Realm 核心二进制库下载失败

当 Realm 构建的时候,其过程还包含了下载静态库形式的核心库,然后将其整合到 realm-cocoa 项目当中。已经有报告指出,在某些情况下,核心二进制库会下载失败,并报以下错误:

Downloading core failed. Please try again once you have an Internet connection.

导致此错误的原因可能有以下几点:

  1. 您的 IP 地址处于 美国禁止区域 列表列出的地区当中。为了遵循美国的相关法律,Realm 无法给在此地区中使用。欲了解更多信息,请参阅我们的 许可证
  2. 您位于中国大陆,由于 GFW 的原因,此时无法正常访问 CloudFlare 或者 Amazon AWS S3 服务。请参见这个 Realm-Cocoa 问题 了解更多信息。
  3. Amazon AWS S3 可能会遇到服务问题,请检查 AWS Service Health Dashboard,稍后再试。

在低内存的限制下运行

如果您想要在内存受限的环境下使用 Realm,例如 watchOS 应用或者应用扩展,那么我们建议您明确将类指定由 Realm 进行管理,这样可以避免 objc_copyClassList() 的额外调用。

RLMRealmConfiguration *config = [RLMRealmConfiguration defaultConfiguration];
config.objectClasses = @[Dog.class, Person.class];
RLMRealm *realm = [RLMRealm realmWithConfiguration:config error:nil];

获得帮助

  • 编码过程中遇到了问题? 在 StackOverflow 上提问,我们会经常在上面查看以及回答问题!
  • 发现了 BUG? 可以直接在GitHub repo提交给我们。如果可以的话,请给我们提供您所使用的 Realm 版本号、完整的日志记录、Realm 文件以及您的当前项目,以便我们能够重现您所发现的问题。
  • 希望我们新增功能? 可以直接在GitHub repo提交给我们。请告诉我们需要实现何种功能和特性,以及新增这些功能的理由。

如果您在使用崩溃检测 SDK (诸如 Crashlytics 或者 HockeyApp) 的话, 请确保开启了日志收集功能。 Realm 会在抛出异常以及发生致命错误的时候会记录下元数据信息(不是用户数据), 这些信息可以在出现问题的时候有效地帮助您进行解决。