RealmSwift

作者: 磊Se | 来源:发表于2019-05-30 14:19 被阅读55次

官网链接

简介

  • Realm是由美国YCombinator孵化的创业团队历时几年打造,第一个专门针对移动平台设计的数据库
  • Realm是一个跨平台的移动数据库引擎,目前支持iOS、Android平台,同时支持Objective-C、Swift、Java、React Native、Xamarin等多种编程语言
  • Realm并不是对SQLite或者CoreData的简单封装, 是由核心数据引擎C++打造,是拥有独立的数据库存储引擎,可以方便、高效的完成数据库的各种操作

优势与亮点

  • 开源
  • 简单易用:使用用Realm,则可以极大地减少学习代价和学习时间
  • 跨平台:使用Realm数据库,iOS和Android无需考虑内部数据的架构,调用Realm提供的API就可以完成数据的交换
  • 线程安全。程序员无需对在不同线程中,对数据库的读取一致性做任何考虑,Realm会保证每次读取都得到一致的数据

使用cocoaPods安装

  • Podfile中,使用user_frameworks!pod 'RealmSwift'
  • 执行命令pod install

使用Realm Studio

  • 为了配合Realm的使用,Realm还提供了一个轻量级的数据库查看工具Realm Studio,借助这个工具,开发者可以查看数据库当中的内容,并执行简单的插入和删除操作。
  • 如果需要调试, 可以通过NSHomeDirectory()打印出Realm数据库地址, 找到对应的Realm文件, 然后用Realm Studio可视化工具打开即可


    Realm Studio

使用Realm框架

1、Realm数据库

  • 本地化数据库(我们主要使用这个,还有内存中数据库)
  • 可同步数据库(使用 Realm 对象服务器 (Realm Object Server) 来实现其内容与其他设备之间的同步)

2、打开Realm数据库

  • 首先初始化一个新的 Realm 对象
  //默认defalut.realm
  let realm = try! Realm()
  try! realm.write {
    realm.add(dog)
  }

第一次创建Realm实例在资源受限的情况下可能会发生错误,使用swift内置的错误处理机制

  do {
    let realm = try Realm()
  } catch let error as NSError {
      // 错误处理
  }
  • 配置Realm,使用Relam.Configuration()
  var config = Realm.Configuration()
  //设置某些类只能存储到当前realm数据库中
  config.objectTypes = [MyClass.self, MyOtherClass.self]
  // 使用默认的目录,但是请将文件名替换为用户名
  config.fileURL = config.fileURL!.deletingLastPathComponent().appendingPathComponent("\(username).realm")
  // 只读
  config.readOnly = true
  // 将该配置设置为默认 Realm 配置 设置后Realm()就是当前Realm对象
  Realm.Configuration.defaultConfiguration = config
  • 异步打开Realm数据库
    如果打开Realm数据库的操作需要耗费大量时间,比如需要执行迁移压缩等操作,建议使用asyncOpen API。
  let config = Realm.Configuration(schemaVersion: 1, migrationBlock: { migration,   oldSchemaVersion in
      // 可能会进行冗长的数据迁移操作
  })
  Realm.asyncOpen(configuration: config) { realm, error in
      if let realm = realm {
          // 成功打开 Realm 数据库,迁移操作在后台线程中进行
      } else if let error = error {
          // 处理在打开 Realm 数据库期间所出现的错误
      }
  }

3、数据模型

创建数据模型
  • 创建数据模型需要继承Object或某个已存在的Realm数据模型类
  • Realm模型对象的绝大部分功能与其他swift对象相同。主要限制在于,您只能在对象被创建的线程中使用该对象。
支持的属性类型
  • Bool、Int、Int8、Int16、Int32、Int64、Double、Float、String、Date 以及 Data。
  • CGFloat 属性被取消了,因为它不具备平台独立性。
  • String、Date 以及 Data 属性都是可空的。Object 属性必须可空。
属性特性
  • 必须使用@objc dynamic 使swift具有oc动态特性
  • 但三种例外属性:LinkingObjectsList以及 RealmOptional,这些属性不能声明为动态类型,因为泛型无法在oc中正确表示。且这些属性应使用let进行声明
class Person: Object {
    // 可空字符串属性,默认为 nil
    @objc dynamic var name: String? = nil
    // 可选 int 属性,默认为 nil
    // RealmOption 属性应该始终用 `let` 进行声明,
    // 因为直接对其进行赋值并不会起任何作用
    let age = RealmOptional<Int>()
    //一般不用可空数据类型,常规写法
    @objc dynamic var num: Int = 0
    let dogs = List<Dog>()
}
关系
  • 多对一关系
class Dog: Object {
    // ... 其余属性声明
    @objc dynamic var owner: Person? // 对一关系必须设置为可空
}

let jam = Person()
let rex = Dog()
rex.owner = jim
  • 多对多关系
    通过List属性
class Person: Object {
    // ...其他属性声明
    let dogs = List<Dog>()
}

您可以照常对 List 属性进行访问和赋值:

let someDogs = realm.objects(Dog.self).filter("name contains 'Fido'")
jim.dogs.append(objectsIn: someDogs)
jim.dogs.append(rex)

List属性会确保其内部插入次序不会被打乱
注意,不支持包含原始类型的List进行查询

  • 双向关系
    上面的例子,只是dog单向的拥有了owner属性,通过LinkingObjects类型使dog和person拥有双向关系
class Dog: Object {
    @objc dynamic var name = ""
    @objc dynamic var age = 0
    let owners = LinkingObjects(fromType: Person.self, property: "dogs")
}
主键、被忽略属性、索引属性
  • 主键 重写 Object.primaryKey() 可以设置模型的主键
  • 被忽略属性 不想某些字段保存在Realm数据库中,可以重写Object.ignoreProperties()
  • 索引属性 重写Object.indexedProperties() 需要为某些特定情况优化读取性能的时候使用
import RealmSwift

// 狗狗的数据模型
class Dog: Object {
    @objc dynamic var id: Int = 0
    @objc dynamic var owner: Person? // 属性可以设置为可选

    override static func primaryKey() -> String? {
        return "id" //这里id 是模型类声明的名称
    }
    override static func ignoreProperties() -> [String] {
        return ["owner"]
    }
}

4、数据库基本操作

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

添加数据
  //(1)创建Dog对象
  let dog: Dog = Dog()
  dog.name = "Wang"
  dog.age = 18
  dog.id = 1
  // (2) 从字典中创建 Dog 对象
  let myOtherDog = Dog(value: ["name" : "Pluto", "age": 3,"id":2])
  // (3) 从数组中创建 Dog 对象
  let myThirdDog = Dog(value: ["Fido", 5, 3])
  try! realm!.write {
        realm!.add(dog)  //注意如果添加已有主键对象会崩溃
  //   realm!.add(dog, update: true)//有主键则更新,无则更新
  }
更新数据
  //更新
  //通过主键更新
  let dog: Dog = Dog()
  dog.age = 19
  dog.id = 1
  dog.name = "Lala"
  //当没有当前主键即增
  try! realm!.write {
            realm!.add(dog, update: true)
  }
 //直接更新
  try! realm!.write {
        dog.age = 20
  }
//键值编码
//`Object`、`Result` 和 `List` 均允许使用 键值编码(KVC)
  let persons = realm.objects(Person.self)
  try! realm.write {
      //第一个person对象的isFirst设置为true
      persons.first?.setValue(true, forKeyPath: "isFirst")
      // 将每个 person 对象的 planet 属性设置为 "Earth"
      persons.setValue("Earth", forKeyPath: "planet")
  }
查询数据
  • 查询将会返回一个 Results实例,其中包含了一组 Object 对象
  • 所有的查询操作(包括检索和属性访问)在 Realm 中都是延迟加载的。只有当属性被访问时,数据才会被读取。
  • 查询结果并不是数据的拷贝,修改查询到的数据将会修改数据库
  • 从 Realm 数据库中检索对象的最基本方法是Realm().objects(_:)],这个方法将会返回 Object 子类类型
//最基本查询方法
let dogs = realm.objects(Dog.self)//从realm数据库查询所有dog对象

//条件查询
// 使用断言字符串来查询
var tanDogs = realm.objects(Dog.self).filter("color = 'tan' AND name BEGINSWITH 'B'")
// 使用 NSPredicate 来查询
let predicate = NSPredicate(format: "color = %@ AND name BEGINSWITH %@", "tan", "B")
tanDogs = realm.objects(Dog.self).filter(predicate)

//链式查询:realm它能够用很小的事务开销来实现链式查询
let tanDogs = realm.objects(Dog.self).filter("color = 'tan'")
let tanDogsWithBNames = tanDogs.filter("name BEGINSWITH 'B'")

  • 排序
    Results 允许您指定一个排序标准,然后基于关键路径、属性或者多个排序描述符来进行排序。例如,下列代码让上述示例中返回的 Dog 对象按名字进行升序排序:
// 对颜色为棕黄色、名字以 "B" 开头的狗狗进行排序
let sortedDogs = realm.objects(Dog.self).filter("color = 'tan' AND name BEGINSWITH 'B'").sorted(byKeyPath: "name")

请注意,sorted(byKeyPath:)sorted(byProperty:) 不支持 将多个属性用作排序基准,此外也无法链式排序(只有最后一个 sorted 调用会被使用)如果要对多个属性进行排序,请使用 sorted(by:) 方法,然后向其中输入多个 SortDescriptor 对象。

  • 限制查询结果
    大多数其他数据库技术都提供了从检索中对结果进行“分页”的能力(例如 SQLite 中的 “LIMIT” 关键字)。这通常是很有必要的,可以避免一次性从硬盘中读取太多的数据,或者将太多查询结果加载到内存当中。
    由于 Realm 中的检索是惰性的,因此这行这种分页行为是没有必要的。因为 Realm 只会在检索到的结果被明确访问时,才会从其中加载对象。
// 循环读取出前 5 个 Dog 对象
// 从而限制从磁盘中读取的对象数量
let dogs = try! Realm().objects(Dog.self)
for i in 0..<5 {
    let dog = dogs[i]
    // ...
}
  • 结果自更新
    查询后结果修改会自动更新,但for...in除外,它会将开始满足条件的遍历完(即使遍历过程中有修改或删除)
  let puppies = realm.objects(Dog.self).filter("age < 2")
  puppies.count // => 0
  try! realm.write {
      realm.create(Dog.self, value: ["name": "Fido", "age": 1])
  }
  puppies.count // => 1
删除对象
  // 在事务中删除对象
try! realm.write {
    realm.delete(cheeseBook)
}
// 从 Realm 数据库中删除所有对象
try! realm.write {
    realm.deleteAll()
}

数据迁移(更新数据库)

假设原有模型类

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

添加fullname属性,去掉firstName和lastName

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

本地迁移

通过设置Realm.Configuration.schemaVersion 以及 Realm.Configuration.migrationBlock可以定义本地迁移。

  // 此段代码位于 application(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.Configuration.defaultConfiguration = Realm.Configuration(
    schemaVersion: 1,
    migrationBlock: { migration, oldSchemaVersion in
        if (oldSchemaVersion < 1) {
            // enumerateObjects(ofType:_:) 方法将会遍历
            // 所有存储在 Realm 文件当中的 `Person` 对象
            migration.enumerateObjects(ofType: Person.className()) { oldObject, newObject in
                // 将两个 name 合并到 fullName 当中
                let firstName = oldObject!["firstName"] as! String
                let lastName = oldObject!["lastName"] as! String
                newObject!["fullName"] = "\(firstName) \(lastName)"
            }
        }
    })

还有属性重命名、线性迁移

通知

  • 通知只会在最初所注册的注册的线程中传递,并且该线程必须拥有一个正在运行的 Run Loop。
  • 无论写入事务是在哪个线程或者进程中发生的,一旦提交了相关的写入事务,那么通知处理模块就会被异步调用。
  • 如果某个写入事务当中包含了 Realm 的版本升级操作,那么通知处理模块很可能会被同步调用。这种情况只会在 Realm 升级到最新版本的时候发生,会抛出异常。可以使用Realm.isInWriteTransaction 来确定是否正处于写入事务当中。
  • 由于通知的传递是通过 Run Loop 进行的,因此 Run Loop 中的其他活动可能会延迟通知的传递。如果通知无法立即发送,那么来自多个写入事务的更改可能会合并到一个通知当中。
Realm通知

当整个 Realm 数据库发生变化时,就会发送通知

// 注册 Realm 通知
let token = realm.observe { notification, realm in
    viewController.updateUI()
}

// 随后
token.invalidate()
集合通知

通过传递到通知模块当中的 RealmCollectionChange 参数来访问这些变更。该对象存放了受删除 (deletions)、插入 (insertions) 以及修改 (modifications) 所影响的索引信息

  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")

        // 订阅 Results 通知
        notificationToken = results.observe { [weak self] (changes: RealmCollectionChange) in
            guard let tableView = self?.tableView else { return }
            switch changes {
            case .initial:
                // Results 现在已经填充完数据,无需阻塞 UI 便可直接访问
                tableView.reloadData()
            case .update(_, let deletions, let insertions, let modifications):
                // 检索结果发生改变,将其应用到 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):
                // 在后台工作线程中打开 Realm 文件发生了错误
                fatalError("\(error)")
            }
        }
    }

    deinit {
        notificationToken?.invalidate()
    }
}
对象通知

您可以在特定的 Realm 对象上进行通知的注册,这样就可以在此对象被删除时、或者该对象所管理的属性值被修改时,获取相应的通知。

  class StepCounter: Object {
    @objc dynamic var steps = 0
}

let stepCounter = StepCounter()
let realm = try! Realm()
try! realm.write {
    realm.add(stepCounter)
}
var token : NotificationToken?
token = stepCounter.observe { change in
    switch change {
    case .change(let properties):
        for property in properties {
            if property.name == "steps" && property.newValue as! Int > 1000 {
                print("Congratulations, you've exceeded 1000 steps.")
                token = nil
            }
        }
    case .error(let error):
        print("An error occurred: \(error)")
    case .deleted:
        print("The object was deleted.")
    }
}
界面驱动更新
  • Realm 的通知总是以异步的方式进行传递,因此这些操作永远不会阻塞主 UI 线程,也不会导致应用卡顿
  • 有时我们需要在主线程进行同步传递,并能够立即反映在 UI 的时候Realm提供了Realm.commitWrite(withoutNotifying:)
  // 添加细粒化通知模块
token = collection.observe { changes in
    switch changes {
    case .initial:
        tableView.reloadData()
    case .update(_, let deletions, let insertions, let modifications):
        // 检索结果发生改变,将其应用到 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):
        // 处理错误
        ()
    }
}

func insertItem() throws {
     // 在主线程执行界面驱动更新:
     collection.realm!.beginWrite()
     collection.insert(Item(), at: 0)
     // 随后立即将其同步到 UI 当中
     tableView.insertRows(at: [IndexPath(row: 0, section: 0)], with: .automatic)
     // 确保变更通知不会再次响应变更
     try collection.realm!.commitWrite(withoutNotifying: [token])
}
键值观察
  • Realm 对象的大多数属性都遵从键值观察机制。所有 Object 子类的持久化存储(未被忽略)的属性都是遵循 KVO 机制的,并且 Object 以及 List 中的无效属性也同样遵循(然而 LinkingObjects 属性并不能使用 KVO 进行观察)。

加密

  • Realm 支持在创建 Realm 数据库时采用64位的密钥对数据库文件进行 AES-256+SHA2 加密。
  // 生成随机秘钥
var key = Data(count: 64)
_ = key.withUnsafeMutableBytes { bytes in
    SecRandomCopyBytes(kSecRandomDefault, 64, bytes)
}

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

线程

  • 单个线程中无需考虑并行或多线程处理问题。
  • Realm 通过确保每个线程始终拥有 Realm 的一个快照,以便让并发运行变得十分轻松。
  • 不能让多个线程都持有同一个 Realm 对象的实例

检视其他线程上的变化

  • 在主 UI 线程中(或者任何一个位于 runloop 中的线程),对象会在 runloop 的每次循环过程中自行获取其他线程造成的更改
  • Realm 会自每个 runloop 循环的开始自动进行刷新,除非 Realm 的 autorefresh 属性设置为 NO。如果某个线程没有 runloop 的话(通常是因为它们被放到了后台进程当中),那么 Realm.refresh() 方法必须手动调用,以确保让事务维持在最新的状态当中。
跨线程传递实例
跨线程使用 Realm 数据库

JSON

Realm 没有提供对 JSON 的直接支持,但是您可以使用 NSJSONSerialization.JSONObjectWithData(_:options:) 的输出,实现将 JSON 添加到 Object 的操作。

相关文章

网友评论

    本文标题:RealmSwift

    本文链接:https://www.haomeiwen.com/subject/jesdtctx.html