iOS数据库-Realm官方文档(一)

Realm是一个移动端数据库容器.本文根据Realm的官方文档自行翻译, 如有不准望指正.

Realm是一个跨平台的移动数据库引擎,目前支持iOS, Android, Swift, Java, Reat等多种语言.

Realm并不是对SQLite或者CoreData的简单封装, 是由核心数据引擎C++打造,是拥有独立的数据库存储引擎,可以方便、高效的完成数据库的各种操作.

打开本地Realm

开启Realm的方法就是初始化一个Realm对象

1
2
3
4
5
let realm = try! Realm()

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

配置本地Realm

再打开数据库之前创建一个 Realm.Configuration对象, 然后赋值给对应的属性, 常见配置如下:

  • 数据库路径
  • 数据库的迁移函数
  • 数据压缩函数(用来优化数据库的磁盘空间)

配置可以通过如下两种方式实现:

1
2
3
Realm(configuration: config)

Realm.Configuration.defaultConfiguration = config

例如: 根据指定用户名访问不同的数据库

1
2
3
4
5
6
7
8
9
func setDefaultRealmForUser(username: String) {
    var config = Realm.Configuration()

    // Use the default directory, but replace the filename with the username
    config.fileURL = config.fileURL!.deletingLastPathComponent().appendingPathComponent("\(username).realm")

    // Set this as the configuration used for the default Realm
    Realm.Configuration.defaultConfiguration = config
}

你可以配置多个参数, 来控制版本,url等信息:

1
2
3
4
5
6
7
8
9
10
11
let config = Realm.Configuration(
    // 获取bundle资源的url
    fileURL: Bundle.main.url(forResource: "MyBundledData", withExtension: "realm"),
    // 使用只读的方式打开bundle资源, 因为bundle资源一般是不可写的
    readOnly: true)

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

// 按照条件读取数据库中的资源
let results = realm.objects(Dog.self).filter("age > 5")

默认Realm

我们可以使用 Realm() 可以生成一个名为 default.realm, 并存放在Documents中的数据库.

内存数据库(in-memory Realm)

通过设置 inMemoryIdentifier 而不是 fileURLRealm.Configuration 我们就能创建一个完全运行在内存中的数据库.

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

内存数据库在暂存目录中写入了多个文件. 当in-memory Realm对象被释放时所有存储在其中的数据都会被删除. 所以建议在app运行时创建一个被强引用的in-memory Realm对象.

错误处理

和其他I/O操作一样, 创建一个Realm对象有时候也会失败. 实际上这种情况只会出现在第一次创建的时候.因为随后在相同线程中获取Realm将会使用第一次创建的缓存,并且获取缓存不会失败.

处理第一次获取Realm对象时的失败情况我们使用的是Swift内置的错误处理机制:

1
2
3
4
5
do {
    let realm = try Realm()
} catch let error as NSError {
    // handle error
}

创建Realm产生的附件

在 xxx.realm文件产生的同时, Realm也会生成或者维护额外的文件和目录来完成它的内部操作. 例如:

  • .realm.lock - 资源锁文件.
  • .realm.management - 内部处理的锁目录.
  • .realm.note - 通知名录文件.

以上文件对 .realm没有任何影响, 删掉其父级数据库文件不会对他们造成任何影响.

捆绑 Reaml

app初始化的时候可能需要很多数据, 也就是说在我们第一次启动app时,我们可以使用Realm预埋数据.方法如下;

  1. 给数据库添加数据, 一旦创建好数据模型就不能轻易改变.Reaml是跨平台的, 你可以使用macOS或者iOS模拟器运行JSONImport
  2. 在生成Reaml文件的的代码地方, 你应该做一个压缩副本(参考:Realm().writeCopyToPath(_:encryptionKey:)). 这样可以减小Realm文件的大小,使你的app包变小一些.
  3. 将第2步中的压缩副本拖入xcode项目中
  4. 在xcode中找到 build phases ,并将Realm文件添加到 Copy Bundle Resources 中.
  5. 现在资源目录中的Reamlm文件已经能被app访问. 你能通过 NSBundle.main.pathForResource(_:ofType:)找到.
  6. 如果资源目录中的Realm文件包含一些固定的数据,你可以设置 Realm.Configuration 为只读 readOnly = true 来打开 Realm. 当然,如果这些初始化的数据需要修改, 你可以将bundle中的realm文件通过NSFileManager.default.copyItemAtPath(_:toPath:)复制到程序的Documents目录中,然后再做修改.

要了解更多使用bundled Realm的使用方法请访问migration sample app

类分组(Class subsets)

有些情况你想限定哪些类能被存到你指定的Realm. 例如, 协同开发过程中, 都在使用Realm, 你就可以限定对象类型.

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

压缩数据(Compacting Realms)

Realm工作工程中Realm文件总是会比存储在其内部所有文件要大. 为了避免在调用方法时消耗过多资源, Realm不会在运行时进行压缩. 相反,他们的大小会不端增加.当然也会出现没有用处的Realm占用空间的情况.为了处理这个问题,你可以在配置文件中规定在什么情况下要压缩Realm文件. 例如;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
let config = Realm.Configuration(shouldCompactOnLaunch: { totalBytes, usedBytes in
    // totalBytes 文件在磁盘中的总大小 (data + free space)
    // usedBytes 使用数据占用自盘的空间大小

    // 如果文件大小超过100M并且可用控件小于50%
    let oneHundredMB = 100 * 1024 * 1024
    return (totalBytes > oneHundredMB) && (Double(usedBytes) / Double(totalBytes)) < 0.5
})
do {
    // Realm 第一次启动时如果上面的条件满足, 就会压缩
    let realm = try Realm(configuration: config)
} catch {
    // handle error compacting or opening Realm
}
  • 压缩操作过程是现将所有文件读出来, 写到另一个路径中, 然后替换原来的文件. 如果数据量很大, 这将是一个很昂贵的操作.

  • 你需要在良好的性能和占用空间大小这两个因素中找到一个平衡点.即在保持良好性能的同时尽量控制数据文件占用内存的大小.

  • 最后要注意一点: 当Realm正在被访问时不会进行压缩操作, 即使符合上面block中的条年也不会, 因为当Realm被访问时如果进行压缩操作不能保证此次访问的安全

删除Realm文件 (Deleting Realm files)

如果你想清缓存或者重置数据, 可能需要将Realm从磁盘中删除.除非显示的声明,Realm会禁止将数据拷贝到内存中,所有被Realm管理的资源在删除之前必须先被释放. 包括从Realm中读取的或者被添加进去的所有列表, 对象以及Realm自己.

实际上删除操作应该在app启动时,打开Realm之前或者在显示声明的自动释放池中打开的Realm释放之后.

最后再删除Realm的同时,你也要删掉Realm的所有附件文件.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
autoreleasepool {
    // all Realm usage here
}
let realmURL = Realm.Configuration.defaultConfiguration.fileURL!
let realmURLs = [
    realmURL,
    realmURL.appendingPathExtension("lock"),
    realmURL.appendingPathExtension("note"),
    realmURL.appendingPathExtension("management")
]
for URL in realmURLs {
    do {
        try FileManager.default.removeItem(at: URL)
    } catch {
        // handle error
    }
}

模型(Models)

Realm数据的模型和常规Swift的类的定义方式一致. 但是必须继承自 Object 或者Realm模型类的子类.你可以定义方法, 遵循协议等等.但是一定要注意: 你只能在你创建的线程中使用它, 不能跨线程使用

定义数组

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

// Dog model
class Dog: Object {
    @objc dynamic var name = ""
    @objc dynamic var owner: Person? // 可选类型
}

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

Realm在app启动时就会解析模型,所以所有模型必须是有效的, 即使他们不会被用到. 而且所有非可选类型必须有默认值.

支持的类型(Supported property types)

  • Realm支持如下类型: Bool, Int, Int8, Int16, Int32, Int64, Double, Float, String, Date, and Data.

  • CGFloat 不可使用,因为他OC独有的, 不能跨平台.

  • String, Date and Data 支持可选类型. Object必须是可选类型. 存储基本数据类型的可选类型使用 RealmOptional.

不可选属性(Required properties)

String, Date, Data可以使用可选或者不可选.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Person: Object {
    
    @objc dynamic var name: String? = nil

    // 可选类型默认nil
    
    // 基本数据类型的可选类型必须使用 `let`,
    // 不推荐直接赋值
    let age = RealmOptional<Int>()
}

let realm = try! Realm()
try! realm.write() {
    var person = realm.create(Person.self, value: ["Jane", 27])
    person.age.value = 28
}

RealmOptional 支持 Int, Float, Double, Bool, 以及Int的不同类型 (Int8, Int16, Int32, Int64).

主键(Primary keys)

可以通过重写Object.primaryKey()方法设置模型的主键.但是一旦主键被设置就不能更改.

1
2
3
4
5
6
7
8
class Person: Object {
    @objc dynamic var id = 0
    @objc dynamic var name = ""

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

索引属性(Indexing properties)

索引一个属性可以通过重写 Object.indexedProperties(). 和主键一样, 索引会稍微减慢写数据的速度(同时也会使Realm文件变大一些), 但是可以提高查询的速度. 所以最好在需要做查询优化的表中使用索引. Realm支持索引的类型有 string, integer,boolean, date

1
2
3
4
5
6
7
8
class Book: Object {
    @objc dynamic var price = 0
    @objc dynamic var title = ""

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

忽略属性(Ignoring properties)

如果你不想保存模型中的摸个字段,重写 Object.ignoredProperties() 即可.Realm不会干涉这些被忽略属性的常规操作, 你可以重写他们的get和set方法, 以及KVO等,但是即使他们继承自Object但是不能使用Realm特有的函数 .并且,被声明为只读属性的会自动被Realm忽略.

1
2
3
4
5
6
7
8
9
10
11
class Person: Object {
    @objc dynamic var tmpID = 0
    var name: String { //只读属性的会自动被Realm忽略.
        return "\(firstName) \(lastName)"
    }
    @objc dynamic var firstName = ""
    @objc dynamic var lastName = ""

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

属性特点(Property attributes)

所有Realm数据模型属性必须以@objc dynamic var开头,除了三种列外情况: LinkingObjects, List 以及 RealmOptional.这三中类型不能被定义为dynamic,因为泛型在OC运行时不能被表示.这三种类型必须用let定义.

属性对应清单

Type Non-optional Optional
Bool @objc dynamic var value = false let value = RealmOptional()
Int @objc dynamic var value = 0 let value = RealmOptional()
Float @objc dynamic var value: Float = 0.0 let value = RealmOptional()
Double @objc dynamic var value: Double = 0.0 let value = RealmOptional()
String @objc dynamic var value = “” @objc dynamic var value: String? = nil
Data @objc dynamic var value = Data() @objc dynamic var value: Data? = nil
Date @objc dynamic var value = Date() @objc dynamic var value: Date? = nil
Object n/a: must be optional @objc dynamic var value: Class?
List let value = List() n/a: must be non-optional
LinkingObjects let value = LinkingObjects(fromType: Class.self, property: “property”) n/a: must be non-optional

使用Realm对象

自动更新对象(Auto-updating objects)

对象实例是实时的,自动更新的.有不用手动去刷新数据对象.修改一个对象的属性,可以在其被引用的其他地方立刻反应出来.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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的高效, 也是代码更加简单明了.如果你的UI依赖一个Realm对象, 你完全不用担心触发UI重绘时数据的刷新和重复获取. 你可以直接订阅一个Realm 通知去监听Realm数据的变化,然后更新UI.

模型的继承(Model inheritance)

Realm允许继承,但是一些运行时特性却不被支持. 支持特性如下:

  1. 类方法, 对象方法,以及父类属性可以被子类继承.
  2. 方法和函数的参数为父类的可以在子类运行.

如下特性不支持:

  1. 多态类型转化(子类转化为子类, 子类转化为父类)
  2. 多线程查询
  3. 多种对象容器

    // Base Model class Animal: Object { @objc dynamic var age = 0 }

    // Models composed with Animal class Duck: Object { @objc dynamic var animal: Animal? = nil @objc dynamic var name = “” } class Frog: Object { @objc dynamic var animal: Animal? = nil @objc dynamic var dateProp = Date() }

    // Usage let duck = Duck(value: [ “animal”: [ “age”: 3 ], “name”: “Gustav” ])

集合

Realm有多重能表示集合的类型, 如下:

  1. Results, 专门用来容纳查询结果的集合
  2. List
  3. LinkingObject标示类关系的模型列表
  4. RealmCollection,这个是所有集合对象都要遵循的协议
  5. AnyRealmCollection,能代表Results, ListLinkingObjects.

使用RealmCollection协议泛型的用法如下:

1
2
3
4
func operateOn<C: RealmCollection>(collection: C) {
    // Collection could be either Results or List
    print("operating on collection containing \(collection.count) objects")
}

由于Swift类型限制, 使用AnyRealmCollection做类型约束:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class ViewController {
//    let collection: RealmCollection
//                    ^
//                    error: protocol 'RealmCollection' can only be used
//                    as a generic constraint because it has Self or
//                    associated type requirements
//
//    init<C: RealmCollection>(collection: C) where C.ElementType == MyModel {
//        self.collection = collection
//    }

    let collection: AnyRealmCollection<MyModel>

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

数据库之间的对象复制(Copying objects between Realms)

Real之间的对象复制很简单,使用Realm().create(_:value:update:). 一步到位.但是要注意Realm对象只能在第一次创建它的线程操作,所以复制也只有在相同线程才会起作用.另外复制的对象也不能包含涉及集成关系.

关系(Relationships)

建立两个Realm之间的关系在Realm中不论是运行速度还是占用内存都做了非常好的优化.

多对一

1
2
3
4
class Dog: Object {
//to-one必须是可选类型
@objc dynamic var owner: Person?
}

多对多

1
2
3
4
class Person: Object {
    //other property declarations
    let dogs = List<Dog>()
}

关系反转(Inverse relationships)

按照上面的例子, 如果Person.dogs连接到一个Dog实例, 你能根据person,找到dog, 但是想通过dog找到Person是不可能的. 当然你可以设置一对一的关系将dog.owner连接到Person,但是这些关系每个都是相对独立的. 添加一条dog到person.owner不会设置dog.owner到对应Person.为了解决这个问题Realm提供了如下方法:

1
2
3
4
5
class Dog: Object {
    @objc dynamic var name = ""
    @objc dynamic var age = 0
    let owners = LinkingObjects(fromType: Person.self, property: "dogs")
}

通过连接对象属性你可以获取连接到是定属性的所有对象.