Realm数据库介绍

简介:

我在瓣读中使用了Realm做数据的持久化存储,所以把自己的一些实践体验分享一下。

Realm 的身世,大家可以在官网上查看,当初我是看了一遍 Realm 官网上的介绍,就很快决定了选用它,它第一眼吸引的特点有以下几点:

1、原生就支持 Swift,有 RealmSwift
2、多平台、可视化支持,同时支持iOS和Androi**d,还有一个Realm Brower应用,可以查看数据库中的内容。
3、使用简单方便,它不是基于Sqlite的封装,而是完全自己开发了一个数据库,完全没有那些sqlite语句,基本上看一眼示例代码就知道怎么用了。

基本操作

下面我们就来看一下 Realm 的一些基本操作

创建数据库

1
2
3
4
5
6
7
8
// 默认配置
let realm = try! Realm()

// 自定义配置
let url = URL(fileURLWithPath: RLMRealmPathForFile("evernoteData.realm"), isDirectory: false)
var config = Realm.Configuration(fileURL: url)
config.schemaVersion = realm.configuration.schemaVersion
let evernoteRealm = try! Realm(configuration: config)

add update delete query

需要存储到 Realm 数据库中的 Model 都必须继承自 RealmSwift 自定义的一个类: Object, 自持 Swift 的 nullability 特性,支持一对一和一对多的关系。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

import RealmSwift

// Dog model
class Dog: Object {
@objc dynamic var name = ""
@objc dynamic var owner: Person? // Properties can be optional
}

// Person model
class Person: Object {
@objc dynamic var name = ""
@objc dynamic var birthdate = Date(timeIntervalSince1970: 1)
let dogs = List<Dog>()
}

还可以给 Model 设置主键primaryKey,默认值defaultPropertyValues,忽略的属性ignoredProperties,必要属性requiredProperties,索引indexedProperties。

比较有用的是主键和索引,最好是给每个 Model 都设置主键,这样便于查询和更新,一开始的时候瓣读上的图书评分model没有设置主键,结果所有的书都用了同一个评分,导致评分都显示一样了。

要将一个 Model 添加进数据库中也很简单:

1
2
3
4
5
6

let dog = Dog()
let realm = try! Realm()
try! realm.write() {
realm.add(dog)
}

更新 删除 Model:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

let dog = Dog()
let realm = try! Realm()
try! realm.write() {
realm.add(dog, update: true)
}

try! realm.write() {
dog.name = "ww"
dog.setValue("hh", forKeyPath: "name")
}

// 删除
try! realm.write() {
realm.delete(dog)
realm.deleteAll()
}

查询:

1
2
3
4
5
6
7
8
9
10
11
12

let dogs = realm.objects(Dog.self) // retrieves all Dogs from the default Realm

// Query using a predicate string
var tanDogs = realm.objects(Dog.self).filter("color = 'tan' AND name BEGINSWITH 'B'")

// Query using an NSPredicate
let predicate = NSPredicate(format: "color = %@ AND name BEGINSWITH %@", "tan", "B")
tanDogs = realm.objects(Dog.self).filter(predicate)

// Sorting
let sortedDogs = realm.objects(Dog.self).filter("color = 'tan' AND name BEGINSWITH 'B'").sorted(byKeyPath: "name")

Realm 中的所有查询都是懒加载,当你执行上面这些代码的时候,真正的数据并不会加载到内存中,只有当 Model 的属性被真正访问的时候,它才回被加载。而且查询出来的结果是实时更新的。当数据发生变化时不需要再重新查询一遍。

通知:

查询出来的结果可以添加通知,监听数据库的变化,来改变UI:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37

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

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

// Observe Results Notifications
notificationToken = results.observe { [weak self] (changes: RealmCollectionChange) in
guard let tableView = self?.tableView else { return }
switch changes {
case .initial:
// Results are now populated and can be accessed without blocking the UI
tableView.reloadData()
case .update(_, let deletions, let insertions, let modifications):
// Query results have changed, so apply them to the UITableView
tableView.beginUpdates()
tableView.insertRows(at: insertions.map({ IndexPath(row: $0, section: 0) }),
with: .automatic)
tableView.deleteRows(at: deletions.map({ IndexPath(row: $0, section: 0)}),
with: .automatic)
tableView.reloadRows(at: modifications.map({ IndexPath(row: $0, section: 0) }),
with: .automatic)
tableView.endUpdates()
case .error(let error):
// An error occurred while opening the Realm file on the background worker thread
fatalError("\(error)")
}
}
}

deinit {
notificationToken?.invalidate()
}
}

数据库的迁移

因为瓣读是不断在增加新的功能,所以数据也是不断增加的,这就需要经常迁移数据库,而Realm要做迁移也非常简单,新增删除表和字段,都不需要做额外的操作,只有修改了字段的时候才需要做手动的迁移,这是瓣读数据库迁移的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13

let config = Realm.Configuration(
schemaVersion: 2,
migrationBlock: { migration, oldSchemaVersion in
if (oldSchemaVersion < 1) {
migration.enumerateObjects(ofType: DBCollection.className()) { oldObject, newObject in
if let newObject = newObject, let oldObject = oldObject, let oldId = oldObject["id"] as? Int {
newObject["id"] = String(oldId)
}
}
}
})
Realm.Configuration.defaultConfiguration = config

多线程操作

Realm 是一个MVCC数据库,MVCC是Multiversion concurrency control,多版本并发控制。采用的方法是,每一个连接的线程都会数据在一个特定时刻的快照,采用的是和Git一样的源文件管理算法,每一个线程都有一个特定的数据库版本,就像Git的分支。

因此,Realm 是不允许多个线程访问同一个数据库实例的,必须要每个线程都单独创建一个数据库实例,但线程runloop循环开始的时候,线程中的数据库实例能够自动刷新获取最新的数据库数据,也可以通过执行Realm.refresh() 或者 Realm.commitWrite() 方法来手动刷新。

对于需要跨线程访问的 Model, 需要用 ThreadSafeReference 来包一层:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

let person = Person(name: "Jane")
try! realm.write {
realm.add(person)
}
let personRef = ThreadSafeReference(to: person)
DispatchQueue(label: "background").async {
autoreleasepool {
let realm = try! Realm()
guard let person = realm.resolve(personRef) else {
return // person was deleted
}
try! realm.write {
person.name = "Jane Doe"
}
}
}

如果想要深入了解 Realm 的多线程处理机制,可以查看这篇官方文章:数据库的设计:深入理解Realm的多线程处理机制

缺点

官方列了很多缺点:一些类名、属性名的长度,属性值的大小等等,影响比较大的缺点应该就是 Model 有很多限制:

只能继承自 Object
不能重写 Setter 和 getter 方法

类的继承,也基本不能使用多态特性,子、父类不能互相转换,不能对多个类同时进行查询,也没有多类容器。
而且最蛋疼的一点是,属性不支持基本的数组和字典,有些属性其实就是一个字符串数组,但因为不支持数组,只能为一个字符串新建一个Model,里面只有一个字符串属性。

坚持原创技术分享,您的支持将鼓励我继续创作!