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

如果你只想使用Swift版本的程序,那么请安装Realm的Swfit版本。 注意:不能同时使用Objective‑C版本和Swift版本的 Realm。

从这里开始

Download Realm Objective‑C or see the source on GitHub.

准备工作

  • 使用 Realm 构建 Apps 的基本要求:iOS >= 7, OS X >= 10.9 & WatchKit.
  • 需要Xcode >= 6.
  • 程序支持Objective‑C, Swift 1.2 & Swift 2.0。

安装

  1. 下载最新的Realm发行版本并解压软件。
  2. 从’ios/static’目录中拖拽’Realm.framework’到你的 Xcode 的文件导航栏中,选中Copy items if needed选项并点击Finish按钮。
  3. 在Xcode file explorer中选中你要的开发项目。选择target,点击 Build Phases 选项. 在Link Binary with Libraries里按+, 添加libc++.dylib
  4. 如果使用Swift加载Realm,请拖拽Swift/RLMSupport.swift到 Xcode 工程的文件导航栏中并选中Copy items if needed
  1. 下载最新的Realm发行版本,并解压 zip 文件。
  2. 前往你 Xcode 工程的”General”设置项中,从’ios/dynamic/’或者’osx/’中拖拽’Realm.framework’到”Embedded Binaries”选项中。确认Copy items if needed被选中后,点击Finish按钮。
  3. 在你测试目标的”Build Settings”中,添加Realm.framework的上级目录到”Framework Search Paths”中。
  4. 如果希望使用 Swift 加载 Realm,请拖动Swift/RLMSupport.swift文件到 Xcode 工程的文件导航栏中并选中Copy items if needed
  5. 如果在 iOS8 的项目中使用 Realm,请在你 App 目标的”Build Phases”中,创建一个新的”Run Script Phase”,并将bash "${BUILT_PRODUCTS_DIR}/${FRAMEWORKS_FOLDER_PATH}/Realm.framework/strip-frameworks.sh"复制到文本框中。 因为要绕过APP商店提交的bug,这一步在打包二进制发布版本时是必须的。

注意: 动态frameworks在OS X 或者 iOS 8和以上版本中被支持。

  1. 安装CocoaPods 0.37.1 或者更高版本([sudo] gem install cocoapods)。
  2. 在你的Podfile中,添加pod 'Realm'到你的 app 目标中,添加pod 'Realm/Headers'到你的测试目标中。
  3. 在终端运行pod install
  4. 采用 CocoaPods 生成的.xcworkspace来运行工程!

点击CocoaPods网站来查询相关内容。

  1. 添加github "realm/realm-cocoa"到你的Cartfile。
  2. 运行carthage update.
  3. iOS:Carthage/Build/iOS/文件夹中拖拽Realm.framework 到你 Xcode 工程”General”的”Linked Frameworks and Libraries”选项卡中。 OS X:Carthage/Build/Mac/文件夹中拖拽 Realm.framework到你 Xcode 工程”General”的”Embedded Binaries”选项卡中。
  4. iOS: 在你APP targets的“Build Phases”设置选项卡,点击“+”图标并选择“New Run Script Phase”。在新建的Run Script中,填写:

    /usr/local/bin/carthage copy-frameworks

    在“Input Files”内添加frameworks路径,例如:

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

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

点击Carthage工程查看更多内容。

Realm浏览器/数据库管理器

我们也提供一个独立Mac app 对.realm数据库进行读取和编辑。它位于browser/下的release zip 中。

你可以使用菜单中的Tools(工具) > Generate demo database(生成演示数据库)中加载一个有样本数据的测试数据库。

如果你需要寻找你程序的Realm文件,请查看StackOverflow上的这个答案

你可以从Mac App Stroe安装Realm Browser。

Xcode插件

我们的Xcode插件使新建 Realm 模型更加方便。

安装 Realm 插件的最简单方式是通过点击”RealmPlugin”文件夹下的Alcatraz。你也可以手动进行安装:打开release zip 中的plugin/RealmPlugin.xcodeproj并进行编译。重启 Xcode后生效。如果你使用 Xcode 菜单来建立一个新文件(File > New > File… — or ⌘N) ,你就可以看到有一个新建Realm模型(create a new realm model)的选项。

API手册

你能查询我们的完整版API手册,里面包含所有类和方法等信息。

示例

你可以在examples/目录release zip 下查看iOS和OS X版本的示例。它们演示了Realm得很多功能和特性,例如数据库迁移,如何与UITableViewController’s一起使用,加密等等。

获得帮助

数据模型(model)

Realm数据模型是基于传统特征类Objective‑C来进行定义的。 可通过简单的RLMObject的子类或者一个已经存在的模型类,你就能轻松创建一个Realm的数据模型对象(data model object)。 Realm模型类函数与其他Objective‑C对象相同 - 你可以给它们添加你自己的方法(method)和协议(protocol)然后和其他的对象一样使用。 唯一的限制就是从它们被创建开始,只能在一个进程中被使用。

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

你只需要为对象列表添加目标类型的属性(property)或者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> *dogs;
@end
RLM_ARRAY_TYPE(Person) // 定义RLMArray<Person>

// 实现
@implementation Dog
@end // 没有需要

@implementation Person
@end // 没有需要

所有模型在代码启动时就已经被定义,所以即使全程没有调用,它们也会被赋值。

通过RLMObject 可查看更多细节。

属性(property)种类

Realm支持以下的属性(property)种类:BOOL, bool, int, NSInteger, long, long long, float, double, CGFloat, NSString, NSDate 精度为秒, 和NSData.

你可以使用RLMArray\<_Object_\>RLMObject来模拟对一或对多的关系——Realm也支持RLMObject继承。

属性(property)特性(attributes)

注意由于 Realm 在自己的引擎内部有很好的语义解释系统,所以 Realm 忽略了Objective‑C的许多特性,如nonatomic, atomic, strong, copyweak等。 ,为了避免误解,我们推荐你在写入模型的时候不要使用任何的property attributes。当然,如果你已经设置了,这些attributes会一直生效直到通过RLMObject被写入realm数据库。无论RLMObject是否在 realm 中,你为getter和setter自定义的名字都能正常工作。

数据模型定制

许多类的方法都支持深度定制:

  • +indexedProperties可以通过重写来设定特定属性(property)的属性值(attrbutes)。例如某个属性值要添加索引。 ```objective-c @interface Book : RLMObject @property float price; @property NSString *title; @end

@implementation Book

  • (NSArray *)indexedProperties { return @[@”title”]; } @end ```

  • +defaultPropertyValues 能被重载来实现,在对象被创建时来创建默认值。因为 Swift 已经提供一种通过属性定义来实现设定默认属性值的方法,所以你应使用这个方法来代替defaultPropertyValues()

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

@implementation Book
+ (NSDictionary *)defaultPropertyValues {
    return @{@"price" : @0, @"title": @""};
}
@end
  • +primaryKey 能通过重载来设置模型主键。通过声明主键,用户可以查询数据并可高效更新。程序会强制要求主键具有唯一性。
@interface Person : RLMObject
@property NSInteger id;
@property NSString *name;
@end

@implementation Person
+ (NSString *)primaryKey {
    return @"id";
}
@end
@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

存储对象

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

Rrealm的对象可以被实例化并且被单独使用,和其他常规Objective‑C对象无异。 如果你想要在多个线程中共享或者永久保存以重复使用对象,你必须将其存储到Realm数据库中——这个操作必须在写事务中完成。

添加对象

向 Realm 中添加对象的步骤如下:

// 创建对象
Person *author = [[Person alloc] init];
author.name    = @"David Foster Wallace";

// 获取默认的 Realm
RLMRealm *realm = [RLMRealm defaultRealm];
// You only need to do this once (per thread)

// 通过处理添加数据到 Realm 中
[realm beginWriteTransaction];
[realm addObject:author];
[realm commitWriteTransaction];

等到你把这个对象添加到realm数据库里面之后, 你可以在多个线程里面共享他们。并且从现在开始,你所做的每一次更改(必须在一个写事务中完成)也会被永久储存。等到写事务完成,这个更改将对所有共享这个Realm数据库的线程可见。

这类似其他永久性解决方案,我们建议你在一个独立的进程中进行写入。

因为 Realm 采用的是 MVCC 框架,读取并 不会 因为一个进行中的写事务而受到影响。除非你需要多个线程同时写入数据,不然你应该采用批量化的写入模式而不是多次少量的写入方式。

查看RLMRealmRLMObject来获得更多内容。

更新数据

我们可以通过写入来更新对象数据。

// 更新对象数据
[realm beginWriteTransaction];
author.name = @"Thomas Pynchon";
[realm commitWriteTransaction];

如果你为你的模型设置了主键,你可以使用+[RLMObject createOrUpdateInRealm:withValue:]来更新对象或者在对象不存在时插入新的对象。

// 创建一个已存在的主键的book对象。
previously saved book
Book *cheeseBook = [[Book alloc] init];
cheeseBook.title = @"Cheese recipes";
cheeseBook.price = @9000;
cheeseBook.id = @1;

// 更新 book 的 id 为1
[realm beginWriteTransaction];
[Book createOrUpdateInRealm:realm withValue:cheeseBook];
[realm commitWriteTransaction];

如果 book 的主键(id)的值不为1,那么系统会新建一个 book 对象插入数据库。

删除对象

通过写入处理的-[RLMRealm deleteObject:],我们能够删除数据库中的对象。

Book *cheeseBook = ... // 存储在 Realm 的 Book

// 删除对象
[realm beginWriteTransaction];
[realm deleteObject:cheeseBook];
[realm commitWriteTransaction];

你也能删除 Realm 下的所有对象。注意,在删除数据后,Realm文件的大小不会改变。通过保留空间以便于日后快速存储数据。

// 删除 realm 下的所有数据
[realm beginWriteTransaction];
[realm deleteAllObjects];
[realm commitWriteTransaction];

查询

通过查询,系统会返回包含一连串对象的RLMResults 。RLMResults 的接口与NSArray十分相似,存储在RLMResults 的对象可通过序号进行索引。不像NSArray的是,RLMResults一样需要被赋予类型且存储的对象属于一个子类类型。

所有的查询(包括查询和熟性访问)都是在 Realm 可用的。数据访问时,数据仅能进行读取。

数据的查询结果不是对你的数据进行拷贝:修改查询结果(如使用写入操作)将直接改动硬盘上的数据。同样,你能直接从包含于RLMResults的RLMObjects中遍历所有映射

通过类型检索对象

从 Realm 中检索对象的最基本方法是+[RLMObject allObjects]。 它将返回 Realm 中满足子类的所有结果。

// 查询默认的 Realm
RLMResults *dogs = [Dog allObjects]; // 从默认 Realm 中,检索所有狗

// 查询指定的 Realm
RLMRealm *petsRealm = [RLMRealm realmWithPath:@"pets.realm"]; // 获得一个特别的 Realm
RLMResults *otherDogs = [Dog allObjectsInRealm:petsRealm]; // 从那个 Realm 中,检索所有狗

谓词/条件查询

如果你熟悉NSPredicate,那么你就很容易掌握 Realm 的查询方法。RLMObjects, RLMRealm, RLMArray, and RLMResults 所有的查询方法都允许使用简单的NSPredicate语句,你只需要传递相应地NSPredicate实例,谓词字符串,谓词格式字符串,就可以获取你想要的NSArray实例啦。就和NSArray一样的。

例如,下面的代码就展示了如何调用[RLMObject objectsWhere:]来在 Realm 数据库中的所有颜色是黄褐色的,名字开头是“B”的狗的实例:

// 使用条件字符串查询
RLMResults *tanDogs = [Dog objectsWhere:@"color = 'tan' AND name BEGINSWITH 'B'"];

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

查看苹果的谓词编程指导手册来获取更多关于使用谓词/条件查询和NSPredicate Cheatsheet的信息。 Realm支持许多常见谓词/条件:

  • 在比较中, 操作数可以是属性名或者常量。但是其中至少有一个是属性名。
  • 比较操作符==, <=, <, >=, >, !=, and BETWEEN支持int, long, long long, float, double, and NSDate。例如age == 45
  • ==, !=, e.g. [Employee objectsWhere:@”company == %@”, company]
  • 布尔属性支持 == and !=
  • 对于NSString和NSData, 我们支持的操作符有 ==, !=, BEGINSWITH, CONTAINS, 和 ENDSWITH operators,例如name CONTAINS ‘Ja’
  • string支持模糊查询, 例如name CONTAINS[c] ‘Ja’。注意其中字符大小写不做区分。
  • Realm 还支持如下的复合型操作符: “AND”, “OR”, and “NOT”。例如 name BEGINSWITH ‘J’ AND age >= 32
  • 包含操作符 IN 例如 name IN {‘Lisa’, ‘Spike’, ‘Hachi’}

  • ==, !=支持与 Nil 比较,例如 [Company objectsWhere:@"ceo == nil"]。注意这只用于有映射关系的对象,例如,CEO 是公司模型的属性。
  • ANY比较, 例如ANY student.age < 21
  • 注意,我们虽然不支持复合表达式(aggregate expression type),但是我们支持BETWEEN操作符,例如:[Person objectsWhere:@"age BETWEEN %@", @[42, 43]]

详情请点击[RLMObject objectsWhere:]

条件排序

RLMResults 允许你指定一个排序要求并且根据一个或多个属性进行排序。例如,下列代码调用排序功能并把结果按狗的名称升序排列:

// 通过名称查询名称首字母为“B”的狗
RLMResults *sortedDogs = [[Dog objectsWhere:@"color = 'tan' AND name BEGINSWITH 'B'"]
                               sortedResultsUsingProperty:@"name" ascending:YES];

更多信息请查询[RLMObject objectsWhere:] and [RLMResults sortedResultsUsingProperty:ascending:]

###链式查询

Realm查询引擎的一个独特属性就是它能够进行简单快捷的链式查询,而不需要像传统数据库一样的麻烦。

例如,如果你要所有黄褐色的小狗的结果序列,然后从中查找名字开头是“B“的小狗。 你可以通过以下两个操作的来完成链式查询:

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

Realm数据库

默认的realm数据库

你可能已经发现我们总是通过[RLMRealm defaultRealm]来对 Realm 进行初始化。这个方法会返回RLMRealm对象,并指向到你 app 的Documents文件夹下的“default.realm”文件。

你可以使用+[RLMRealm setDefaultRealmPath:]来自定义默认 realm 数据库。这步操作常用于来测试 app 或者在iOS 8 Shared Container的多个 app 间共享数据库。

许多 Realm API 的方法都支持两种输入的方式,一种接受RLMRealm 输入,另一种会自动调用默认 realm 数据的简便方法。例如 [RLMObject allObjects]就等同于 [RLMObject allObjectsInRealm:[RLMRealm defaultRealm]].

其他的realm数据库

有的时候,在不同位置拥有多个分离的数据库是十分有用的。例如,如果你需要将数据绑定到一个App中,你需要打开一个只读的 Realm。点击[RLMRealm realmWithPath:] and [RLMRealm realmWithPath:readOnly:error:]来查询更多信息。

请注意初始化 Realm 的第三方路径需要有写入权限。常用存储 Realm 的文件夹是iOS的“Documents”和OSX的“Application Support”。具体情况,请遵循Apple’s iOS Data Storage Guidelines, 它推荐将文件存储在<Application_Home>/Library/Caches目录下。

内存数据库

通常情况下,Realm 数据库是存储在硬盘中的,但是你能够使用[RLMRealm inMemoryRealmWithIdentifier:]创建一个纯内存数据库:

RLMRealm *realm = [RLMRealm inMemoryRealmWithIdentifier:@"MyInMemoryRealm"];

内存数据库在每次程序启动时不会保存数据。但是,这不会妨碍到realm的其他功能,包括请求,关系和线程安全。假如你需要灵活的数据读写但又不想永久储存,那么纯内存数据库对你来说一定是一个不错的选择。

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

跨线程使用数据库

在不同的线程间使用同一个Realm数据库,你需要为 app 的不同线程初始化相应的Realm。只要他们路径是一样的,那么他们就会调用相同的文件。

我们还_不支持_跨线程共享Realm 相同 realm 文件的初始化需要有相同的readOnly 属性(或者readwrite或者readonly).

在一个独立流程中,一次性写入大量数据是一个高效的方式。它可能使用Grand Central Dispatch在后台完成写入操作。RLMRealm对象使用线程并不安全,也不能在线程中共享数据,你必须在你想读取或者写入的每个thread/dispatch_queue中获取一个 Realm 实例。下面例子展示如何在后台插入一百万个数据:

dispatch_async(queue, ^{
  @autoreleasepool {
    // 在这个线程中获取realm和table
    RLMRealm *realm = [RLMRealm defaultRealm];

    // 先开启写入操作,然后将写入操作分成多个微小操作
    for (NSInteger idx1 = 0; idx1 < 1000; idx1++) {
      [realm beginWriteTransaction];

      // 通过字典插入排数据,其中属性属性忽略不记
      for (NSInteger idx2 = 0; idx2 < 1000; idx2++) {
        [Person createInRealm:realm
                    withValue:@{@"name"      : [self randomString],
                                @"birthdate" : [self randomDate]}];
      }

      // 关闭写入流程来确保数据在其他线程可用
      [realm commitWriteTransaction];
    }
  }
});

在Realm数据库间拷贝数据

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

查询 Realm 文件

如果你需要寻找 app的 Realm 文件,那么请查看StackOverflow答案 for detailed instructions.

在 app 中建立 Realm 数据库

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

  1. 第一步定位 realm。你应该使用与最终发售版相同的数据模型来创建 realm 并绑定数据到你的 app 中。因为 realm 文件是跨平台,所以你能够在模拟器中测试你的OS X app (查看你的JSONImport example)或者你的 iOS app。
  2. 在加载 realm 文件的代码处,你需要在程序最后对文件进行拷贝(就是-[RLMRealm writeCopyToPath:error:])。这有助于减少数据库体积,使你发布的 app 体积更小。
  3. 拖拽你的最终的 realm 文件的拷贝到你的最终 app 的Xcode工程导航栏中。
  4. 前往你的 appp 的Xcode编译选项卡,添加 realm 文件到”Copy Bundle Resources”。
  5. 这样你就能够在你的 app 中更加便捷的使用数据库。你能通过使用[[NSBundle mainBundle] pathForResource:ofType:]来得到数据库路径。
  6. 你也能通过调用[RLMRealm realmWithPath:readOnly:error:]来创建一个只读 realm。或者,你想基于初始化数据来创建一个可写入的 realm 文件,那么你可以使用[[NSFileManager defaultManager] copyItemAtPath:toPath:error:] 把绑定文件拷贝到 app 的 Documents 文件夹下。然后使用[RLMRealm realmWithPath:]来构建 realm 数据库。

你能够参考我们的迁移例程app来学习如何使用绑定的 realm 文件。

映射关系

通过使用RLMObject和RLMArray来连接各个RLMObject。RLMArray与NSArray有详细的接口,包含在RLMArray 的对象能通过索引进行访问。不像NSArray,RLMArray需要赋予属性,并且要保证加载的RLMObject有这单一的属性。更多信息请查看RLMArray

假如说你预先定义了一个”人“模型 (如下),我们再来创建一个”狗“模型:

// 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非常相似的接口。

为了向已经包含多个“狗(Dog)“的”人(Person)”模型添加一个“狗”属性,我们必须定义一个RLMArray<Dog>类型。这可以通过在相应的模型接口中添加一个宏来实现:

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

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

然后你可以定义一个类型为RLMArray的属性:

// Person.h
@interface Person : RLMObject
... // 其他属性声明
@property RLMArray<Dog> *dogs;
@end

你可以可以像往常一样对 RLMArray进行读取和赋值:

// Jim是 Rex 的主任,所有狗的名字叫做"Fido"
RLMArray *someDogs = [Dog objectsWhere:@"name contains 'Fido'"];
[jim.dogs addObjects:someDogs];
[jim.dogs addObject:rex];

反向关系

通过反向关系(又称 backlink),你可以通过一个特定的属性获得所有链接到一个给定对象的对象。例如,通过对‘狗’调用-linkingObjectsOfClass:forProperty:就能够返回所有连接到调用目标的特性类型的对象:

@interface Dog : RLMObject
@property NSString *name;
@property NSInteger age;
@property (readonly) NSArray *owners; // Realm doesn't persist this property because it is readonly
@end

@implementation Dog
//通过 Person.dogs的逆向关系来定义“主人”
- (NSArray *)owners {
    return [self linkingObjectsOfClass:@"Person" forProperty:@"dogs"];
}
@end

通知

每当一次写事务完成Realm实例都会向其他线程上的实例发出通知,可以通过注册一个block来响应通知:

// 监视Realm通知
self.token = [realm addNotificationBlock:^(NSString *note, RLMRealm * realm) {
    [myViewController updateUI];
}];

只要有任何的引用指向这个返回的notification token,它就会保持激活状态。在这个注册更新的类里,你需要有一个强引用来钳制这个token, 因为一旦notification token被释放,通知也会自动解除注册。

点击[RLMRealm addNotificationBlock:] and [RLMRealm removeNotificationBlock:] 查看更多信息。

迁移

当你使用任意一个数据库时,你都可能希望修改你的数据模型。因为Realm 的数据模型有着标准Objective‑C的接口,使得修改模型就像修改其他Objective‑C接口一样方便。例如,假设我们有如下‘人’模型:

@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就会注意到代码和硬盘数据不匹配。每当这时,你必须对数据构架进行迁移,否则就会抛出错误。

进行迁移

通过调用+[RLMRealm setSchemaVersion:forRealmAtPath:withMigrationBlock:]可以进行自定义数据迁移以及构架相应版本。数据迁移模块将会为你提供相应地逻辑操作来用于更新数据构架。调用+[RLMRealm setSchemaVersion:forRealmAtPath:withMigrationBlock:]之后, 任何需要迁移的Realm数据库都会自动使用指定的迁移模块并且更新到相应地版本。

例如,假设我们想要把之前‘人’的子类迁移,如下所示是最简化的数据迁移流程:

// 插入到[AppDelegate didFinishLaunchingWithOptions:]

// Notice setSchemaVersion设置为1,这通常需要手动设置。它必须比之前版本的高,否则就会抛除一个RLMException
[RLMRealm setSchemaVersion:1
            forRealmAtPath:[RLMRealm defaultRealmPath]
        withMigrationBlock:^(RLMMigration *migration, uint64_t oldSchemaVersion) {
  // 因为我们没有进行过数据迁移,所以oldSchemaVersion == 0
  if (oldSchemaVersion < 1) {
    // 什么也不要做,Realm会自动监测新属性和就属性,并自动完成更新。
  }
}];

// 现在能调用`setSchemaVersion:withMigrationBlock:`来打开老数据。
// Realm 自动完成数据更新,并成功打开数据库。
[RLMRealm defaultRealm];

对于版本更新,人们所需要做的就是留出一个空模块,Realm通过这个最简短的程序可以自动完成更新。

虽然这是系统能接受的最简化的迁移,我们应当用有意义的代码来添加新的属性(这里指“fullname”)。在数据迁移模块中,我们可以调用[RLMMigration enumerateObjects:block:]来列举Realm中每一特定文件,并执行必要的迁移判定。 注意遍历RLMObject需要使用 oldObject ,而更新是使用newObject

//插入到你的AppDelegate didFinishLaunchingWithOptions:]

[RLMRealm setSchemaVersion:1
            forRealmAtPath:[RLMRealm defaultRealmPath]
        withMigrationBlock:^(RLMMigration *migration, uint64_t oldSchemaVersion) {
  // 因为我们没有进行过数据迁移,所以oldSchemaVersion == 0
  if (oldSchemaVersion < 1) {
    // 迭代方法enumerateObjects:block
    // 遍历 Realm 文件中的每一个'Person'对象
    [migration enumerateObjects:Person.className
                          block:^(RLMObject *oldObject, RLMObject *newObject) {

      // 将字母合并到一个字符串中
      newObject[@"fullName"] = [NSString stringWithFormat:@"%@ %@",
                                         oldObject[@"firstName"],
                                         oldObject[@"lastName"]];
    }];
  }
}];

一旦迁移成功结束,Realm和其所有文件都可被你的app正常存取。

添加更多的版本

假如说现在我们有两个的之前版本:

// 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

我们的迁移模块里面的逻辑大致如下:

[RLMRealm setSchemaVersion:2 forRealmAtPath:[RLMRealm defaultRealmPath]
                         withMigrationBlock:^(RLMMigration *migration,
                                              uint64_t oldSchemaVersion) {
    // 迭代方法enumerateObjects:block
    // 遍历 Realm 文件中的每一个'Person'对象
  [migration enumerateObjects:Person.className
                        block:^(RLMObject *oldObject, RLMObject *newObject) {
    // 添加'fullName'属性到版本0中
    if (oldSchemaVersion < 1) {
      newObject[@"fullName"] = [NSString stringWithFormat:@"%@ %@",
                                oldObject[@"firstName"],
                                oldObject[@"lastName"]];
    }

    // 添加'email'属性到版本0或版本1中
    if (oldSchemaVersion < 2) {
      newObject[@"email"] = @"";
    }
  }];
}];

// 现在能调用`setSchemaVersion:withMigrationBlock:`来打开老数据。
// Realm 自动完成数据更新,并成功打开数据库。

[RLMRealm defaultRealm];

想要了解更多关于数据库迁移的信息,请点击迁移例程app

线性迁移(Linear Migrations)

假如说,我们的app有两个用户: JP和Tim. JP经常更新app,但Tim却经常跳过。 所以JP可能下载过这个app的每一个版本,并且一步一步的跟着更新构架:他下载第一次更新,从v0到v1, 第二次更新从v1到v2,以此类推,井然有序。相反,Tim很有可能直接从v0跳到了v2。 所以,你应该使用非嵌套的 if (oldSchemaVersion < X)结构来构造你的数据库迁移模块,以确保不管他们是在使用哪个版本的构架,都能看见所有的更新。

当你的用户不按规则出牌,跳过有些更新版本的时候,另一种情况也会发生。 假如你在v2里删掉了一个“email”属性,然后在v3里又把它重新引进了。假如有个用户从v1直接跳到v3,那Realm不会自动检测到v2的这个删除操作因为存储的数据构架和代码中的构架吻合。这会导致Tim的Person对象有一个v3的email property,但里面的内容却是v1的。这个看起来没什么大问题,但是假如两者的内部存储类型不同(比如说: 从ISO email representation 变成了自定义),那麻烦就大了。为了避免这种不必要的麻烦,我们推荐你在if (oldSchemaVersion < 3)中,nil out所有的email property。

加密

Please take note of the Export Compliance section of our LICENSE, as it places restrictions against the usage of Realm if you are located in countries with an export restriction or embargo from the United States.

在iOS平台,通过调用NSFileProtection的 API 可以使用很少的资源就能完成 Realm 的文件加密。如果这样做要注意两点:1)这种Realm文件不能跨平台(因为NSFileProtection只有 iOS 有),2)Realm文件不能在没有密码保护的 iOS 设备中进行编码。为了避免这些(或者你想构建一个 OS X 的 app),你需要使用Realm级别的加密。

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

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

// 打开加密文件
NSError *error;
RLMRealm *realm = [RLMRealm realmWithPath:RLMRealm.defaultRealmPath
                                      key:key
                                 readOnly:NO
                                    error:&error];
if (!realm) {
    // 如果密钥错误,`error` 会提示an invalid database
    NSLog(@"Error opening realm: %@", error);
    return;
}

// 像正常情况一样使用 Realm
RLMResults *dogs = [[Dog objectsInRealm:realm where:@"name contains 'Fido'"]];

这样硬盘上的数据都能都采用AES-256来进行加密和解密,并用 SHA-2 HMAC 来进行核实。每次你要获得一个 Realm 实例时,你都需要提供一次密钥。Realm 也会在内存中存储你的密钥,这样再打开相同路径下的 Realm ,密钥就会被自动调用。例如,为默认 Realm 设置密钥(便捷方式):

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

// 为默认 Realm 设置默认密钥
[RLMRealm setEncryptionKey:key forRealmsAtPath:RLMRealm.defaultRealmPath];

// 像正常情况一样使用 Realm
RLMResults *dogs = [[Dog objectsWhere:@"name contains 'Fido'"]];

加密例程app展示了一个产生密钥,安全存储和解密的 app 例程。

加密 Realm 只占用很少的资源(通常只会慢10%)。

请注意,如果你使用第三方的崩溃记录工具(crashlytics,plcrashreporter等)应在打开加密 Realm 前完成注册,不然你得到的数据将都是错误的。

调试

调试你的 Realm app是十分简单的,你可用通过Realm浏览器来实时查看你 app 的数据。

我们的Xcode 插件带有 LLDB 脚本,可以Xcode UI 来检查检查的RLMObjects, RLMResults 和 RLMArrays对象,而不是只是简单的显示nil0:

Xcode截图

注意: 这一功能只支持Objective‑C。Swift支持扔在计划中。

调试加密的Realm

附带的 LLDB 可用于查看加密的 Realm现在还不被支持。当你需要调试时,你可以在你的环境中设置REALM_DISABLE_ENCRYPTION=YES来禁用加密,这可以将加密的 Realm 转化为一个未加密的 Realm。

测试

测试Realm App

重载默认 Realm 的路径

最简单的使用和测试 Realm app 的方法就是使用默认realm。为了避免在测试中重载 app 数据,需要在测试中设置为默认路径(+[RLMRealm setDefaultRealmPath:])。

Realm 注入

另一种测试 realm 相关代码的方式是加载你想测试RLMRealm的所有方法。这样你就能在运行 app 和测试 app 时,查看这些方法是否可行。例如,假如你 app 使用了一种通过 JSON API 来获得用户的配置文件的方法,那么你肯定想测试本地配置文件是否被正确的创建:

// APP 编码
+ (void)updateUserFromServer
{
  NSURL *url = [NSURL URLWithString:@"http://myapi.example.com/user"];
  [[[NSURLSession sharedSession] dataTaskWithURL:url
                               completionHandler:^(NSData *data,
                                                   NSURLResponse *response,
                                                   NSError *error) {
    [self createOrUpdateUserInRealm:[RLMRealm defaultRealm] withData:data];
  }] resume];
}

+ (void)createOrUpdateUserInRealm:(RLMRealm *)realm withData:(NSData *)data
{
  id object = [NSJSONSerialization JSONObjectWithData:data options:nil error:nil];
  [realm transactionWithBlock:^{
    [User createOrUpdateInRealm:realm withValue:object];
  }];
}

// 测试编码
- (void)testThatUserIsUpdatedFromServer
{
  RLMRealm *testRealm = [RLMRealm realmWithPath:kTestRealmPath];
  NSData *jsonData = [@"{\"email\": \"help@realm.io\"}"
                      dataUsingEncoding:NSUTF8StringEncoding];
  [ClassBeingTested createOrUpdateUserInRealm:testRealm withData:jsonData];
  User *expectedUser = [User new];
  expectedUser.email = @"help@realm.io";
  XCTAssertEqualObjects([User allObjectsInRealm:testRealm][0],
                        expectedUser,
                        @"User was not properly updated from server.");
}

避免在测试目标中连接 Realm 和你的测试代码

如果 Realm 是一个动态框架,你需要确认你的测试单元能否正确的找到 Realm。你需要去添加Realm.framework的上级目录到你的测试单元的”Framework Search Paths”中。

如果你的测试失败,并提示"Object type '...' not persisted in Realm",这很有可能是因为直接将你的测试目标连接到你的 Realm 框架。你应该从你测试目标中移除 Realm。

你也应该确保你模型类文件只连接到你的应用或框架目标中,而不是你的测试目标中。否则,这些类将会在测试时被复制,这将为调试带来麻烦(see https://github.com/realm/realm-cocoa/issues/1350 for details)。

你也要保证所有的测试代码在你测试目标中(对于 Swift 需要使用‘public’ ACL)

重置状态

使测试单元彼此隔离是十分重要的。出于这个目的,我们建议你在硬盘删除 realm 文件并在每次测试前重置 Realm 的内部状态。这可以在使用 XCTest 时,调用setUptearDown来完成:

// 帮助

static NSString *RealmPathForTesting = @"";

static void RLMDeleteRealmFilesAtPath(NSString *path) {
  NSFileManager *fileManager = [NSFileManager defaultManager];
  [fileManager removeItemAtPath:path error:nil];
  NSString *lockPath = [path stringByAppendingString:@".lock"];
  [fileManager removeItemAtPath:lockPath error:nil];
}

// 在XCTestCase子类里:

- (void)setUp {
  [super setUp];
  RLMDeleteRealmFilesAtPath(RealmPathForTesting);
  // Optionally override the default realm path.
  [RLMRealm setDefaultRealmPath:RealmPathForTesting];
}

- (void)tearDown {
  [super tearDown];
  RLMDeleteRealmFilesAtPath(RealmPathForTesting);
}

测试 Realm

Realm有一套完整的测试套件,它能测试每一条语句,所以你的 app 的测试不应只测试 Realm 本身,还要测试 Realm 的使用情况。 因为 Realm 是开源的,我们很乐意接受绑定、测试和文档相关的帮助。

REST APIs

Realm轻松的整合了REST API, 这使得Realm在几个方面胜过了无本地缓存的REST API:

  • Realm缓存数据使得你能提供离线体验,普通的REST API无法做到这一点,他们通常一定需要网络连接.
  • 通过缓存整个数据集,你能实现本地查询,这将比只使用 REST 有更好的使用体验。
  • 减轻服务器端负荷,只需要在更新和修改数据时进行必要的访问。

最佳操作

  1. 异步请求 — 网络请求和其他一些操作应该放到后台,以免影响交互效果。同理Realm数据库中大规模插入和修改应该在后台进行。你可以用通知来相应后台操作。
  2. 缓存大数据库 — 我们建议你对可能使用的数据进行预处理并且存储到Realm中。 这么做可以让你在本地数据集中进行查询。
  3. 插入或更新 — 如果你的数据集有一个特有的标识符, 例如一个主键, +[RLMObject createOrUpdateInRealm:withValue:]:如果你从API得到响应, 这个method会从Realm中查询这个响应是否有记录。 如果在本地有记录, 就可以从响应中根据最新的数据进行更新。如果没有,就将该响应插入到Realm数据库中。

范例

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

如果你想参考类似示例的实际操作,请观看video demo

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

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

// 反序列化JSON响应
NSDictionary *json = [[NSJSONSerialization
                       JSONObjectWithData:response
                                  options:kNilOptions
                                    error:&error] objectForKey:@"response"];

这条响应包含了JSON数组,形式类似于:

{
  "venues": [
    {
      "id": "4c82f252d92ea09323185072",
      "name": "Golden Gate Park",
      "contact": {
        "phone": "4152522590"
      },
      "location": {
        "lat": 37.773835608329,
        "lng": -122.41962432861,
        "postalCode": "94103",
        "cc": "US",
        "state": "California",
        "country": "United States"          
      }
    }
  ]
}

要想把JSON数据导入Realm中我们有很多办法,殊途同归。你可以读取 NSDictionary然后将其属性通过插入功能手动映射到一个 RLMObject 上。为了演示效果,在这个示例里,我们将直接把 NSDictionary插入到Realm中,然后让Realm自动快速将其属性映射到RLMObjects上。为了确保示例能够成功,我们需要一个属性完全匹配JSON数据特点的RLMObjects框架。JSON数据特点如果不匹配RLMObjects,数据将在植入时自动被忽略。 以下RLMObjects的定义是有效的:

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

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

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

@implementation Location
@end
RLM_ARRAY_TYPE(Location)

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

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

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

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

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

下一步

你可以看一下我们给出的示例 看看在app中应该如何使用realm。(我们已经有越来越多的样本了!) 做一个愉快的开发者!你也总是可以在realm-cocoa上实时的和其他开发者聊天。

当前局限

Realm现在还是beta版本。我们还在为1.0的发布一直不断地添加新特性,修复bug。我们整理了一些普遍存在的限制

如果你想要看到完整地issue列表,参见 GitHub issues for a more comprehensive list of known issues.

通用限制

Realm 致力于在灵活性和性能之间做取舍。为了实现这个目标,在一个Realm中存储信息各个方面都有通用限制。例如:

  1. 类名称必须在长度为0和63字节之间。支持utf8字符。如果超出了这个限制,应用程序的初始化将引发异常。
  2. 属性名称必须在长度为0和63字节之间。支持utf8字符。如果超出了这个限制,应用程序的初始化将引发异常。
  3. NSData保存的数据应不大于16MB。存储大量的数据,可通过将其分解为16MB的块或存储直接在文件系统,需要将路径存储在 realm 中。如果你的应用程序试图存储一个大于16MB的单一文件,系统将抛除异常。
  4. NSDate存储精度为秒。它不能使用+[NSDate distantFuture] or +[NSDate distantPast]。参考NSDate在现实的局限可查看更多相关信息。
  5. 任何一个 Realm 文件也不能大于 iOS app 允许的内存空间总量。这个值会根据不同设备和当时的内存使用情况有所不同(这里又一个关于相关方面的讨论: rdar://17119975)如果你想存储更大的数据,请将数据映射到多个文件中。

暂时不支持细粒化通知

虽然要在realm发生变化的时候可以接到通知 (参见 通知), 但现在我们还不能从notification里面得知什么东西被添加/删减/移动/更新了。 我们会尽快完善这个功能的。

NSDate精度到秒

一个包含非整秒数的NSDate在存入realm的时候,会在秒的地方被截断。我们正在修复这个问题。 可参考 GitHub issue #875。同时,你可以无损存储NSTimeInterval格式。

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

因为Realm重写了setters和getters,所以你不可以在你的对象上再重写。一个简单的替代方法就是:创建一个新的realm-ignored属性(它的accessors可以被重写, 并且可以呼叫其他的getter和setter)。

不支持 KVO

Realm不支持KVO, 但它有自己的通知机制(Notifications)。我们已经添加了 KVO 支持: 详看GitHub issue #601.

文件大小&版本跟踪

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

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

为了避免这个问题,你可以调用invalidate,来告诉 Realm 你不再需要那些拷贝到 Realm 的数据了。这可以使我们不必跟踪这些对象的中间版本。在下次出现新版本时,再进行版本更新。

你可能在 Realm 使用Grand Central Dispatch时也发现了这个问题。在 dispatch 结束后自动释放调度队列(dispatch queue)时,调度队列(dispatch queue)没有随着程序释放。这造成了直到RLMRealm对象被释放后,Realm 中间版本的数据空间才会被再利用。为了避免这个问题,你应该在 dispatch 队列中,使用一个显式的自动调度队列(dispatch queue)。

FAQ

realm的支持库有多大?

一旦你的app编译完成, realm的支持库应该只有1 MB左右。 我们发布的那个可能有点大(iOS ~37MB, OSX ~2.4MB), 那是因为它们还包含了对其他构架的支持(ARM, ARM64,模拟器的是X86)和一些编译符号。 这些都会在你编译app的时候被Xcode自动清理掉。

我应该在正式产品中使用realm吗?

自2012年起, realm就已经开始被用于正式的商业产品中了。

正如你预期,我们的objective-c & Swift API 会随着社区的反馈不断的完善和进化。 所以,你也应该期待realm带给你更多的新特性和版本修复。

我要付realm的使用费用吗?

不要, Realm的彻底免费的, 哪怕你用于商业软件。

你们计划怎么赚钱?

其实,我们靠着我们的技术,已经开始赚钱啦!这些钱来自于我们销售企业级产品的利润。如果你想要得到比普通发行版本或者realm-cocoa更多的支持, 我们很高兴和你发by email聊聊。 我们一直致力于开发开源的(Apache 2.0),免费的realm-cocoa

我看到你们在代码里有“tightdb”或者“core”, 那是个什么?

TightDB是我们的C++存储引擎的旧名。core 现在还没有开源但我们的确想这样做(依旧使用Apache2.0)假如我们有时间来进行清理,重命名等工作。同时,它的二进制发行版在Realm Core(tightDB)Binary License里面可以找到。