你正在浏览一个旧版本的 Realm 文档。你可能需要查看当前版本的文档

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

Objective‑C版本的 Realm 能够让您以一种安全、耐用以及迅捷的方式来高效地编写应用的数据模型层,如下例所示:

// 定义模型的做法和定义常规 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 = @"大黄";
mydog.age = 1;
mydog.picture = nil; // 属性的值可以为空
NSLog(@"狗狗的名字: %@", 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), ^{
  Dog *theDog = [[Dog objectsWhere:@"age == 1"] firstObject];
  RLMRealm *realm = [RLMRealm defaultRealm];
  [realm beginWriteTransaction];
  theDog.age = 3;
  [realm commitWriteTransaction];
});

如果您的应用正在使用 Core Data 并打算换用 Realm 的话,我们最近发布了一篇关于如何执行转换的文章,点击此处查看!

从这里开始

下载 Realm Objective‑C 或者在GitHub上查看源码!

准备工作

  • 使用 Realm 构建应用的基本要求:iOS 7 及其以上版本, macOS 10.9 及其以上版本,此外 Realm 支持 tvOS 和 watchOS 的所有版本。
  • 需要使用 Xcode 7.3 或者以后的版本。

安装

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

  1. 下载最新的Realm发行版本,并解压;
  2. 前往Xcode 工程的”General”设置项中,从ios/dynamic/osx/tvos/或者watchos/中将’Realm.framework’拖曳到”Embedded Binaries”选项中。确认Copy items if needed被选中(除非您的项目中需要在多个平台中使用 Realm),点击Finish按钮;
  3. 在单元测试 Target 的”Build Settings”中,在”Framework Search Paths”中添加Realm.framework的上级目录;
  4. 如果希望使用 Swift 加载 Realm,请拖动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商店提交的bug,这一步在打包通用设备的二进制发布版本时是必须的。

  1. 安装CocoaPods 0.39.0 或者更高版本
  2. 在您的Podfile中,添加pod 'Realm'到您的 app 目标中,添加pod 'Realm/Headers'到您的测试目标中;
  3. 在终端运行pod install
  4. 采用 CocoaPods 生成的.xcworkspace来运行工程!
  5. 如果需要在 Swift 当中使用的话,将位于 Swift/RLMSupport.swift 的这个文件拖动到您 Xcode 项目的文件导航器当中,检查以确保 Copy items if needed 选项已被勾选。
  1. 安装 Carthage 0.17.0 或者更高版本
  2. 在Carthage 中添加github "realm/realm-cocoa"
  3. 运行carthage update;为了修改用以构建项目的 Swift toolchain,通过 --toolchain 参数来指定合适的 toolchain。--no-use-binaries 参数也是必需的,这可以避免 Carthage 将预构建的 Swift 3.0 二进制包下载下来。 例如:

    carthage update --toolchain com.apple.dt.toolchain.Swift_2_3 --no-use-binaries
  4. Carthage/Build/ 目录下对应平台文件夹中,将 Realm.framework 拖曳到您 Xcode 工程”General”设置项的”Linked Frameworks and Libraries”选项卡中;
  5. 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商店提交的bug,这一步在打包通用设备的二进制发布版本时是必须的。

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

导入 Realm 框架

在您的 Objective-C 源文件的顶部,使用 #import <Realm/Realm.h> 来导入 Objective-C 版本的 Realm,以便让其能够在代码中使用。在您的 Swift 源文件的顶部(如果有的话),使用 import Realm。这就是开始使用 Realm 之前您所需要做的步骤!

在 Swift 当中使用 Objective‑C 版本的 Realm

如果您的项目上完全基于 Swift 构建的话,那么您应当考虑使用 Swift 版本的 Realm

Objective‑C 版本的 Realm 的设计目标包含了能够在 Objective‑C 和 Swift 混编的项目中使用。在 Swift 中使用此 API 的话,您可以实现所有与在 Objective‑C 中使用 Realm 所能够完成的功能,例如定义模型以及使用 Realm 的 Objective‑C API。 然而,与纯 Objective‑C 项目相比,您仍然需要对某些部分进行一些小小的改变:

RLMSupport.swift

我们建议您将 Swift/RLMSupport.swift 文件一同编译进去(这个文件您同样能够在我们的发行版本压缩包中找到)。

这个文件为 Objective-C 版本的 Realm 集合类型中引入了 Sequence 一致性,并且重新暴露了一些不能够从 Swift 中进行原生访问的 Objective-C 方法,例如可变参数 (variadic arguments)。

Objective‑C 版本的 Realm 默认情况下不包含这个文件,因为这会强制让所有使用 Objective‑C 版本 Realm 的用户去包含额外的 Swift 动态库,不管他们有没有在项目中使用 Swift!

RLMArray 属性

在 Objective‑C 中,我们依赖协议一致性,从而让 Realm 能够明白 RLMArray 对多关系 中包含的对象类型。在 Swift 中,这种类型的语法是不可能实现的。因此,您应当用下列语法形式来声明您的 RLMArray 属性:

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

这等同于 Objective‑C 中的声明:

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

tvOS

由于在 tvOS 中向 Doucments 目录写入数据是被禁止的,因此默认的 Realm 路径位置将被设置为 NSCachesDirectory 。然而,要注意的是 tvOS 会随时清除 Caches 目录下的文件,因此我们建议您将 Realm 数据库作为一个新的缓存机制使用,而不是用其来存储重要的用户数据。

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

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

您同样可以在您应用中加入预构建的 Realm 文件。不过,一定要确保遵循 App Store 应用上架指南,保证应用大小在 200 MB 以内。

您可以浏览我们的 tvOS 示例项目 ,以此来展示简单 tvOS 应用是如何使用 Realm 进行离线缓存以及预加载数据的。

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

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

数据模型(Model)

Realm数据模型是基于标准 Objective‑C 类来进行定义的,使用属性来完成模型的具体定义。

通过简单的继承 RLMObject 或者一个已经存在的模型类,您就可以创建一个新的 Realm 数据模型对象。

Realm模型对象在形式上基本上与其他 Objective‑C 对象相同 - 您可以给它们添加您自己的方法(method)和协议(protocol),和在其他对象中使用类似。

主要的限制是某个对象只能在其被创建的那个线程中使用, 并且您无法访问任何存储属性的实例变量(ivar)。

如果您安装了我们的Xcode插件 ,那么可在”New File…“对话框中会有一个很漂亮的模板,可用来创建接口(interface)和执行(implementation)文件。

您只需要为对象的类型列表添加目标类型的属性,或者RLMArray,就可以创建数据关系(relationship)和嵌套数据结构(nested data structure)。

#import <Realm/Realm.h>

@class Person;

// 狗狗的数据模型
@interface Dog : RLMObject
@property NSString *name;
@property Person   *owner;
@end
RLM_ARRAY_TYPE(Dog) // 定义RLMArray<Dog>

// 狗狗主人的数据模型
@interface Person : RLMObject
@property NSString      *name;
@property NSDate        *birthdate;
@property RLMArray<Dog> *dogs;
@end
RLM_ARRAY_TYPE(Person) // 定义RLMArray<Person>

// 实现文件
@implementation Dog
@end // 暂时没用

@implementation Person
@end // 暂时没用

由于 Realm 中定义的所有模型在程序启动时就会被解析,所以即使代码中没有调用,它们都需要被初始化。

在 Swift 中使用 Realm 的时候,Swift.reflect(_:) 函数可用于确定您模型中的信息,这需要确保 init() 已被成功调用。这意味着所有非可选的属性必须添加一个默认值。

通过RLMObject 可查看更多细节。

支持的类型

Realm支持以下的属性类型:BOOLboolintNSIntegerlonglong longfloatdoubleNSStringNSDateNSData 以及 被特殊类型标记的 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”建立关系,也就是表明一个「人」可以养多条「狗」,那么我们首先需要定义一个 RLMArray<Dog> 类型。通过对应数据模型接口文件下的宏命令即可完成:

//Dog.h
@interface Dog : RLMObject
// 属性声明...
@end

RLM_ARRAY_TYPE(Dog) // 定义一个 RLMArray<Dog> 类型

RLM_ARRAY_TYPE 宏创建了一个协议,从而允许 RLMArray<Dog> 语法的使用。如果该宏没有放置在模型接口的底部的话,您或许需要提前声明该模型类。

接下来您就能定义RLMArray<Dog>类型的属性了:

```objc
// 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 属性将确保其当中的插入次序不会被扰乱。

反向关系(Inverse Relationship)

链接是单向性的。因此,如果对多关系属性 Person.dogs 链接了一个 Dog 实例,而这个实例的对一关系属性 Dog.owner 又链接到了对应的这个 Person 实例,那么实际上这些链接仍然是互相独立的。为 Person 实例的 dogs 属性添加一个新的 Dog 实例,并不会将这个 Dog 实例的 owner 属性自动设置为该 Person。但是由于手动同步双向关系会很容易出错,并且这个操作还非常得复杂、冗余,因此 Realm 提供了“链接对象 (linking objects)” 属性来表示这些反向关系。

借助链接对象属性,您可以通过指定的属性来获取所有链接到指定对象的对象。例如,一个 Dog 对象可以拥有一个名为 owners 的链接对象属性,这个属性中包含了某些 Person 对象,而这些 Person 对象在其 dogs 属性中包含了这一个确定的 Dog 对象。您可以将 owners 属性设置为 RLMLinkingObjects 类型,然后重写 +[RLMObject linkingObjectsProperties] 来指明关系,说明 ownders 中包含了 Person 模型对象。

@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

可空属性(Optional Properties)

通常情况下,NSString *NSData * 以及 NSDate * 属性可以设置为 nil。如果你不需要实现此功能,你可以重写您的 RLMObject 子类的 +requiredProperties 方法。

比如对于以下的模型定义来说,如果尝试给 name 属性设置为 nil 将会抛出一个异常,但是将 birthday 属性设置为 nil 却是允许的:

@interface Person : RLMObject
@property NSString *name;
@property NSDate *birthday;
@end

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

存储可空数字目前已经可以通过 NSNumber * 属性完成。

由于 Realm 对不同类型的数字采取了不同的存储格式,因此设置可空的数字属性必须是 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 值。

简单说明

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

类型 非可选值形式 可选值形式
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 n/a: 必须是可选值 @property Object *value;
List @property RLMArray<Object *><Object> *value; n/a: 必须是非可选值
LinkingObjects @property (readonly) RLMLinkingObjects<Object *> *value; 2 n/a: 必须是非可选值

1) Objective‑C 引用类型的必需属性必须要声明在联合体当中:

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

2) 链接对象属性必须连带 +linkingObjectsProperties 方法一同声明:

@implementation MyModel
+ (NSDictionary *)linkingObjectsProperties {
    return @{ @"property": [RLMPropertyDescriptor descriptorWithClass:Class.class propertyName:@"link"] };
}
@end

属性特性(attributes)

注意由于 Realm 在自己的引擎内部有很好的语义解释系统,所以 Objective‑C 的许多属性特性将被忽略,如nonatomic, atomic, strong, copyweak 等。 因此为了避免误解,我们推荐您在编写数据模型的时候不要使用任何的属性特性。 当然,如果您已经设置了这些属性特性,那么在 RLMObject 对象被写入 Realm 数据库前,这些特性会一直生效。 无论 RLMObject 对象是否受到 Realm 管理,您为其编写的自定义 getter 和 setter 方法都能正常工作。

如果您在 Swift 中使用 Objective-C 版本的 Realm 的话,模型的属性前面需要加上 dynamic var,这是为了让这些属性能够被底层数据库数据所访问。

索引属性(Indexed Properties)

重写 +indexedProperties 方法可以为数据模型中需要添加索引的属性建立索引:

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

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

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

对属性进行索引可以减少插入操作的性能耗费,加快比较检索的速度(比如说 = 以及 IN 操作符)。

属性默认值

重写+defaultPropertyValues 可以每次在对象创建之后为其提供默认值。

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

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

对象的自更新特性

RLMObject 实例是底层数据的动态表现,其会进行自动更新,这意味着对象不需要进行刷新。修改某个对象的属性会立刻影响到其他所有指向同一个对象的实例。

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

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

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

myDog.age; // => 2

RLMObject 的这个特性不仅让 Realm 保证速度和效率,它同时还让代码更加简洁、更为灵活。比如说,如果您的 UI 代码是基于某个特定的 Realm 对象来现实的,那么在触发 UI 重绘之前,您不用担心数据的刷新或者重新检索等问题。

您也可以查看 Realm 通知 一节以确认 Realm 数据何时被更新,比如说由此来决定应用 UI 何时需要被更新。此外,还可以使用 键值编码,当某个 RLMObject 的特定属性发生更新时去发送通知。

主键(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

模型继承

Realm 允许模型能够生成更多的子类,也允许跨模型进行代码复用,但是由于某些 Cocoa 特性使得运行时中丰富的类多态无法使用。以下是可以完成的操作:

  • 父类中的类方法,实例方法和属性可以被它的子类所继承
  • 子类中可以在方法以及函数中使用父类作为参数

以下是不能完成的:

  • 多态类之间的转换(例如子类转换成子类,子类转换成父类,父类转换成子类等)
  • 同时对多个类进行检索
  • 多类容器 (RLMArray 以及 RLMResults)。

向 Realm 中增加此特性已经在规划当中,并且我们暂时提供了一些代码示例,以便能够对更常见的模式进行处理。

另外,如果您的代码实现允许的话,我们建议您使用以下模式,也就是使用类组合模式来构建子类,以便能够包含其他类中的相关逻辑:

// 基础模型
@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

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

集合

Realm 拥有一系列能够帮助表示一组对象的类型,我们称之为「Realm 集合」:

  1. RLMResults类,表示从检索 中所返回的对象集合。
  2. RLMArray类,表示模型中的对多关系
  3. RLMLinkingObjects类,表示模型中的反向关系
  4. RLMCollection 协议,定义了所有 Realm 集合所需要遵守的常用接口。

Realm 集合实现了 RLMCollection 协议,这确保它们能够保持一致。这个协议继承自 NSFastEnumeration,因此它应当与其他 Foundation 当中的集合用法一致。 其他常用的 Realm 集合 API 也在这个协议当中进行了声明,例如其中包括检索、排序以及聚合操作。 RLMArray拥有额外的修改操作,这些操作不在协议接口当中有定义,例如添加和删除对象。

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

@implementation MyObject
- (void)operateOnCollection:(id<RLMCollection>)collection {
  // collection 既可以是 RLMResults,也可以是 RLMArray
  NSLog(@"对集合 [email protected] 进行操作", collection.objectClassName);
}
@end

对象存储

对对象的所有更改(添加,修改和删除)都必须通过写入事务(transaction)完成。

Realm 的对象可以被实例化并且作为unmanaged对象使用(也就是还未添加到 Realm 数据库中的对象),和其他常规Objective‑C对象无异。

如果您想要在多个线程中共享对象,或者在应用重启后重复使用对象,那么您必须将其添加到 Realm 数据库中——这个操作必须在写入事务中完成。

因为写入事务将会产生不可忽略的性能消耗,因此你应当检视你的代码以确保减少写入事务的次数。

由于写入事务像其余硬盘读写操作一样,会出现失败的情况,因此 -[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. 使用指定初始化器(designated initializer)创建对象是最简单的方式。请注意,所有的必需属性都必须在对象添加到 Realm 前被赋值。
  2. 通过使用恰当的键值,对象还可以通过字典完成创建。
  3. 最后,RLMObject 子类还可以通过数组完成实例化,数组中的值必须和数据模型中对应属性的次序相同。

嵌套属性(Nested Object)

如果某个对象中有 RLMObject 或者 RLMArray 类型的属性,那么通过使用嵌套的数组或者字典便可以对这些属性递归地进行设置。您只需要简单的用表示其属性的字典或者数组替换每个对象即可:

// 这里我们就可以使用已存在的狗狗对象来完成初始化
Person *person1 = [[Person alloc] initWithValue:@[@"李四", @30, @[aDog, anotherDog]]];

// 还可以使用多重嵌套
Person *person2 = [[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的 Book 对象已经存在于数据库当中了,那么对象就会简单地进行更新。而如果不在数据库中存在的话,那么这个操作将会创建一个新的 Book 对象并添加到数据库当中。

您同时通过传递您想要更新值的集合,从而更新带有主键的某个对象的部分值,比如说如下所示:

// 假设带有主键值 `1` 的“书籍”对象已经存在
[realm beginWriteTransaction];
[Book createOrUpdateInRealm:realm withValue:@{@"id": @1, @"price": @9000.0f}];
// 这本书的`title`属性不会被改变
[realm commitWriteTransaction];

如果对象没有定义主键的话,那么您不能够调用出现在本章的这些方法(也就是那些以 OrUpdate 结尾的方法)。

请注意,当对象更新的时候,NSNull 仍然会被认为是可选属性 的有效值。如果您提供了一个属性值为 NSNull 的字典,那么这些都会应用到您的对象当中,并且对应的属性都将为空。为了确保不出现任何意外的数据丢失,请在使用此方法的时候再三确认只提供了您想要进行更新的属性。

键值编码

RLMObjectRLMResult 以及 RLMArray 都遵守键值编码(Key-Value Coding)(KVC)机制。 当您在运行时才能决定哪个属性需要更新的时候,这个方法是最有用的。

将 KVC 应用在集合当中是大量更新对象的极佳方式,这样就可以不用经常遍历集合,为每个项目创建一个访问器了。

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

删除数据

通过在写入事务中将要删除的对象传递给 -[RLMRealm deleteObject:] 方法,即可完成删除操作。

// 存储在 Realm 中的 cheeseBook 对象

// 在事务中删除一个对象
[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对象完成遍历关系图的操作。

除非查询结果被使用,否则检索的执行将会被推迟。这意味着链接几个不同的临时 {RLMResults} 来进行排序和匹配数据,不会执行额外的工作,例如处理中间状态。

一旦检索执行之后,或者 通知模块 被添加之后, RLMResults 将随时保持更新,接收 Realm 中,在后台线程上执行的检索操作中可能所做的更改。

从 Realm 中检索对象的最基本方法是+[RLMObject allObjects],这个方法将会返回带有所有RLMObjectRLMResults实例,并且这个实例的类型将是默认 Realm 数据库中被查询的子类类型。

RLMResults<Dog *> *dogs = [Dog allObjects]; // 从默认的 Realm 数据库中,检索所有狗狗

条件查询(Filtering)

如果您熟悉NSPredicate的话,那么您就能很容易掌握其在 Realm 中的查询方法。RLMObjects、RLMRealm、RLMArray 以及 RLMResults 都提供了方法,允许您通过简单地传递一个 NSPredicate 实例、断言字符串或者断言格式化字符串来完成查询这顶RLMObject实例的操作,正如您在 NSArray 中执行查询的哪样。

比如说,下面的例子就展示了如何通过从默认的 Realm 数据库中调用 [RLMObject objectsWhere:] 方法来检索所有棕黄色,并且以“大”开头命名的狗狗的:

// 使用断言字符串查询
RLMResults<Dog *> *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 以及 RLMResults 属性支持集合表达式:@count@min@max@sum 以及 @avg,例如 [Company objectsWhere:@"employees.@count > 5"] 用以寻找所有超过 5 名雇员的公司。
  • 支持子查询,不过有限制:
    • @count 是唯一能应用在 SUBQUERY 表达式中的操作符
    • SUBQUERY(…).@count 表达式必须与常量进行比较
    • 相关子查询目前还不支持

要了解关于断言的更多详情,请查看[RLMObject objectsWhere:] 的详细信息。

排序

RLMResults 允许您指定一个排序标准,从而可以根据一个或多个属性进行排序。比如说,下列代码将上面例子中返回的狗狗根据名字升序进行排序:

// 排序名字以“大”开头的棕黄色狗狗
RLMResults<Dog *> *sortedDogs = [[Dog objectsWhere:@"color = '棕黄色' AND name BEGINSWITH '大'"]
                               sortedResultsUsingProperty:@"name" ascending:YES];

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

注意到,只有当检索操作排序过的时候,Results 的次序只能保证不变。出于性能考虑,插入后的顺序不能保证稳定。如果您需要保证插入数据后的顺序,在这里有一些解决方案。

链式查询

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

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

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

自动更新

RLMResults 是底层数据的动态表现,其会进行自动更新,这意味着检索到的结果不能进行重复检索。它们会反映出当前线程上 Realm 的当前状态,包括在当前线程上的写事务当中。唯一的例外是,当使用 for...in 遍历时,因为当遍历开始的时候,总会全部遍历完所有匹配该检索的对象,即使某些对象在遍历过程中被过滤器修改或者删除。

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

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

puppies.count; // => 1

这对所有的 RLMResults 都有影响,不管其是匹配查询的还是链式查询所检索出来的。

RLMResults 的这个特性不仅让 Realm 保证速度和效率,它同时还让代码更加简洁、更为灵活。比如说,如果您的视图控制器是基于查询结果而现实的,您可以将 RLMResults 存储为一个属性,这样就无需在每次访问前都要刷新数据以确保数据最新了。

您也可以查看 Realm 通知 一节以确认 Realm 数据何时被更新,比如说由此来决定应用 UI 何时需要被更新,这样就无必重新检索 RLMResults 了。

由于检索结果是自动更新的,因此不要迷信 index 以及 count 会一直保持静止。RLMResults 数据不变的唯一情况就是处于快速枚举的过程当中,这允许修改匹配检索的对象:

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

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

查询结果限制

大多数其他数据库技术都提供了从检索中对结果进行“分页”的能力(例如 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 数据库

默认的 Realm 数据库

您可能很早就已经注意到,我们总是通过调用 [RLMRealm defaultRealm] 来初始化以及访问我们的 realm 变量。这个方法将会返回一个 RLMRealm 对象,并指向您应用的 Documents (iOS) 或者 Application Support (OS X)文件夹下的一个名为“default.realm”的文件。

许多 Realm API 中的方法都支持两种默认的数据库访问方式,一种是 RLMRealm 实例,另一种是访问默认 Realm 数据库的便捷版本。例如 [RLMObject allObjects] 等同于 [RLMObject allObjectsInRealm:[RLMRealm defaultRealm]]

请注意,默认的 Realm 构造方法和便利方法都不允许进行错误处理;您应当在初始化 Realm 数据库一定不能失败的情况下使用这些方法。参考错误处理一节获取更多信息。

Realm 配置

通过RLMRealmConfiguration您可以配置诸如 Realm 文件在何处存储之类的信息。

配置同时也可以在每次您需要使用 Realm 实例的时候传递给[RLMRealm realmWithConfiguration:config error:&err],或者您也可以通过 [RLMRealmConfiguration setDefaultConfiguration:config] 来为默认的 Realm 数据库进行配置。

比如说,假设有这样一个应用,用户必须登录到您的网站后台才能够使用,然后您希望这个应用支持快速帐号切换功能。 您可以为每个帐号创建一个特有的 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数据库

有的时候,在不同位置存储多个 Realm 数据库是十分有用的。 例如,如果您需要将您应用的某些数据打包到一个 Realm 文件中,作为主要 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"];

请注意,使用自定义 URL 路径来初始化 Realm 数据库需要拥有路径所在位置的写入权限。 通常存储可写 Realm 文件的地方是位于 iOS 上的“Documents”文件夹以及位于 OS X 上的“Application Support”文件夹。 具体情况,请遵循苹果的 iOS 数据存储指南, 它推荐将文件存储在<Application_Home>/Library/Caches目录下。

内存数据库

通常情况下,Realm 数据库是存储在硬盘中的,但是您能够通过设置 inMemoryIdentifier 而不是设置RLMRealmConfiguration 中的 fileURL 属性,以创建一个完全在内存中运行的数据库。

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

内存数据库在每次程序运行期间都不会保存数据。但是,这不会妨碍到 Realm 的其他功能,包括查询、关系以及线程安全。 假如您需要灵活的数据读写但又不想储存数据的话,那么内存数据库对您来说一定是一个不错的选择。

内存数据库会在临时文件夹中创建多个文件,用来协调处理诸如跨进程通知之类的事务。 实际上没有任何的数据会被写入到这些文件当中,除非操作系统由于内存过满需要清除磁盘上的多余空间。

注意: 如果某个内存 Realm 数据库实例没有被引用,那么所有的数据就会被释放。强烈建议您在应用的生命周期内保持对Realm内存数据库的强引用,以避免不期望的数据丢失。

错误处理

和所有硬盘读写操作一样,当资源受限的时候创建一个 RLMRealm 实例可能会出现失败的情况。在实际情况中,这只会当首次在指定线程中创建 Realm 对象的时候发生。从相同线程中后续访问 Realm 数据库将会重复使用缓存的实例,不会导致失败。

要处理在指定线程中初次 Realm 数据库导致的错误, 给 error 参数提供一个 NSError 指针:

NSError *error = nil;

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

在Realm数据库间拷贝数据

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

查找 Realm 文件

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

附加的 Realm 文件

除了标准的 .realm 文件之外,Realm 同样还会为其内部操作生成和维护一些额外的文件和文件夹。

  • .realm.lock - 对资源进行锁定的文件
  • .realm.management - 存放进程锁文件 (Interprocess Lock) 的文件夹
  • .realm.note - 用于通知的命名管道

这些文件对 .realm 数据库文件本身不会造成任何影响,即时它们所依赖的数据库文件被删除或者被替换掉也不会引发任何错误。

提交 Realm 问题 的时候,请一定要确保提交这些附加文件,因为它们包含了诸多的有效信息可以帮助我们进行 Debug。

在应用中建立 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 数据库,那么您肯定不希望为它们协调进行数据迁移。 您可以通过设置您的 RLMRealmConfigurationobjectClasses 属性来对类做出限制。

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

删除 Realm 文件

在某些情况下,例如清除缓存、重置整个应用数据的时候,可能会需要将 Realm 文件从硬盘上被完全移除。

和其它文件不同,Realm 文件是内存映射的,并且 RLMRealm 实例需要该文件在实例的生命周期中能够一直可用。

因此,尽管大多数文件可以通过 NSFileManagerremoveItemAtPath 方法删除,但是您必须要采取额外的操作,来确保没有任何活跃的 RLMRealm 对象在删除过程中访问数据库。

RLMRealm 实例将会在它们的整个生命周期中,持有对数据库的连接。限制 Realm 实例生命周期的方法就是使用 autorelease pool 将其使用操作包含在其中。

因此,所有 fileURL 指向您想要删除的 Realm 文件的 RLMRealm 实例,都必须要在删除操作执行前被释放掉。

最后,尽管这不是必要的,您应当删除辅助的 Realm 文件 以及主要的 Realm 文件以确保所有相关文件得到了完全清理。

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

在后台应用刷新中使用 Realm

在 iOS 8 以上的系统中,当设备锁定的时候,应用中的文件会自动使用 NSFileProtection 进行加密。当设备锁定,并且您的 Realm 文件的 NSFileProtection 属性被设为『加密』(这个默认情况)的时候,如果您的应用视图执行任何涉及 Realm 的操作,那么就会抛出一条 open() failed: Operation not permitted 的异常。

为了处理这种情况,确认将文件保护属性降级为一个不太严格的、允许即使在设备锁定时都可以访问文件的属性,例如:NSFileProtectionCompleteUntilFirstUserAuthentication,并确保这个属性同时应用到了 Realm 文件本身以及其辅助文件上面。

如果您选择不完整的 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];

线程(Threading)

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

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) 来确定何时传递对象将不可能实现。

相反,还有很多可以安全在线程间传递实例的方法。比如说,一个拥有主键的对象可以通过其主键值来进行传递;还有 RLMResults 可以通过其 NSPredicate 或者检索语句来进行传递;还有 RLMRealm 可以通过其 RLMRealmConfiguration 来进行传递。目标线程可以通过使用其线程安全的表达方式来重新捕获这些 RLMRealmRLMObjectRLMResults 以及 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"      :  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 // 并不需要

NSData *data = [@"{\"name\": \"旧金山\", \"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
  • NSDateNSData 属性无法从字符串进行自动推断,而应该在传递给 [RLMObject createOrUpdateInRealm:withValue:] 之前转换为适当的类型。
  • 如果 JSON 中的属性是 null (例如:NSNull) 提供给了一个必需属性的话,那么会抛出异常。
  • 如果某个必需属性在插入操作中没有提供的话,那么会抛出异常。
  • Realm 将会忽略 JSON 中没有以 RLMObject 定义的任何属性。

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

通知(Notification)

通过调用 addNotificationBlock 方法进行通知注册后,无论哪个 RLMRealmRLMResultsRLMArray 还是 RLMLinkingObjects 对象更新,都可以得到通知。

此外,还可以通过 键值观察 来观察某个单独 RLMObject 对象的变化。

当通知不能即时触发的时候,多个写入事务可能会被合并到单独的一个通知当中。

一旦某个引用被返回的通知令牌持有的话,那么通知就会被触发。 您应当在类注册的时候保持对此令牌的强引用,以便能够进行更新,因为当通知令牌被销毁的时候,通知会被自动解除注册。

Realm 通知

Realm 实例将会在每次写入事务提交后,给其他线程上的 Realm 实例发送通知:

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

// 随后
[token stop];

集合通知

集合通知 (Collection notifications) 与 Realm 通知有着略微的不同,其包含了在细粒层级 (fine-grained level) 中描述发生了何种变化的信息。 包括自上一次通知以来出现的:已插入对象、已删除对象,或者已修改对象的索引。

集合通知是异步触发的,首先它会在初始结果出现的时候触发,随后当某个写入事务改变了集合中的所有或者某个对象的时候,通知都会再次触发。

这些变化可以通过传递到通知闭包当中的 RLMCollectionChange 参数访问到。这个对象当中包含了受 deletionsinsertionsmodifications 状态所影响的索引信息。

前两个状态:删除 (deletions)插入 (insertions) 记录了对象当进入或离开集合时的其所在索引数据。当您向 Realm 中添加对象或者从 Realm 中删除对象的时候会触发这个状态变化。 对于 RLMResults 也是适用的,当您适用特定的值进行匹配的时候,其中的对象就会发生变化,这样就可以知道某个对象现在是否匹配当前的这个查询。 对于基于 RLMArrayRLMLinkingObjects 以及 RLMResults 衍生出来的集合来说,当关系中的对象被添加或者删除的时候,也会触发这个状态变化。

当某个对象的属性发生变化的时候,您都能够获得到 修改 (modifications) 的通知,要注意此时这个对象仍然还位于集合当中。 当对一关系对多关系 发生变化时也会触发这个状态变化,但是对于 反向关系 来说则不会触发这个变化。

@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 对象;
  • 您修改了属于某个 PersonDogage 属性。

这使得

这使得在您的 UI 当中单独控制动画以及视觉效果更新成为了可能,而不是在每次通知发生的时候肆意重载各种东西。

- (void)viewDidLoad {
  [super viewDidLoad];

  // 观察 RLMResults 通知
  __weak typeof(self) weakSelf = self;
  self.notificationToken = [[Person objectsWhere:@"age > 5"] addNotificationBlock:^(RLMResults<Person *> *results, RLMCollectionChange *change, 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 stop];
  }

用户驱动更新

Realm 的通知被设计为可以响应模型层的所有更新,无论引起变化的线程或者进程是什么。

绝大多数应用拥有可以让用户同时直接修改 UI 和基础数据的结构。

虽然对于这一类应用来说,模型也有可能需要观察独立的变化,例如从服务器进行后台更新,这需要将模型反向反射回用户界面当中。

对于这种模式而言,最常见的例子就是在 UITableView 当中对单元格进行重排序了。 假设这里有一个基于 RLMResults 的 UITableViewDataSource,您通过一个集合通知闭包来观察变化。用户可以通过拖曳来重排序这些单元格。 UITableViewDataSource 会接收到已经展示在用户界面当中的变化。 相应的,您需要修改您的模型,然后将更改提交给 Realm。 这些写入事务会触发通知。 但是在集合通知闭包中重新应用这些更改会损坏表视图的次序。 您需要忽略多余的更新来避免这种情况的发生。 因为通知会在不能即时触发的时候聚合在一起,对此我们没有通用的解决方案来过滤这些您自己造成的更改。

为了解决这个问题,您应当手动并同步处理这些 UI 的更新,尤其是要注意:避免这些相同的变化在您的通知闭包中再次被触发。 有很多技术可以实现此功能,这取决于您可能会发生的变更类型、数据模型和用户交互的性质。 有一种方法是,在用户交互操作期间,保持某个写入事务一直开放,一定要牢记阻塞写入事务的性质是什么。 另一种模式是为您的模型对象添加一个标识,这样您就可以根据对象最近是否通过用户交互,或者通过诸如后台导入之类的孤立事务进行了改变,来设置或取消这个标识。

我们意识到这种设计有很大的局限性,这很具有挑战性,我们现在正在进行更多的工作来解决这个问题,以便给大家提供更优雅灵活的解决方案 (#3665)。

通知 APIs

关于通知的更多信息,请参阅下列 API 文档:

键值观察(Key-Value Observation, KVO)

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

观察 RLMObject 子类的某个未管理实例的属性的方法就如同观察其他 NSObject 子类 一样,不过要注意的是,当观察者(observer)存在的时候,您不能够使用诸如 [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机制的简要例子。

数据迁移(Migration)

当您使用任意一个数据库时,您随时都可能打算修改您的数据模型。 由于 Realm 的数据模型是以标准的 Objective‑C 类来定义的,这使得修改模型就像修改其他的 Objective‑C 类一样方便。 例如,假设我们有如下 Person 模型:

@interface Person : RLMObject
@property NSString *firstName;
@property NSString *lastName;
@property int age;
@end

假如我们想要更新数据模型,给它添加一个 fullname 属性, 而不是将“姓”和“名”分离开来。 为此我们只需要改变一下代码即可,范例如下:

@interface Person : RLMObject
@property NSString *fullName;
@property int age;
@end

在这个时候如果您在数据模型更新之前就已经保存了数据的话,那么 Realm 就会注意到代码和硬盘上数据不匹配。 每当这时,您必须进行数据迁移,否则当您试图打开这个文件的话 Realm 就会抛出错误。

注意在进行迁移的时候, default property values 并不适用于新的对象。我们认为这是一个 Bug,我们会在 #1793 对其保持关注。

进行迁移

定义迁移操作

通过设置 RLMRealmConfiguration.schemaVersion 以及 RLMRealmConfiguration.migrationBlock 可以定义一个迁移操作以及与之关联的架构版本。 迁移闭包将会提供提供相应的逻辑操作,以让数据模型从之前的架构转换到新的架构中来。 每当通过配置创建完一个 RLMRealm 之后,迁移闭包将会在迁移需要的时候,将给定的架构版本应用到更新 RLMRealm 操作中。

例如,假设我们想要把上面所声明 Person 数据模型进行迁移。如下所示是最简单的数据迁移的必需流程:

// 在 [AppDelegate didFinishLaunchingWithOptions:] 中进行配置

RLMRealmConfiguration *config = [RLMRealmConfiguration defaultConfiguration];
// 设置新的架构版本。这个版本号必须高于之前所用的版本号(如果您之前从未设置过架构版本,那么这个版本号设置为 0)
config.schemaVersion = 1;

// 设置闭包,这个闭包将会在打开低于上面所设置版本号的 Realm 数据库的时候被自动调用
config.migrationBlock = ^(RLMMigration *migration, uint64_t oldSchemaVersion) {
  // 目前我们还未进行数据迁移,因此 oldSchemaVersion == 0
  if (oldSchemaVersion < 1) {
    // 什么都不要做!Realm 会自行检测新增和需要移除的属性,然后自动更新硬盘上的数据库架构
  }
};

// 告诉 Realm 为默认的 Realm 数据库使用这个新的配置对象
[RLMRealmConfiguration setDefaultConfiguration:config];

// 现在我们已经告诉了 Realm 如何处理架构的变化,打开文件之后将会自动执行迁移
[RLMRealm defaultRealm];

我们最起码需要做的,是使用一个空的闭包来更新版本,以表明这个架构已经被 Realm 升级(自动)完毕。

值的更新

虽然这个迁移操作是最精简的了,但是我们需要让这个闭包能够自行计算新的属性(这里指的是 fullName),这样才有意义。 在迁移闭包中,我们能够调用[RLMMigration enumerateObjects:block:] 来枚举特定类型的每个 RLMObject 对象,然后执行必要的迁移逻辑。注意,对枚举中每个已存在的 RLMObject 实例来说,应该是通过访问 oldObject 对象进行访问,而更新之后的实例应该通过 newObject 进行访问:

// 在 [AppDelegate didFinishLaunchingWithOptions:] 中进行配置

RLMRealmConfiguration *config = [RLMRealmConfiguration defaultConfiguration];
config.schemaVersion = 1;
config.migrationBlock = ^(RLMMigration *migration, uint64_t oldSchemaVersion) {
  // 目前我们还未进行数据迁移,因此 oldSchemaVersion == 0
  if (oldSchemaVersion < 1) {
    // enumerateObjects:block: 方法遍历了存储在 Realm 文件中的每一个“Person”对象
    [migration enumerateObjects:Person.className
                          block:^(RLMObject *oldObject, RLMObject *newObject) {

      // 将名字进行合并,存放在 fullName 域中
      newObject[@"fullName"] = [NSString stringWithFormat:@"%@ %@",
                                         oldObject[@"firstName"],
                                         oldObject[@"lastName"]];
    }];
  }
};
[RLMRealmConfiguration setDefaultConfiguration:config];

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

属性重命名

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

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

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

下面是一个例子,展示了您该如何将 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];

添加更多版本

假如说现在我们有两个先前版本的 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 属性,但里面的内容却是 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.

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

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

// 打开加密文件
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 数据库了。 为了避免在测试中覆盖了应用数据或者泄露,您只需要为每项测试将默认的 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 方法,然后您想要对其进行测试,确保本地配置文件能够正常创建:

// 应用中使用的代码

@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,
                        @"用户信息未能从服务器正常更新");
}
@end

调试

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

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

Xcode截图

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

避免在测试目标中将 Realm 数据库和测试代码相链接

如果您以动态框架的方式运行 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. NSData 以及 NSString 属性不能保存超过 16 MB 大小的数据。如果要存储大量的数据,可通过将其分解为16MB 大小的块,或者直接存储在文件系统中,然后将文件路径存储在 Realm 中。如果您的应用试图存储一个大于 16MB 的单一属性,系统将在运行时抛出异常。
  4. 对字符串进行排序以及不区分大小写查询只支持“基础拉丁字符集”、“拉丁字符补充集”、“拉丁文扩展字符集 A” 以及”拉丁文扩展字符集 B“(UTF-8 的范围在 0~591 之间)。

线程

尽管 Realm 文件可以被多个线程同时访问,但是您不能跨线程处理 Realms、Realm 对象、查询和查询结果。有关 Realm 线程的更多知识,可以阅读线程的更多内容

Realm对象的 Setters & Getters 不能被重载

因为 Realm 在底层数据库中重写了 setters 和 getters 方法,所以您不可以在您的对象上再对其进行重写。 一个简单的替代方法就是:创建一个新的 Realm 忽略属性,该属性的访问起可以被重写, 并且可以调用其他的 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 没有线程/进程安全的自动增长属性机制,这在其他数据库中常常用来产生主键。然而,在绝大多数情况下,对于主键来说,我们需要的是一个唯一的、自动生成的值,因此没有必要使用顺序的、连续的、整数的 ID 作为主键。

在这种情况下,一个独一无二的字符串主键通常就能满足需求了。一个常见的模式是将默认的属性值设置为 [[NSUUID UUID] UUIDString] 以产生一个唯一的字符串 ID。

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

小技巧

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

Realm 移动端平台

Realm 移动端平台 (Mobile Platfom) 基于网络对现有的 Realm 移动端数据库进行了扩展,从而使跨设备间的自动数据同步成为了可能。为了实现这个功能,我们新增了一系列类型和类来支持这些 Realm 文件的同步操作;这些新增的内容将会被添加到既有的 Realm 移动端数据库当中,以下是其相关的介绍。

用户

Realm 用户(RLMSyncUser) 是 Realm 对象服务器 (Object Server) 当中的核心概念。如果要打开在服务器上的某个 Realm 数据库,那么首先您所需要做的就是以授权用户的身份进行登录。可以通过用户名/密码模式或者通过其他的认证方法来让 RLMSyncUser 能够通过认证,从而访问共享的 Realm 数据库。

创建用户并登录需要两个重要的参数:

  1. 您需要进行连接的 Realm 服务器 URL 地址
  2. RLMSyncCredentials,带有能唯一识别用户的认证信息(例如:用户名+密码、访问密钥等等)

创建服务器 URL

这是一个表示 Realm 服务器地址的 NSURL 对象。

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

欲了解更多内容,请参阅认证文档

认证

要了解我们支持哪些认证方式,请参阅 Realm 对象服务器认证文档

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

RLMSyncCredentials *usernameCredentials = [RLMSyncCredentials credentialsWithUsername:@"username"
                                                                             password:@"password"
                                                                              register:NO];
RLMSyncCredentials *googleCredentials = [RLMSyncCredentials credentialsWithGoogleToken:@"Google token"];
RLMSyncCredentials *facebookCredentials = [RLMSyncCredentials credentialsWithFacebookToken:@"Facebook token"];
RLMSyncCredentials *cloudKitCredentials = [RLMSyncCredentials credentialsWithCloudKitToken:@"CloudKit token"];

绝大多数证书只需要一个认证口令即可,这个认证口令是存放在您的应用当中的。

用户名/密码认证是独一无二的,因为它是完全由 Realm 服务器进行管理的,从而让您可以对用户进行全面管理和控制。此外它也比较特殊,因为它需要明确的 AuthenticationActions,从而实现注册、登录以及执行其他基于认证的操作。

用户认证

现在,我们已经拥有了所有必需的参数,现在用户就可以连接到 Realm 对象服务器,然后打开他们有权访问的 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] 来获取。

注销

如果用户想要退出他们的账户,那么您可以调用 -[RLMSyncUser logOut] 方法,这样当任何未决的本地更改与对象服务器完全同步之后,这些用户的所有本地数据都将被清除。

打开可同步的 Realm 数据库

您可以使用与本地 Realm 相同的配置对象和 Realm 构造器来打开可同步的 Realm 数据库。此外,如果要打开可同步的 Realm 数据库的话,那么必须要使用已认证的 RLMSyncUser 和 Realm URL 参数,然后对 syncConfiguration 属性进行配置。Realm URL 或许可能会包含波浪线 (~),它将用来表示用户的唯一标识符。这个模式使得您可以让不同的用户登录访问不同的 Realm 数据库。可同步的 Realm 数据库不能够配置 inMemoryIdentifier 以及 fileURL 属性。可同步的 Realm 数据库的缓存机制是完全由框架来进行管理的。

RLMRealmConfiguration *config = [RLMRealmConfiguration defaultConfiguration]; config.syncConfiguration = [[RLMSyncConfiguration alloc] initWithUser:user realmURL:syncServerURL];; RLMRealm *realm = [RLMRealm realmWithConfiguration:config error:nil]; // Any changes made to this Realm will be synced across all devices!

日志

Realm 支持多种日志级别,我们可以通过同步管理器 (Sync Manager) 来进行配置选择,如下所示:

[[RLMSyncManager sharedManager] setLogLevel:RLMSyncLogLevelOff];

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

错误报告

大多数的 Realm API 支持完成闭包,从而可以在本地(API 调用的位置)进行错误的捕获,此外也可以声明一个全局的错误处理,如下所示:

[[RLMSyncManager sharedManager] setErrorHandler:^(NSError *error, RLMSyncSession *session){
  // handle error
}];

迁移

可同步的 Realm 数据库支持自动迁移,但是当数据库架构发生更改时,还需要将架构版本号进行递增。目前,仅支持增量更改,例如新增类或者新增属性。自定义的迁移闭包将会被忽略。

冲突处理

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

问答时间

Realm 库文件有多大?

一旦您的应用以发布模式编译完成后, Realm 的库文件应该只有1 MB 左右的大小。 我们发布的那个可能有点大,这时因为它们还包含了对 iOS、watchOS 以及 tvOS 模拟器的支持库、某些调试符号以及某些当编译应用时会被 Xcode 自动排除的中间代码。

Realm 开源了吗?

没错!Realm 内部 C++ 存储引擎以及其不同语言封装的 SDK 现已开源,并且基于 Apache 2.0 协议。

不过,Realm 还包含了一个闭源的同步组件,但是如果您将 Realm 作为嵌入式数据库使用的话,这个组件是不必要的。

在我运行应用的时候发现了一个名为 Mixpanel 的网络调用,这是什么?

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

疑难解答

崩溃报告

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

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

报告 Realm 错误

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

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

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

通过依赖管理器重新安装

如果您曾经通过 CocoaPods 或者 Carthage 安装过 Realm,并且遇到了编译错误的话,那么很可能是您使用了该依赖管理器不支持的版本,也可能是 Realm 没有成功整合到项目当中,还可能是您工具链中的某个应用的更新版本(比如说 Xcode)可能改变了安装配置。

如果这样的话,请尝试删除依赖管理器所创建的这些文件夹,并重新安装:

CocoaPods

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

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

pod cache clean Realm
pod cache clean RealmSwift
pod deintegrate || rm -rf Pods
pod install --verbose

或者,您可能需要考虑安装并运行 [cocoapods-deintegrate] (https://github.com/CocoaPods/cocoapods-deintegrate) 来代替,这也可以从您的 Xcode 项目配置中移除 CocoaPods。

您也可以尝试删除导出数据 (derived data),以及清除 Xcode 当中的 build 文件夹;我们的一部分用户报告说,这种做法解决了他们在使用 CocoaPods 当中碰到的问题。要清楚 build 文件夹,您可以在打开 Product 菜单的同时按住 Option 键,接着选择 “Clean Build Folder…”。您同样也可以在 Xcode 帮助搜索栏当中输入 “Clean”,然后当其出现在搜索结果当中的时候,选择 “Clean Build Floder…” 菜单项。

Carthage

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

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

rm -rf Carthage
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,稍后再试。