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

如果您打算使用 Objective‑C 版本的 Realm ,或者构建 Objective‑C 与 Swift 混编的应用,那么请使用 Realm 的 Objective‑C 版本。。 注意:不能同时使用 Objective‑C 版本和 Swift 版本的 Realm,两者是不可互操作的。

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

// 定义模型的做法和定义常规 Swift 类的做法类似
class Dog: Object {
  dynamic var name = ""
  dynamic var age = 0
}
class Person: Object {
  dynamic var name = ""
  dynamic var picture: NSData? = nil // 支持可选值
  let dogs = List<Dog>()
}

// 使用的方法和常规 Swift 对象的使用方法类似
let myDog = Dog()
myDog.name = "大黄"
myDog.age = 1
print(" 狗狗的名字: \(myDog.name)")

// 获取默认的 Realm 数据库
let realm = try! Realm()

// 检索 Realm 数据库,找到小于 2 岁 的所有狗狗
let puppies = realm.objects(Dog).filter("age < 2")
puppies.count // => 0 因为目前还没有任何狗狗被添加到了 Realm 数据库中

// 数据持久化操作十分简单
try! realm.write {
  realm.add(myDog)
}

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

// 可以在任何一个线程中执行检索操作
dispatch_async(dispatch_queue_create("background", nil)) {
  let realm = try! Realm()
  let theDog = realm.objects(Dog).filter("age == 1").first
  try! realm.write {
    theDog!.age = 3
  }
}

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

从这里开始

下载 Realm Swift 或者在GitHub上查看源码!

准备工作

  • 使用 Realm 构建应用的基本要求:iOS >= 8, OS X >= 10.9 并且支持 WatchKit。(不支持iOS 7,因为目前苹果暂不允许 iOS 使用第三方动态框架,并且目前不可能构建含有 Swift 代码的静态库。 );
  • 需要使用 Xcode 7.0 或者以后的版本;
  • 程序支持 Swift 2.x。

对 Xcode 6.x 以及 Swift 2.2 之前版本的支持已经被废弃,我们会在下一个发布版本中将其完全移除。

安装 (Swift 2.2)

  1. 下载Realm的最新版本并解压;
  2. 前往您 Xcode 工程的”General”设置页。从ios/swift-2.2/osx/swift-2.2/tvos/ 或者 watchos/ 文件夹中,拖拽RealmSwift.frameworkRealm.framework到”Embedded Binaries”选项中,确认Copy items if needed被选中并点击Finish
  3. 在您的测试目标的”Build Settings”中,添加RealmSwift.framework的上级目录到您的”Framework Search Paths”中;
  4. 如果您打算在您的 iOS、watchOS 或者 tvOS 工程中使用 Realm,请在您的 app 目标的“Build Phases”中创建一个新的”Run Script Phase”,并在文本框中写入

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

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

注意:目前不支持 CocoaPods 1.0.0 的发布候选版本。请使用最新的稳定版本。

  1. 安装CocoaPods 0.39.0 或者更高版本
  2. 在您的Podfile中,添加use_frameworks!pod 'RealmSwift' 到您的主要和测试目标;
  3. 在终端运行pod install
  4. 采用 CocoaPods 生成的.xcworkspace来运行工程!
  1. 安装 Carthage 0.9.2 或者更高版
  2. 在Carthage 中添加github "realm/realm-cocoa"
  3. 运行carthage update
  4. Carthage/Build/ 目录下对应平台文件夹中,将 RealmSwift.frameworkRealm.framework 拖曳到您 Xcode 工程”General”设置项的”Embedded Binaries”选项卡中;
  5. iOS/watchOS/tvOS: 在您应用目标的“Build Phases”设置选项卡中,点击“+”按钮并选择“New Run Script Phase”。在新建的Run Script中,填写:

    /usr/local/bin/carthage copy-frameworks

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

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

    因为要绕过APP商店提交的bug,这一步在打包通用设备的二进制发布版本时是必须的。确保这个阶段 (phase) 在 “Embed Frameworks” 阶段之后

安装 (旧有的 Swift 版本)

要支持 Swift 2.0 和 Swift 2.1 的话,只能从 Realm 源文件进行编译。我们发布的打包版本只支持 Swift 2.2。

  1. 从我们 GitHub 的仓库中克隆 Realm
  2. 在克隆的仓库中运行 git checkout v1.0.0 指令,以检查发布标记。
  3. 在克隆的仓库中运行 REALM_SWIFT_VERSION=2.0 sh build.sh build 指令,将框架编译出来。
  4. 确认您已经将两个已存在的二进制文件 RealmSwift.framework 以及 Realm.framework 从自己的项目中移除。
  5. 前往您 Xcode 项目的 “General” 设置选项。从 build/ios/swift-2.0/ 或者 build/osx/swift-2.0/ 文件夹中,将 RealmSwift.framework 以及 Realm.framework 文件拖入到 “Embedded Binaries” 栏目当中。确保 Copy items if needed 已被选中,然后单击 Finish
  6. 在您的单元测试目标的 “Build Settings” 选项中,向 “Framework Search Paths” 栏目中添加 RealmSwift.framework 的上级目录地址。
  1. 安装 0.39.0 之后版本的 CocoaPods
  2. 在 Podfile 文件中,为您的主应用目标和单元测试目标添加 use_frameworks! 以及 pod 'RealmSwift'
  3. 在命令行中运行 pod install
  4. 使用由 CocoaPods 自动产生的 .xcworkspace 来进行项目的工作。

tvOS

由于在 tvOS 中向 Doucments 目录写入数据是被禁止的,因此默认的 Realm 路径位置将被设置为 NSCachesDirectory 。然而,要注意的是 tvOS 会随时清除 Caches 目录下的文件,因此我们建议您将 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数据模型是基于标准 Swift 类来进行定义的,使用属性来完成模型的具体定义。

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

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

主要的限制是某个对象只能在其被创建的那个线程中使用。

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

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

import RealmSwift

// 狗狗的数据模型
class Dog: Object {
    dynamic var name = ""
    dynamic var owner: Person? // 属性可以设置为可选
}

// 狗狗主人的数据模型
class Person: Object {
    dynamic var name = ""
    dynamic var birthdate = NSDate(timeIntervalSince1970: 1)
    let dogs = List<Dog>()
}

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

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

通过Object 可查看更多细节。

支持的类型

Realm支持以下的属性类型:BoolInt8Int16Int32Int64DoubleFloatStringNSDate精度到秒)以及NSData.

CGFloat 属性被取消了,因为它的类型不依赖于平台。

StringNSDate 以及 NSData 类型的属性都可以添加可选值。Object 类型的属性必须设置为可选。存储可空数字可以通过 Realm 可选值来实现。

关系(Relationships)

Object 能够借助 Object 以及 List属性来和另一个 Object 建立联系。 List 表面上和Array非常类似,在 List 中的对象能够使用索引下标(indexed subscripting)来进行访问。 和 Array 不同,List 其中只能存放简单的 Object 子类类型。 要了解更详细的信息,请参阅 List

假设现在您已经定义好了 Person 数据模型(见上文),让我们创建另一个名为 Dog 的数据模型:

class Dog: Object {
    dynamic var name = ""
}

对一(To-One)关系

对于多对一(many-to-one)或者一对一(one-to-one)关系来说,只需要声明一个 Object 子类类型的属性即可:

class Dog: Object {
    // 其余属性声明...
    dynamic var owner: Person?  // 对一关系必须可空
}

您可以非常简单的通过这个属性完成关系的绑定:

let jim = Person()
let rex = Dog()
rex.owner = jim

当使用 Object 属性的时候,您可以通过正常的属性访问语法来访问嵌套属性。比如说,rex.owner?.address.country会依次读取对象的属性,然后自动从 Relam 中匹配所需的每一个对象。

对多(To-Many)关系

通过 List 类型的属性您可以定义一个对多关系。List中可以包含简单类型的Object,表面上和可变的Array非常类似。

如果打算给我们的 Person 数据模型添加一个“dogs”属性,以便能够和多个“dogs”建立关系,也就是表明一个 Person 可以有多个 Dog,那么我们可以声明一个List<Dog>类型的属性:

class Person: Object {
    // 其余的属性声明...
    let dogs = List<Dog>()
}

您可以和之前一样,对 List 属性进行访问和赋值:

let someDogs = try! Realm().objects(Dog).filter("name contains '小白'")
ZhangSan.dogs.appendContentsOf(someDogs)
ZhangSan.dogs.append(dahuang)

反向关系(Inverse Relationship)

链接是单向性的。因此,对于对多属性 Person.dogs 来说,其会链接到一个 Dog 实例,对于对一属性 Dog.owner 来说也会链接到 Person 实例。这些链接相互之间是互相独立的。为 Person 实例的 dogs 属性添加一个新的 Dog,并不会自动将狗狗的 owner 属性设置为该 Person。由于手动同步关系对会很容易出错,并且还会让内容变得复杂、冗余,Realm 提供了一个 API 来取得反向链接(backlink),如下所述。

通过反向关系(也被称为反向链接(backlink)),您可以通过一个特定的属性获取和给定对象有关系的所有对象。比如说,对 Dog 的实例调用Object().linkingObjects(_:forProperty:) 会返回对应的与被调用实例所指定属性的类有关系的所有对象。通过在 Dog 中定义一个只读(计算)属性 owners 可以简化这个操作:

class Dog: Object {
    dynamic var name = ""
    dynamic var age = 0
    var owners: [Person] {
        // Realm 并不会存储这个属性,因为这个属性只定义了 getter
        // 定义“owners”,和 Person.dogs 建立反向关系
        return linkingObjects(Person.self, forProperty: "dogs")
    }
}

可选属性(Optional Properties)

StringNSDate 以及 NSData 属性能够通过标准的 Swift 语法声明为可选或者不可选。

通过使用 RealmOptional 可以声明可空的数值类型:

class Person: Object {
	// 可选的字符串属性,默认为 nil
    dynamic var name: String? = nil

    // 可选的 int 属性,默认为 nil
    // RealmOption 属性应该始终声明为常量
    // 因为目前直接给这个属性进行赋值并不会起任何作用
    let age = RealmOptional<Int>()
}

let realm = try! Realm()
try! realm.write() {
    var person = realm.create(Person.self, value: ["Jane", 27])
    // 读取或者修改 `RealmOptional` 可以通过 `value` 属性实现
    person.age.value = 28
}

RealmOptional 支持 IntFloatDouble 以及 Bool 类型,并且所有大小的 Int 都支持(包括Int8, Int16, Int32, Int64)。

简单说明

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

类型 非可选值形式 可选值形式
Bool dynamic var value = false let value = RealmOptional<Bool>()
Int dynamic var value = 0 let value = RealmOptional<Int>()
Float dynamic var value: Float = 0.0 let value = RealmOptional<Float>()
Double dynamic var value: Double = 0.0 let value = RealmOptional<Double>()
String dynamic var value = "" dynamic var value: String? = nil
Data dynamic var value = NSData() dynamic var value: NSData? = nil
Date dynamic var value = NSDate() dynamic var value: NSDate? = nil
Object n/a: 必须是可选值 dynamic var value: Class?
List let value = List<Class> n/a: 必须是非可选值

属性特性(attributes)

Realm 模型的属性需要设置为dynamic var特性,以便其能够访问数据库底层数据。

当然这也包含两个例外: ListRealmOptional 不能被设为动态属性,因为泛型属性不能在Objective‑C运行时中被识别,因为dynamic属性会被用于动态调度。此外这两个类型应当始终声明为常量。

索引属性(Indexed Properties)

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

class Book: Object {
  dynamic var price = 0
  dynamic var title = ""

  override static func indexedProperties() -> [String] {
    return ["title"]
  }
}

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

对属性进行索引可以以极小的插入花费加快比较检索的速度(比如说= 以及 IN 操作符)。

对象的自更新特性

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

let myDog = Dog()
myDog.name = "小白"
myDog.age = 1

try! realm.write {
  realm.add(myDog)
}

let myPuppy = realm.objects(Dog).filter("age == 1").first
try! realm.write {
  myPuppy!.age = 2
}

print("age of my dog: \(myDog.age)") // => 2

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

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

主键(Primary Keys)

重写 Object.primaryKey() 可以设置模型的主键。声明主键之后,对象将被允许查询,更新速度更加高效,并且要求每个对象保持唯一性。 一旦带有主键的对象被添加到 Realm 之后,该对象的主键将不可修改。

class Person: Object {
  dynamic var id = 0
  dynamic var name = ""

  override static func primaryKey() -> String? {
    return "id"
  }
}

忽略属性(Ignored Properties)

重写 Object.ignoredProperties() 可以防止 Realm 存储数据模型的某个属性。Realm 将不会干涉这些属性的常规操作,它们将由成员变量(ivar)提供支持,并且您能够轻易重写它们的 setter 和 getter。

class Person: Object {
  dynamic var tmpID = 0
  var name: String { // 只读属性将被自动忽略
    return "\(firstName) \(lastName)"
  }
  dynamic var firstName = ""
  dynamic var lastName = ""

  override static func ignoredProperties() -> [String] {
    return ["tmpID"]
  }
}

模型继承

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

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

以下是不能完成的:

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

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

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

// 基础模型
class Animal: Object {
  dynamic var age = 0
}

// 包含有 Animal 的模型
class Duck: Object {
  dynamic var animal: Animal? = nil
  dynamic var name = ""
}
class Frog: Object {
  dynamic var animal: Animal? = nil
  dynamic var dateProp = NSDate()
}

// 用法
let duck = Duck(value: [ "animal": [ "age": 3 ], "name": "Gustav" ])

集合

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

  1. Results,类,表示从检索 中所返回的对象集合。
  2. List,类,表示模型中的对多关系
  3. RealmCollectionType,协议,定义了所有 Realm 集合所需要遵守的常用接口。
    1. AnyRealmCollection,类型擦除的类,可以向前调用一个具体的 Realm 集合,例如 Results 或者 List

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

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

func operateOn<C: RealmCollectionType>(collection: C) {
  // collection 既可以是 Results,也可以是 List
  print("对集合的操作包含了 \(collection.count) 个对象")
}

因为 Swift 中包含关联类型的协议不能够拥有具体的形式,因此使用类型擦除的封装 是很有必要的,例如 AnyRealmCollection,这样就可以将这个集合作为属性或者变量进行存储:

class ViewController {
//  let collection: RealmCollectionType
//                  ^
//                  error: protocol 'RealmCollectionType' can only be used
//                  as a generic constraint because it has Self or
//                  associated type requirements
//
//  init<C: RealmCollectionType where C.Element == MyModel>(collection: C) {
//    self.collection = collection
//  }

  let collection: AnyRealmCollection<MyModel>

  init<C: RealmCollectionType where C.Element == MyModel>(collection: C) {
    self.collection = AnyRealmCollection(collection)
  }
}

对象存储

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

Rrealm的对象可以被实例化并且作为独立对象使用(也就是未存储在 Realm 数据库中),和其他常规Swift对象无异。

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

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

由于写入事务像其余硬盘读写操作一样,会出现失败的情况,因此 Realm.write() 以及 Realm.commitWrite() 可以加上 throws标记。因此你可以处理和恢复诸如硬盘空间溢出之类的错误。此外,其他的错误都无法进行恢复。简单起见,我们的代码示例并不会处理这些错误,但是您应当在您应用当中注意到这些问题。

创建对象

当定义完数据模型之后,您可以将您的 Object 子类实例化,然后向 Realm 中添加新的实例。我们以下面这个简单的模型为例:

class Dog: Object {
    dynamic var name = ""
    dynamic var age = 0
}

我们可以用多种方法创建一个新的对象:

// (1) 创建一个狗狗对象,然后设置其属性
var myDog = Dog()
myDog.name = "大黄"
myDog.age = 10

// (2) 通过字典创建狗狗对象
let myOtherDog = Dog(value: ["name" : "豆豆", "age": 3])

// (3) 通过数组创建狗狗对象
let myThirdDog = Dog(value: ["豆豆", 5])
  1. 使用指定初始化器(designated initializer)创建对象是最简单的方式。
  2. 通过使用恰当的键值,对象还可以通过字典完成创建。
  3. 最后,Object 子类还可以通过数组完成实例化,数组中的值必须和数据模型中对应属性的次序相同。

嵌套属性(Nested Object)

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

// 这里我们就可以使用已存在的狗狗对象来完成初始化
let aPerson = Person(value: ["李四", 30, [aDog, anotherDog]])

// 还可以使用多重嵌套
let anotherPerson = Person(value: ["李四", 30, [["小黑", 5], ["旺财", 6]]])

即使是数组以及字典的多重嵌套,Realm 也能够轻松完成对象的创建。注意 List 只能够包含 Object 类型,不能包含诸如String之类的基础类型。

添加数据

向 Realm 中添加数据的步骤如下:

// 创建一个 Person 对象
let author = Person()
author.name = "大卫·福斯特·华莱士"

// 获取默认的 Realm 实例
let realm = try! Realm()
// 每个线程只需要使用一次即可

// 通过事务将数据添加到 Realm 中
try! realm.write {
  realm.add(author)
}

等您将某个对象添加到 Realm 数据库之后,您可以继续使用它,并且您对其做的任何更改都会被保存(必须在一个写入事务当中完成)。当写入事务提交之后,使用相同 Realm 数据源的其他线程才能够对这个对象进行更改。

请注意,如果在进程中存在多个写入操作的话,那么单个写入操作将会阻塞其余的写入操作,并且还会锁定该操作所在的当前线程。

这个特性与其他持久化解决方案类似,我们建议您使用该方案的通常做法,也就是将写入操作转移到一个独立的线程中执行。

由于 Realm 采用了 MVCC 设计架构,读取操作 并不会 因为写入事务正在进行而受到影响。除非您需要立即使用多个线程来同时执行写入操作,不然您应当采用批量化的写入事务,而不是采用多次少量的写入事务。

查看RealmObject来获得更多内容。

更新数据

Realm 提供了一系列用以更新数据的方式,这些方式都有着各自所适应的情景。请选择最符合您当前需求的方式来使用:

内容直接更新

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

// 在一个事务中更新对象
try! realm.write {
  author.name = "托马斯·品钦"
}

通过主键更新

如果您的数据模型中设置了主键的话,那么您可以使用Realm().add(_:update:)来更新对象,或者当对象不存在时插入新的对象。

// 创建一个带有主键的“书籍”对象,作为事先存储的书籍
let cheeseBook = Book()
cheeseBook.title = "奶酪食谱"
cheeseBook.price = 9000
cheeseBook.id = 1

// 通过 id = 1 更新该书籍
try! realm.write {
  realm.add(cheeseBook, update: true)
}

如果主键 id 为1的 Book 对象已经存在于数据库当中了,那么对象就会简单地进行更新。而如果不在数据库中存在的话,那么这个操作将会创建一个新的 Book 对象并添加到数据库当中。

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

// 假设带有主键值 `1` 的“书籍”对象已经存在
try! realm.write {
  realm.create(Book.self, value: ["id": 1, "price": 9000.0], update: true)
// 这本书的`title`属性不会被改变
}

您不能够给未定义主键的对象类型传递 update: true

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

键值编码

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

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

let persons = realm.objects(Person)
try! realm.write {
  persons.first?.setValue(true, forKeyPath: "isFirst")
  // 将每个人的 planet 属性设置为“地球”
  persons.setValue("地球", forKeyPath: "planet")
}

删除数据

通过在写入事务中将要删除的对象传递给 Realm().delete(_:) 方法,即可完成删除操作。

// let cheeseBook = ... 存储在 Realm 中的 Book 对象

// 在事务中删除一个对象
try! realm.write {
  realm.delete(cheeseBook)
}

您也能够删除存储在 Realm 中的所有数据。注意,Realm 文件的大小不会被改变,因为它会保留空间以供日后快速存储数据。

// 从 Realm 中删除所有数据
try! realm.write {
  realm.deleteAll()
}

查询

通过查询操作,Realm 将会返回包含 Object 集合的Results实例。Results 的表现和 Array 十分相似,并且包含在 Results 中的对象能够通过索引下标进行访问。和 Array 不同,Results 其当中只能包含Object 子类类型的属性。

所有的查询(包括查询和属性访问)在 Realm 中都是延迟加载的,只有当属性被访问时,才能够读取相应的数据。

查询结果并不是数据的拷贝:修改查询结果(在写入事务中)会直接修改硬盘上的数据。同样地,您可以直接通过包含在Results中的Object对象完成遍历关系图的操作。

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

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

从 Realm 中检索对象的最基本方法是Realm().objects(_:),这个方法将会返回带有所有Object的Results实例,并且这个实例的类型将是默认 Realm 数据库中被查询的子类类型。

let dogs = realm.objects(Dog) // 从默认的 Realm 数据库中,检索所有狗狗

条件查询(Filtering)

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

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

// 使用断言字符串查询
var tanDogs = realm.objects(Dog).filter("color = '棕黄色' AND name BEGINSWITH '大'")

// 使用 NSPredicate 查询
let predicate = NSPredicate(format: "color = %@ AND name BEGINSWITH %@", "棕黄色", "大")
tanDogs = realm.objects(Dog).filter(predicate)

查看苹果的断言编程指南来获取更多关于断言查询和NSPredicate Cheatsheet的使用信息。 Realm 支持许多常见的断言:

  • 比较操作数(comparison operand)可以是属性名称或者某个常量,但至少有一个操作数必须是属性名称;
  • 比较操作符 ==<=<>=>!=, 以及 BETWEEN 支持 Int, Int8, Int16, Int32, Int64, Float, Double 以及 NSDate 属性类型的比较,比如说 age == 45
  • 相等比较 ==以及!=,比如说Results<Employee>().filter("company == %@", company)
  • 比较操作符 == and != 支持布尔属性;
  • 对于 String 和 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 比较,比如说 Results<Company>().filter("ceo == nil")。注意到这只适用于有关系的对象,这里 ceo 是 Company 模型的一个属性。
  • 通过 ==, != 进行空值比较,比如说 Results<Company>().filter("ceo == nil"); 注意,Realm 将 nil 视为一个特殊的值而不是“缺失值”,不像 SQL 那样 nil 等于自身。
  • ANY 比较,比如说 ANY student.age < 21
  • List 以及 Results 属性支持集合表达式:@count@min@max@sum 以及 @avg,例如 realm.objects(Company).filter("employees.@count > 5") 用以寻找所有超过 5 名雇员的公司。
  • 支持子查询,不过有限制:
    • @count 是唯一能应用在 SUBQUERY 表达式中的操作符
    • SUBQUERY(…).@count 表达式必须与常量进行比较
    • 相关子查询目前还不支持

要了解关于断言的更多详情,请查看Results().filter(_:...) 的详细信息。

排序

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

// 排序名字以“B”开头的棕黄色狗狗
let sortedDogs = realm.objects(Dog).filter("color = '棕黄色' AND name BEGINSWITH 'B'").sorted("大")

关于排序的更多信息,请查看 Results().sorted(_:) and Results().sorted(_:ascending:) 的详细信息。

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

链式查询

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

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

let tanDogs = realm.objects(Dog).filter("color = '棕黄色'")
let tanDogsWithBNames = tanDogs.filter("name BEGINSWITH '大'")

自动更新

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

let puppies = realm.objects(Dog).filter("age < 2")
puppies.count // => 0
try! realm.write {
  realm.create(Dog.self, value: ["name": "大黄", "age": 1])
}
puppies.count // => 1

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

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

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

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

try! realm.write {
  for person in realm.objects(Person).filter("age == 10") {
    person.age++
  }
}

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

查询结果限制

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

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

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

// 循环前 5 个 Dog 对象
// 限制从磁盘读取的对象数量
let dogs = try! Realm().objects(Dog)
for i in 0..<5 {
  let dog = dogs[i]
  // ...
}

Realm 数据库

默认的 Realm 数据库

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

Realm 配置

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

配置同时也可以在每次您需要使用 Realm 实例的时候传递给Realm(configuration: config),或者您也可以通过 Realm.Configuration.defaultConfiguration = config 来为默认的 Realm 数据库进行配置。

比如说,假设有这样一个应用,用户必须登录到您的网站后台才能够使用,然后您希望这个应用支持快速帐号切换功能。 您可以为每个帐号创建一个特有的 Realm 文件,通过对默认配置进行更改,就可以直接使用默认的 Realm 数据库来直接访问了,如下所示:

func setDefaultRealmForUser(username: String) {
  var config = Realm.Configuration()

  // 使用默认的目录,但是使用用户名来替换默认的文件名
  config.fileURL = config.fileURL!.URLByDeletingLastPathComponent?
                         .URLByAppendingPathComponent("\(username).realm")

  // 将这个配置应用到默认的 Realm 数据库当中
  Realm.Configuration.defaultConfiguration = config
}

其他的realm数据库

有的时候,在不同位置存储多个 Realm 数据库是十分有用的。 例如,如果您需要将您应用的某些数据打包到一个 Realm 文件中,作为主要 Realm 数据库的扩展。 您可以像以下代码这样做:

let config = Realm.Configuration(
    // 获取需要打包文件的 URL 路径
    fileURL: NSBundle.mainBundle().URLForResource("MyBundledData", withExtension: "realm"),
    // 以只读模式打开文件,因为应用数据包并不可写
    readOnly: true)

// 通过配置打开 Realm 数据库
let realm = try! Realm(configuration: config)

// 通过配置打开 Realm 数据库
let results = realm.objects(Dog).filter("age > 5")

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

内存数据库

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

let realm = try! Realm(configuration: Realm.Configuration(inMemoryIdentifier: "MyInMemoryRealm"))

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

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

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

错误处理

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

要处理在指定线程中初次 Realm 数据库导致的错误, 使用 Swift 内置的错误处理机制:

do {
  let realm = try Realm()
} catch let error as NSError {
  // 错误处理
}

在Realm数据库间拷贝数据

拷贝 Realm 对象到另一个 Realm 数据库十分简单,只需将原始对象传递给Realm().create(_:value:update:)。例如, realm.create(MyObjectSubclass.self, value: originalObjectInstance).

查找 Realm 文件

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

附加的 Realm 文件

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

  • .realm.lock - 对资源进行锁定的文件
  • .realm.log_a, .realm.log_b - 事务日志记录文件
  • .realm.note - 用于通知的命名管道

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

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

在应用中建立 Realm 数据库

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

  1. 首先,定位 realm 的所在位置。您应该使用与最终版本相同的数据模型来创建 Realm 数据库,并将您想要打包的数据放置到您的应用当中。由于 Realm 文件是跨平台的,因此您能够测试您的OS X app (查看我们的JSONImport example)或者在模拟器中运行您的 iOS app;
  2. 在生成 Realm 文件的代码处,您需要结尾对文件进行压缩拷贝(参见 Realm().writeCopyToPath(_:encryptionKey:))。 这有助于减少 Realm 的文件体积,让您发布的应用体积更小;
  3. 将您最终的 Realm 文件的压缩拷贝拖懂到您最终应用的Xcode项目导航栏中;
  4. 前往您应用的Xcode Build Phase 选项卡,添加 Realm 文件到”Copy Bundle Resources”当中;
  5. 这样,您就能够在您的应用中使用这个打包好的 Realm 数据库了。 您能通过使用NSBundle.mainBundle().pathForResource(_:ofType:)来得到数据库路径;
  6. 如果打包的 Realm 文件包含有您不想修改的固定数据,您也能通过为Realm.Configuration 对象设置 readOnly = true 选项,这样就可以将其从包路径直接打开了。 如果您打算修改初始数据的话,您可以通过NSFileManager.defaultManager().copyItemAtPath(_:toPath:),将这个打包的文件拷贝到应用的 Document 文件夹下。

您能够参考我们的迁移例程应用来学习如何使用打包好的 Realm 文件。

类的子集(Class Subsets)

在某些情况下,您可能想要对哪个类能够存储在指定 Realm 数据库中做出限制。 例如,如果有两个团队分别负责开发您应用中的不同部分,并且同时在应用内部使用了 Realm 数据库,那么您肯定不希望为它们协调进行数据迁移。 您可以通过设置您的 Realm.Configuration 的 objectTypes 属性来对类做出限制。

let config = Realm.Configuration(objectTypes: [MyClass.self, MyOtherClass.self])
let realm = try! Realm(configuration: config)

删除 Realm 文件

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

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

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

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

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

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

autoreleasepool {
  // 所有 Realm 的使用操作
}
let realmURL = Realm.Configuration.defaultConfiguration.fileURL!
let realmURLs = [
  realmURL,
  realmURL.URLByAppendingPathExtension("lock"),
  realmURL.URLByAppendingPathExtension("log_a"),
  realmURL.URLByAppendingPathExtension("log_b"),
  realmURL.URLByAppendingPathExtension("note")
]
let manager = NSFileManager.defaultManager()
for URL in realmURLs {
  do {
    try manager.removeItemAtURL(URL)
  } catch {
    // 处理错误
  }
}

在后台应用刷新中使用 Realm

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

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

如果您选择不完整的 iOS 文件加密的话,我们鼓励您使用 Realm 自己的内置加密来确保您的数据安全。

由于辅助文件有时会进行延迟创建和删除操作,我们建议您将文件保护属性应用到包含 Realm 文件的父级文件夹上面。这将确保该属性被恰当地应用到所有相关的 Realm 文件上面,而无需关心它们的创建时间。

let realm = try! Realm()

// 获取我们的 Realm 文件的父级目录
let folderPath = realm.configuration.fileURL!.URLByDeletingLastPathComponent!.path!

// 解除这个目录的保护
try! NSFileManager.defaultManager().setAttributes([NSFileProtectionKey: NSFileProtectionNone],
                                                  ofItemAtPath: folderPath)

线程(Threading)

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

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

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

检视其他线程上的变化

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

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

Realm 同样也会在写入事务提交(Realm.commitWrite())的时候刷新。

如果定期刷新 Realm 失败的话,就可能会导致某些事务的版本变为“锁定(pinned)”状态,阻止 Realm 重用该版本的硬盘空间,从而导致文件尺寸变大。查看我们的 当前的限制以获取关于此情况的更多信息。

跨线程传递实例

Object 的单例(未保存的)表现的和正常的 NSObject 子类相同,可以安全地跨线程传递。

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

相反,还有很多可以安全在线程间传递实例的方法。比如说,一个拥有主键的对象可以通过其主键值来进行传递;还有 Results 可以通过其 NSPredicate 或者检索语句来进行传递;还有 Realm 可以通过其 Realm.Configuration 来进行传递。目标线程可以通过使用其线程安全的表达方式来重新捕获这些 Realm、Object、Results 以及 List。记住重新捕获可能会得到当前线程所拥有的实例版本,和先前线程的版本可能会有所不同。

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

  • Realm: 所有的属性、类方法和构造器;all properties, class methods, and initializers.
  • Object: invalidatedobjectSchemarealm,以及所有的类方法和构造器;
  • Results: objectClassNamerealm
  • List: invalidatedobjectClassNamerealm

跨线程使用数据库

为了在不同的线程中使用同一个 Realm 文件,您需要为您应用的每一个线程初始化一个新的Realm 实例。 只要您指定的配置是相同的,那么所有的 Realm 实例都将会指向硬盘上的同一个文件。

我们还 不支持 跨线程共享Realm 实例。 Realm 实例要访问相同的 Realm 文件还必须使用相同的 Realm.Configuration

当写入大量数据的时候,在一个单独事务中通过批量执行多次写入操作是非常高效的。事务也可以使用Grand Central Dispatch(GCD)在后台运行,以防止阻塞主线程。 Realm 对象并不是线程安全的,并且它也不能够跨线程共享,因此您必须要为每一个您想要执行读取或者写入操作的线程或者dispatch队列创建一个 Realm 实例。 这里有一个在后台队列中插入百万数据的例子:

dispatch_async(queue) {
  autoreleasepool {
    // 在这个线程中获取 Realm 和表实例
    let realm = try! Realm()

    // 通过开启写入操作将写入闭包分成多个微小部分
    for idx1 in 0..<1000 {
      realm.beginWrite()

      // 通过字典插入行,忽略属性次序
      for idx2 in 0..<1000 {
        realm.create(Person.self, value: [
          "name": "\(idx1)",
          "birthdate": NSDate(timeIntervalSince1970: NSTimeInterval(idx2))
        ])
      }

      // 提交写入事务以确保数据在其他线程可用
      try! realm.commitWrite()
    }
  }
}

JSON

Realm 没有提供对 JSON 的直接支持,但是您可以使用 NSJSONSerialization.JSONObjectWithData(_:options:) 的输出完成从 JSON 中添加 Object 的操作。 由此所产生的 KVC 兼容的对象可以使用创建和更新对象的 标准 API 来添加/更新 Object

// 表示城市的一个 Realm 对象
class City: Object {
  dynamic var city = ""
  dynamic var id = 0
  // 其它空余的属性…
}

// 从包含 JSON 的 NSData 中插入数据
try! realm.write {
  let json = try! NSJSONSerialization.JSONObjectWithData(data, options: NSJSONReadingOptions())
  realm.create(City.self, value: json, update: true)
}

如果在 JSON 中包含了嵌套的对象或者数组的话,这些数据都将被自动映射到对一以及对多关系——参见嵌套对象 一节获取详情。

当使用这种方法在 Realm 中插入或者更新 JSON 数据的时候,要注意 Realm 需要确保 JSON 的属性名和类型能够与 Object 属性完全匹配。例如:

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

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

通知(Notification)

通过调用 addNotificationBlock 方法进行通知注册后,无论哪个 Realm, Results 或者 List 对象更新,都可以得到通知。

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

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

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

Realm 通知

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

// 获取 Realm 通知
let token = realm.addNotificationBlock { notification, realm in
    viewController.updateUI()
}

// 随后
token.stop()

集合通知

集合通知 (Collection notifications) 与 Realm 通知有着略微的不同。 集合通知包含了描述在细粒层级中发生了何种变化的信息,包括自上一次通知以来出现的:已插入对象、已删除对象,或者已修改对象的索引。这使得在您的 UI 当中单独控制动画以及视觉效果更新成为了可能,而不是在每次通知发生的时候肆意重载各种东西。

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

这些变化可以通过传递到通知闭包当中的 RealmCollectionChange 参数访问到。

class ViewController: UITableViewController {
  var notificationToken: NotificationToken? = nil

  override func viewDidLoad() {
    super.viewDidLoad()
    let realm = try! Realm()
    let results = realm.objects(Person).filter("age > 5")

    // 观察 Results 通知
    notificationToken = results.addNotificationBlock { [weak self] (changes: RealmCollectionChange) in
      guard let tableView = self?.tableView else { return }
      switch changes {
      case .Initial:
        // Results 现在已经填充完毕,可以不需要阻塞 UI 就可以被访问
        tableView.reloadData()
        break
      case .Update(_, let deletions, let insertions, let modifications):
        // 检索结果被改变,因此将它们应用到 UITableView 当中
        tableView.beginUpdates()
        tableView.insertRowsAtIndexPaths(insertions.map { NSIndexPath(forRow: $0, inSection: 0) },
          withRowAnimation: .Automatic)
        tableView.deleteRowsAtIndexPaths(deletions.map { NSIndexPath(forRow: $0, inSection: 0) },
          withRowAnimation: .Automatic)
        tableView.reloadRowsAtIndexPaths(modifications.map { NSIndexPath(forRow: $0, inSection: 0) },
          withRowAnimation: .Automatic)
        tableView.endUpdates()
        break
      case .Error(let error):
        // 如果在后台工作线程中打开 Realm 文件的时候,就会发生错误
        fatalError("\(error)")
        break
      }
    }
  }

  deinit {
    notificationToken?.stop()
  }
}

用户驱动更新

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

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

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

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

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

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

通知 APIs

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

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

Realm 对象的大多数属性都遵从 KVO 机制。 所有 Object 子类的持久化(persisted)存储(未被忽略)的属性都是遵循 KVO 机制的,并且 Object 以及 List 中 无效的(invalidated) 属性也同样遵循。

观察 Object 子类的单个实例的属性的方法就如同观察其他 动态属性 一样,不过要注意的是,当观察者(observer)存在的时候,您不能够使用诸如 realm.add(obj) 此类的方法向 Realm 数据库中添加对象。

观察持久化对象属性的方法有些许不同。 对于持久化对象来说,有三种能够改变其属性值的方法:直接赋值修改;调用 realm.refresh() 方法或者当另一个线程提交了写入事务之后,Realm 自行进行了更新;当另一个线程发生了改变,但还未刷新的时候,在当前进程调用 realm.beginWrite()

在后面两种情况中,所有在另一个线程的写入事务中进行的修改都将会立即实现,并且会立刻发送 KVO 通知。 所有的中间过程都会被抛弃掉,因此如果在写入事务中您将一个属性从 1 递增到 10,那么在主线程您只会得到一个属性从 1 直接变到 10 的通知。 由于属性的值可以不在写入事务中发生改变,甚至还可以作为写入事务开始的一部分,因此我们不推荐在 -observeValueForKeyPath:ofObject:change:context: 中尝试修改持久化 Realm 对象的值。

NSMutableArray 属性不同,观察 List 属性值的改变并不需要使用 -mutableArrayValueForKey: 方法,虽然这个方法适合未写入 Realm 数据库中的数据。 相反,您可以直接调用 List 中的修改方法,任何观察该属性的对象都将会得到通知。 和其他正常的属性不同,List 类型的属性不需要被标记为 dynamic 就可以被观察。

在我们的例程应用ReactiveCocoa from Objective‑CReactKit from Swift中,您可以找到关于使用 Realm KVO机制的简要例子。

数据迁移(Migration)

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

class Person: Object {
    dynamic var firstName = ""
    dynamic var lastName = ""
    dynamic var age = 0
}

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

class Person: Object {
    dynamic var fullName = ""
    dynamic var age = 0
}

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

进行迁移

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

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

// 在(application:didFinishLaunchingWithOptions:)中进行配置

let config = Realm.Configuration(
  // 设置新的架构版本。这个版本号必须高于之前所用的版本号(如果您之前从未设置过架构版本,那么这个版本号设置为 0)
  schemaVersion: 1,

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

// 告诉 Realm 为默认的 Realm 数据库使用这个新的配置对象
Realm.Configuration.defaultConfiguration = config

// 现在我们已经告诉了 Realm 如何处理架构的变化,打开文件之后将会自动执行迁移
let realm = try! Realm()

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

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

// 在 application(application:didFinishLaunchingWithOptions:) 中进行配置

Realm.Configuration.defaultConfiguration = Realm.Configuration(
  schemaVersion: 1,
  migrationBlock: { migration, oldSchemaVersion in
    if (oldSchemaVersion < 1) {
      // enumerate(_:_:) 方法遍历了存储在 Realm 文件中的每一个“Person”对象
      migration.enumerate(Person.className()) { oldObject, newObject in
        // 将名字进行合并,存放在 fullName 域中
        let firstName = oldObject!["firstName"] as! String
        let lastName = oldObject!["lastName"] as! String
        newObject!["fullName"] = "\(firstName) \(lastName)"
      }
    }
  })

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

添加更多版本

假如说现在我们有两个先前版本的 Person 类:

// v0
// class Person: Object {
//     dynamic var firstName = ""
//     dynamic var firstName = ""
//     dynamic var age = 0
// }

// v1
// class Person: Object {
//     dynamic var fullName = "" // 新属性
//     dynamic var age = 0
// }

// v2
class Person: Object {
    dynamic var fullName = ""
    dynamic var email = "" // 新属性
    dynamic var age = 0
}

我们的迁移闭包里面的逻辑大致如下:

Realm.Configuration.defaultConfiguration = Realm.Configuration(
  schemaVersion: 2,
  migrationBlock: { migration, oldSchemaVersion in
    // enumerateObjects:block: 遍历了存储在 Realm 文件中的每一个“Person”对象
    migration.enumerate(Person.className()) { oldObject, newObject in
      // 只有当 Realm 数据库的架构版本为 0 的时候,才添加 “fullName” 属性
      if oldSchemaVersion < 1 {
        let firstName = oldObject!["firstName"] as! String
        let lastName = oldObject!["lastName"] as! String
        newObject!["fullName"] = "\(firstName) \(lastName)"
      }

      // 只有当 Realm 数据库的架构版本为 0 或者 1 的时候,才添加“email”属性
      if oldSchemaVersion < 2 {
          newObject!["email"] = ""
      }
    }
  })

// Realm 数据库会自动执行此数据迁移,然后成功进行访问
let realm = try! Realm()

要了解关于数据架构迁移如何实现的更完整信息,请参考我们的迁移例程应用

线性迁移(Linear Migrations)

假如说,我们的应用有两个用户: JP和Tim。

JP经常更新应用,但Tim却经常跳过某些版本。

所以JP可能下载过这个应用的每一个版本,并且一步一步地跟着更新构架:第一次下载更新后,数据库架构从v0更新到v1;第二次架构从v1更新到v2……以此类推,井然有序。

相反,Tim很有可能直接从v0版本直接跳到了v2版本。

因此,您应该使用非嵌套的 if (oldSchemaVersion < X) 结构来构造您的数据库迁移模块,以确保无论用户在使用哪个版本的架构,都能完成必需的更新。

当您的用户不按套路出牌,跳过有些更新版本的时候,另一种情况也会发生。

假如您在v2里删掉了一个“email”属性,然后在v3里又把它重新引进了。假如有个用户从v1直接跳到v3,那 Realm 不会自动检测到v2的这个删除操作,因为存储的数据构架和代码中的构架吻合。

这会导致Tim的Person对象有一个v3的email property,但里面的内容却是v1的。

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

加密(Encryption)

Realm 的加密 API 目前支持 iOS、OS X 以及 WatchKit 平台,但 不支持 watchOS 平台,因为 Realm 加密机制使用的 <mach/mach.h> 以及 <mach/exc.h> API 被标记为__WATCHOS_PROHIBITED` 了。

我们针对这个问题提交了一个 radar 请求:rdar://22063654

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

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

// 产生随机密钥
let key = NSMutableData(length: 64)!
SecRandomCopyBytes(kSecRandomDefault, key.length,
    UnsafeMutablePointer<UInt8>(key.mutableBytes))

// 打开加密文件
let config = Realm.Configuration(encryptionKey: key)
do {
  let realm = try Realm(configuration: config)
  // 和往常一样使用 Realm 即可
  let dogs = realm.objects(Dog).filter("name contains 'Fido'")
} catch let error as NSError {
  // 如果密钥错误,`error` 会提示数据库不可访问
  fatalError("Error opening realm: \(error)")
}

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

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

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

测试与调试

配置默认的 Realm 数据库

使用和测试 Realm 应用的最简单方法就是使用 默认的 Realm 数据库了。 为了避免在测试中覆盖了应用数据或者泄露,您只需要为每项测试将默认的 Realm 数据库设置为新文件即可。

import XCTest

// 一个基本的测试类,每个使用 Realm 进行的测试都应当继承自该类,而不是直接继承自 XCTestCase 类
class TestCaseBase: XCTestCase {
  override func setUp() {
    super.setUp()

    // 使用当前测试名标识的内存 Realm 数据库。
    // 这确保了每个测试都不会从别的测试或者应用本身中访问或者修改数据,并且由于它们是内存数据库,因此无需对其进行清理。
    Realm.Configuration.defaultConfiguration.inMemoryIdentifier = self.name
  }
}

注入(injecting) Realm 实例

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

// 应用中使用的代码
public func updateUserFromServer() {
  let url = NSURL(string: "http://myapi.example.com/user")
  NSURLSession.sharedSession().dataTaskWithURL(url!) { data, _, _ in
    let realm = try! Realm()
    createOrUpdateUserInRealm(realm, withData: data!)
  }
}

public func createOrUpdateUserInRealm(realm: Realm, withData data: NSData) {
  let object = try! NSJSONSerialization.JSONObjectWithData(data, options: [])
                      as! [String: String]
  try! realm.write {
    realm.create(User.self, value: object, update: true)
  }
}

// 测试中使用的代码

let testRealmURL = NSURL(fileURLWithPath: "...")

func testThatUserIsUpdatedFromServer() {
  let config = Realm.Configuration(fileURL: testRealmURL)
  let testRealm = try! Realm(configuration: config)
  let jsonData = "{\"email\": \"[email protected]\"}"
                  .dataUsingEncoding(NSUTF8StringEncoding)!
  createOrUpdateUserInRealm(testRealm, withData: jsonData)
  let expectedUser = User()
  expectedUser.email = "[email protected]"
  XCTAssertEqual(testRealm.objects(User).first!,
                 expectedUser,
                 "用户信息未能从服务器正常更新")
}

调试

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

使用 Realm Swift API 对应用进行调试需要在 LLDB 控制台中进行。

注意,尽管通过我们的Xcode插件安装的 LLDB 脚本允许您在 Xcode 用户界面中查看 Realm 变量的内容,但是目前对于 Swift 来说还无法使用。这些变量显示的数据并不正确。您需要在 LLDB 中使用 po 命令来查看存储在 Realm 中的数据内容。

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

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

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

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

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

当前版本的限制

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

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

基本的限制

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

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

NSDate 的精度只能到秒

一个包含非整数秒的 NSDate 对象存入 Realm 后,会在秒的地方截断。我们正在修复这个问题。 可参考 GitHub issue #875获取此问题的更多信息。同时,您可以存储无损的 NSTimeInterval 格式来获取精密的时间。

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

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

文件大小 & 版本跟踪

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

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

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

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

Realm 没有自动增长属性

Realm 没有线程/进程安全的自动增长属性机制,这在其他数据库中常常用来产生主键。然而,在绝大多数情况下,对于主键来说,我们需要的是一个唯一的、自动生成的值,因此没有必要使用顺序的、连续的、整数的 ID 作为主键。

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

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

不可以在Objective‑C中加载 List 和 RealmOptional 属性

如果您需要在 Objective‑C 中访问您的 Realm Swift 模型的话,那么注意所有 List 以及 RealmOptional 属性都不可用(就像其他 Swift 独有的数据类型一样)——如果有必要的话,您可以添加封装的 getter 和 setter 方法,将其在 NSNumber 或者 NSArray 之间进行转化。

此外,早于 Xcode 7 Beta 5 之前的版本有一个 已知的Swift bug,它会导致自动生成的 Objective‑C 头文件(-Swift.h)无法通过编译。您就必须将 List 类型的属性设置为 private 或者 internal。请前往 GitHub issue #1925了解更多信息。

为 Object 子类添加自定义构造器

当您创建 Object 子类模型的时候,您或许会想要添加自己的构造器方法,以便增加便利性。

由于 Swift 的内省(introspection)机制中现有的一些限制,我们不能给这个类中添加指定构造器(designated initializer)。相反,它们需要被标记为便利构造器(convenience initializer),使用相同名字的 Swift 关键词:

class MyModel: Object {
    dynamic var myValue = ""

    convenience init(myValue: String) {
        self.init() //注意这里需要调用 'self' 而不是 'super'
        self.myValue = myValue
    }
}

小技巧

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

问答时间

Realm 库文件有多大?

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

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

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

您应当期待我们的 Objective‑C & Swift API 会随着社区的反馈不断的完善和进化。同时,您也应该期待 Realm 带给您更多的新特性和版本修复。

我要付费使用 Realm 吗?

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

你们计划怎么赚钱?

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

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

Core是我们内部的C++存储引擎的名称。Core 现在还没有开源,假如我们有时间来进行清理、重命名以及确定其当中的主要功能的话,但我们的确想将其开源(依旧使用Apache2.0开源协议)。同时,它的二进制发行版本在 Realm Core(TightDB)Binary License里面可以找到。

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

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

疑难解答

崩溃报告

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

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

报告 Realm 错误

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

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

  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。

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