Realm 提供了如下两个主要产品:
- Realm ORM 数据库: 开源的 ORM 数据库. (写这篇文章时 iOS 端版本: 4.3.2)
- Realm 平台: 商用的数据同步(syncing server solution)解决方案, 可以打通多个平台(移动端, 桌面端).
本文主要介绍 Realm 数据库(简称 Realm).
1 Realm 数据库简介
Realm 数据库主要用于客户端数据持久化(不过近期也发布了服务器版本, 但主要应用领域还是在客户端), 和老牌的 SQLite 相比, 它们各有各的优势:
- SQLite: 速度快, 且遵循 SQL 标准. 使用 C 实现并且可在多个平台移植. 但正是这样, 造成开发速度较慢(需要写纯 SQL, 虽然可以通过一些三方库部分加以解决).
- Realm: 现代化的数据库解决方案, 擅长快速读写数据. 但需要使用者对其 API 有一定程度的掌握.
就开发角度看, 无疑 Realm 更加符合现代 APP 的开发需求. 下面是一段最简的使用代码:
class Person: Object { dynamic var firstName: String? dynamic var lastName: String? } let me = Person() me.firstName = "Marin" me.lastName = "Todorov" // 写入 try realm.write { realm.add(me) } // 读取 realm.objects(Person.self) .filter("age > 21") .sorted("age")
Realm 的基石正是对象, 满足现代 APP 开发的一个重要需求: 类型安全. 传统的数据库通常是如下图这样的流程, 在开发中不得不多次对数据进行转换:
而使用 Realm 时, 只需要如下的流程:
另外, Realm 可以自动检测模型类的变化, 从而对应修改数据库 schema.
Realm 的核心是 Realm Core, 采用 C++ 实现, 并根据不同平台使用不同语言提供了多个 SDK, 且本地 API 的行为一致:
特别说明的是, Swift 版的 Realm 是 OC 版的封装.
Realm Object 及其运作
数据模型类必须继承自 Object, schema 对应属性需要使用 dynamic var 修饰. 当程序运行后, Realm 会自动查找所有的 Object 子类, 将这些类作为数据持久化时候的 Data Schema.
数据模型的定义如下所示:
class Car: Object { @objc dynamic var brand = "" @objc dynamic var year = 0 convenience init(brand: String, year: Int) { self.init() self.brand = brand self.year = year } }
标记为 @objc dynamic 的属性在运行时可通过动态调度(Dynamic Dispatch)机制访问, 从而实现对该属性的:
- 读: 由 Realm 来提供这个属性的实际数据, 而不是直接从内存读.
- 写: 由 Realm 将值直接写入到存储(内存或外存).
这样形成如下数据流:
当某个数据模型对象未被加入到 realm 中时, 它就和普通对象一样, 称为 unmanaged(非托管)对象.
Realm 中的 Object 实例都是动态的, 即它们可以自动更新数据视图, 而不用手动刷新. 针对实例中属性的修改可以直接反映到所有该对象的引用上, 即使查询出来的是若干个在内存中的不同对象:
let myDog = Dog() myDog.name = "Fido" myDog.age = 1 try! realm.write { realm.add(myDog) } let myPuppy = realm.objects(Dog.self).filter("age == 1").first try! realm.write { myPuppy!.age = 2 } print("age of my dog: \(myDog.age)") // => 2
Realm 数据类型
Realm 中数据类型分为两大类:
- 原子类型
- 对象类型
特别地, 在 Swift 存储 enum 时, enum 必须拥有 rawValue.
Schema 间关系
只能对位于同一个 Realm 数据库中的对象建立关系.
一对一关系
一对一关系的简单例子如下所示:
一个更为详细的设计:
一对多关系
通过 Realm 的 List 可以实现一对多关系.
使用 LinkingObjects
Realm 新版本中提供 LinkingObjects 用来手动建立两个对象的关系.
Predicate Cheat-sheet
如下是 predicate 的一个列表:
Property
排序
排序比较简单, 使用如下方式即可进行:
let sortedPeople = realm.objects(Person.self) .filter("firstName BEGINSWITH %@", "J").sorted(byKeyPath: "firstName") let names = sortedPeople.map { $0.firstName }.joined(separator: ", ") print("Sorted people: \(names)") let sortedArticles = realm.objects(Article.self).sorted(byKeyPath: "author.firstName") print("Sorted articles by author: \n\(sortedArticles.map { "- \ ($0.author!.fullName): \($0.title!)" }.joined(separator: "\n"))") let sortedPeopleMultiple = realm.objects(Person.self) .sorted(by: [ SortDescriptor(keyPath: "firstName", ascending: true), SortDescriptor(keyPath: "born", ascending: false) ]) print(sortedPeopleMultiple.map { "\($0.firstName) @ \ ($0.born)" }.joined(separator: ", "))
实践
-
实现便捷构造方法来避免外部赋值操作:
swiftconvenience init(firstName: String, born: Date, id: Int) { self.init() self.firstName = firstName self.born = born self.id = id } -
设置主键:
swift// ... @objc dynamic var key = UUID().uuidString // 默认实现中返回的是 nil, 意味着默认没有主键 override static func primaryKey() -> String? { return "key" } -
避免在移动端使用自增整数作为主键.
-
可添加索引加快查询速度:
swiftoverride static func indexedProperties() -> [String] { return ["firstName", "lastName"] }其中
firstName和lastName均为属性名称, 对应数据表的两个列.建立索引可有效提高查询或过滤时候的速度, 但会增大数据库文件体积, 且重建索引时会有一定开销. 故应尽量只对频繁查询的数据添加索引.
-
忽略数据模型中的某些属性有两种方法:
-
去掉属性前面的
dynamic @objc标记 -
显式声明忽略属性:
swift@objc dynamic var temporaryUploadId = 0 override static func ignoredProperties() -> [String] { return ["temporaryUploadId"] }
-
-
使用
@objcMembers标记简化属性字段的书写:swift@objcMembers class Article: Object { dynamic var id = 0 dynamic var title: String? }使用
@objcMembers后就不必每一个属性都写@objc了.