RealmSwift

官网链接

简介

  • 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 的操作。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,463评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,868评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,213评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,666评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,759评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,725评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,716评论 3 415
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,484评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,928评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,233评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,393评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,073评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,718评论 3 324
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,308评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,538评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,338评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,260评论 2 352

推荐阅读更多精彩内容

  • Realm是由Y Combinator公司孵化出来的一款可以用于iOS(同样适用于Swift&Objective-...
    小歪子go阅读 2,223评论 6 9
  • 跨平台:现在很多应用都是要兼顾iOS和Android两个平台同时开发。如果两个平台都能使用相同的数据库,那就不用考...
    CoderZS阅读 2,488评论 2 16
  • Swift1> Swift和OC的区别1.1> Swift没有地址/指针的概念1.2> 泛型1.3> 类型严谨 对...
    cosWriter阅读 11,094评论 1 32
  • ORA-00001: 违反唯一约束条件 (.) 错误说明:当在唯一索引所对应的列上键入重复值时,会触发此异常。 O...
    我想起个好名字阅读 5,299评论 0 9
  • 手机屏碎了!!早起打卡!!六二十!早餐中!
    小九一阅读 158评论 0 0