iOS技术资料

iOS数据库之FMDB、Realm、WCDB

2020-06-24  本文已影响0人  苹果上的小豌豆

1.引子FMDB

FMDB详解
FMDB的git链接


2.初见Realm

2.1 什么是Realm

Realm 于2014 年7月发布,是一个跨平台的移动数据库引擎,专门为移动应用的数据持久化而生。其目的是要取代 Core DataSQLite

Realm官网
Realm官方文档
Realm GitHub

2.2 Realm的优缺点

1.跨平台(可以在 iOS 和 Android 平台上共同使用),上手比较简单易用,文档比较完善;
2.可视化Realm 还提供了一个轻量级的数据库查看工具,在Mac Appstore 可以下载“Realm Browser”这个工具,开发者可以查看数据库当中的内容,执行简单的插入和删除数据的操作。

1.基类只能继承自RLMObject ,不能自由继承;
2.字符串数组解析不了([String] ,枚举类型定义复杂);
3.切换分支会崩溃,删掉重装才行;
4.多线程崩溃频发;
5.性能不如WCDB

因为Realm有一部分是Objective-C 编写,一部分是swift 编写。Objective-C 的消息发送是完全动态,而Swift 中的函数可以是静态调用,静态调用会更快。SwiftObjective-C 交互时,Objective-C 动态查找方法地址,就有可能找不到 Swift 中定义的方法。这样就需要在 Swift 中添加一个提示关键字,告诉编译器这个方法是可能被动态调用的,需要将其添加到查找表中。这个就是关键字 dynamic 的作用。

2.3 Realm支持的类型

Realm支持以下的属性类型:BOOL、bool、int、NSInteger、long、long long、float、double、NSString、NSDate、NSData 以及 被特殊类型标记的NSNumber ,注意,不支持集合类型和CGFloat,只有一个集合RLMArray,如果服务器传来的有数组,那么需要我们自己取数据进行转换存储。

2.4 Realm数据库配置

  let schemaVersion: UInt64 = 1
    /// 启动
   func start() {
        /// 数据库地址
        let realmPath = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask,true).last! + "/XZBRealmDataBase/RealmSwift.realm"
        /// 数据迁移
        /*
         为什么要数据迁移?
         假如我们想要更新数据模型,给它添加一个属性,或者更改删除了一个属性。
         在这个时候如果您在数据模型更新之前就已经保存了数据的话,那么` Realm` 就会注意到代码和硬盘上数据不匹配。 每当这时,您必须进行数据迁移,否则当你试图打开这个文件的话` Realm `就会抛出错误。
         */
        let config = Realm.Configuration(
            fileURL: URL.init(fileURLWithPath: realmPath),
            // 设置新的架构版本。这个版本号必须高于之前所用的版本号
               // (如果您之前从未设置过架构版本,那么这个版本号设置为 0)
            schemaVersion: schemaVersion,
            // 设置闭包,这个闭包将会在打开低于上面所设置版本号的 Realm 数据库的时候被自动调用
            migrationBlock: { (migration, oldSchemaVersion) in
                 // 目前我们还未进行数据迁移,因此 oldSchemaVersion == 0
                guard oldSchemaVersion == self.schemaVersion else {
                     // 什么都不要做!Realm 会自行检测新增和需要移除的属性,然后自动更新硬盘上的数据库架构
                    return
                }
                // 手动迁移
                // ...
        })
        // 告诉 Realm 为默认的 Realm 数据库使用这个新的配置对象
        Realm.Configuration.defaultConfiguration = config
        // 触发配置
        //打印出数据库地址
        //使用 Realm Browser 工具可以很方便的对Realm数据库进行读取和编辑(在 App Store 中搜索 Realm Browser 即可下载)。
        print("数据库地址====\(realmPath)")
    }

手动配置数据库地址,Realm.Configuration初始化,使用 Realm Browser工具可以很方便的对.Realm数据库进行读取和编辑(在 App Store 中搜索 Realm Browser即可下载)

2.5 Realm数据库迁移

为什么要数据迁移?

代码见2.4
假如我们想要更新数据模型,给它添加一个属性,或者更改删除了一个属性。 在这个时候如果您在数据模型更新之前就已经保存了数据的话,那么 Realm 就会注意到代码和硬盘上数据不匹配。 每当这时,您必须进行数据迁移,否则当你试图打开这个文件的话 Realm 就会抛出错误。

2.6 Realm数据库模型

class BoxModel: Object {
    /// 名称
    @objc dynamic var boxName: String = ""
    /// 数量
    @objc dynamic var num: Int = 0
    /// 作数据库主键,固定值为1
    @objc dynamic var id: String = ""

    /// 添加主键(Primary Keys)
    static override func primaryKey() -> String? {
           return "id"
    }
    // MARK: model保存
    static func save(boxName: String, num: Int, id: String) {
        let model = BoxModel()
        model.boxName = boxName
        model.num = num
        model.id = id
        let realm = try! Realm()
        try? realm.write {
//            realm.add(model)
            realm.add(model, update: .all)
        }
    }
}
    override static func ignoredProperties() -> [String] {
           return ["num"]
    }
/// 添加主键(Primary Keys)
    override static func primaryKey() -> String? {
           return "id"
    }

2.7 Realm数据库的增删改查操作

let model = BoxModel()
        model.boxName = boxName
        model.num = num
        model.id = id
        let realm = try! Realm()
        try? realm.write {
//            realm.add(model)
         realm.add(model, update: .all)
  }

这里需要注意的是realm.add(model)realm.add(model, update: .all)的区别,如果主键id相同的话使用realm.add(model)会直接导致项目运行崩溃,这一点也是Realm的不足之处,realm.add(model, update: .all)直接就更新了主键相同的表。

    // MARK: 删(清空本地所有数据)
    @objc private func clearData() {
        let realm = try! Realm()
        try! realm.write {
            realm.deleteAll()
        }
    }
    // MARK: 删(删除指定类型的数据)
    private func clearSingleData(id: String) {
        let realm = try! Realm()
        let tem = realm.objects(BoxModel.self).filter("id == %@", id)
        try! realm.write {
            realm.delete(tem)
        }
    }

一种是删除全部数据库表,一种是根据主键ID删除表。

let model = BoxModel()
        model.boxName = boxName
        model.num = num
        model.id = id
        let realm = try! Realm()
        try? realm.write {
         realm.add(model, update: .all)
  }
// 假设主键为 `1` 的 "Book" 对象已经存在
try! realm.write {
    realm.create(BoxModel.self, value: ["id": 1, "boxName": "丰巢"], update: true)
    // BoxModel 对象的 `num ` 属性仍旧保持不变
}
// MARK: 查
    private func getData() {
        let realm = try! Realm()
        let results = realm.objects(BoxModel.self)
    }
    // MARK: 主键查询(查询某张表的某条数据,模型必须包含主键,否则会崩溃)
    private func getDataFromPromaykey() {
        self.dataArr.removeAll()
        let realm = try! Realm()
        guard let model = realm.object(ofType: BoxModel.self, forPrimaryKey: "2") else { return }
        self.dataArr.append(model)
        self.tableView.reloadData()
    }
    // MARK: 条件查询: 根据断言字符串 或者 NSPredicate 谓词 查询某张表中的符合条件数据
    private func getDataFromPredicate() {
        self.dataArr.removeAll()
        let realm = try! Realm()
        let predicate = NSPredicate(format: "boxName contains %@ and num == ","京东", 68)
        let temps = realm.objects(BoxModel.self).filter(predicate)
        self.dataArr.append(contentsOf: temps)
        self.tableView.reloadData()
    }
    // MARK: 数据排序查询
    @objc private func getDataSorted() {
        self.dataArr.removeAll()
        let realm = try! Realm()
        let temps = realm.objects(BoxModel.self).sorted(byKeyPath: "num", ascending: true)
        self.dataArr.append(contentsOf: temps)
        self.tableView.reloadData()
    }

2.8 问题和坑(欢迎补充)


3.WCDB Swift 基础使用

官方文档介绍

3.1 关于 WCDB Swift

3.2 模型绑定

3.2.1 字段映射

class PersonModel: TableCodable {
    var identifier: Int? = nil
    var title: String? = nil
    var num: Int? = nil
    /// 对应数据库表名
    static var tableName: String { "PersonModel" }

    enum CodingKeys: String, CodingTableKey {
        typealias Root = PersonModel
        static let objectRelationalMapping = TableBinding(CodingKeys.self)
        case identifier
        case title
        case num
//        case name
//      case newName = "name"

        static var columnConstraintBindings: [CodingKeys: ColumnConstraintBinding]? {
            return [
                identifier: ColumnConstraintBinding(isPrimary: true)
            ]
        }

    }
    var isAutoIncrement: Bool = true // 用于定义是否使用自增的方式插入
    var lastInsertedRowID: Int64 = 0 // 用于获取自增插入后的主键值

    // MARK: model保存
    static func save(title: String, num: Int) {
        let model = PersonModel()
        model.title = title
        model.num = num
        WCDBDataBaseManager.shared.insertOrReplaceToDb(object: model, table: PersonModel.tableName)
    }
}

1.在类内定义 CodingKeys 的枚举类,并遵循 String 和 CodingTableKey。
2.枚举列举每一个需要定义的字段。
3.对于变量名与表的字段名不一样的情况,可以使用别名进行映射,如 case identifier = "id"
4.对于不需要写入数据库的字段,则不需要在 CodingKeys 内定义,如 debugDescription
5.对于变量名与 SQLite 的保留关键字冲突的字段,同样可以使用别名进行映射,如 offset 是 SQLite 的关键字。

3.2.2 字段约束

ColumnConstraintBinding(
    isPrimary: Bool = false, // 该字段是否为主键。字段约束中只能同时存在一个主键
    orderBy term: OrderTerm? = nil, // 当该字段是主键时,存储顺序是升序还是降序
    isAutoIncrement: Bool = false, // 当该字段是主键时,其是否支持自增。只有整型数据可以定义为自增。
    onConflict conflict: Conflict? = nil, // 当该字段是主键时,若产生冲突,应如何处理
    isNotNull: Bool = false, // 该字段是否可以为空
    isUnique: Bool = false, // 该字段是否可以具有唯一性
    defaultTo defaultValue: ColumnDef.DefaultType? = nil // 该字段在数据库内使用什么默认值
)

3.2.2 自增属性

3.3 数据库初始化

/// wcdb数据库
let dbPath = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask,true).last! + "/XZBWCDBDataBase/WCDBSwift.db"
static let defaultDatabase: Database = {
        return Database.init(withFileURL: URL.init(fileURLWithPath: dbPath))
    }()

3.4 数据库的增删改查

3.4.1 增

  • 插入操作有"insert""insertOrReplace"两个接口。故名思义,前者只是单纯的插入数据,当数据出现冲突时会失败,而后者在主键一致时,新数据会覆盖旧数据。
let model = PersonModel()
 model.isAutoIncrement = false
 WCDBDataBaseManager.shared.insertOrReplaceToDb(object: model, table: PersonModel.tableName)
  • "insert" 函数的原型为:
// insert 和 insertOrReplace 函数只有函数名不同,其他参数都一样。
func insert<Object: TableEncodable>(
    objects: [Object], // 需要插入的对象。WCDB Swift 同时实现了可变参数的版本,因此可以传入一个数组,也可以传入一个或多个对象。
    on propertyConvertibleList: [PropertyConvertible]? = nil, // 需要插入的字段
    intoTable table: String // 表名
) throws

注意:

插入是最常用且比较容易操作卡顿的操作,因此WCDB Swift 对其进行了特殊处理。 当插入的对象数大于 1 时,WCDB Swift 会自动开启事务,进行批量化地插入,以获得更新的性能。

/// 执行事务
try dataBase?.run(transaction: {
       try dataBase?.insert(objects: objects, intoTable: table)
})

3.4.1 删

func delete(fromTable table: String, // 表名
            where condition: Condition? = nil, // 符合删除的条件
            orderBy orderList: [OrderBy]? = nil, // 排序的方式
            limit: Limit? = nil, // 删除的个数
            offset: Offset? = nil // 从第几个开始删除
) throws
  • 1.删(清空本地所有数据)
  • 2.删(删除指定类型的数据)
  • 3.删除 PersonModel 中 按 identifier 升序排列后的前 4 行的后 2 行数据
// MARK: 删(清空本地所有数据)
    @objc private func clearData() {
        WCDBDataBaseManager.shared.deleteFromDb(fromTable: PersonModel.tableName)
    }
    // MARK: 删(删除指定类型的数据)
    private func clearSingleData(identifier: Int) {
        WCDBDataBaseManager.shared.deleteFromDb(fromTable: PersonModel.tableName, where: PersonModel.Properties.identifier == identifier)
    }

   @objc private func clearOtherData() {

        // 删除 PersonModel 中 按 identifier 升序排列后的前 4 行的后 2 行数据
        WCDBDataBaseManager.shared.deleteFromDb(fromTable: PersonModel.tableName, orderBy: [PersonModel.Properties.num.asOrder(by: .ascending)], limit: 2, offset: 4)
    }

3.4.1 改(更新)

func update<Object: TableEncodable>(
    table: String,
    on propertyConvertibleList: [PropertyConvertible],
    with object: Object,
    where condition: Condition? = nil,
    orderBy orderList: [OrderBy]? = nil,
    limit: Limit? = nil,
    offset: Offset? = nil) throws

func update(
    table: String,
    on propertyConvertibleList: [PropertyConvertible],
    with row: [ColumnEncodableBase],
    where condition: Condition? = nil,
    orderBy orderList: [OrderBy]? = nil,
    limit: Limit? = nil,
    offset: Offset? = nil) throws
  • 更新(通过"with object" 接口更新)

    @objc private func updateSingleData(_ indexPath: IndexPath, _ identifier: Int) {
        let model = self.dataArr[indexPath.row]
        model.num = 999
        WCDBDataBaseManager.shared.updateToDb(table: PersonModel.tableName, on: PersonModel.Properties.all, with: model, where: PersonModel.Properties.identifier == identifier)
        getData()
    }
  • 更新(通过"with row"接口更新)
  • "with row"接口则是通过row 来对数据进行更新。row 是遵循 ColumnEncodable 协议的类型的数组。
    private func updateWithRowData(_ indexPath: IndexPath, _ identifier: Int) {
        do {
            let row = [self.dataArr[indexPath.row].title!] as [ColumnEncodable]
            try WCDBDataBaseManager.defaultDatabase.update(table: PersonModel.tableName, on: PersonModel.Properties.title, with: row, where: PersonModel.Properties.identifier == identifier)
            self.getData()
        } catch  {
            print("查询失败:\(error.localizedDescription)")
        }
    }

3.4.1 查

+"getObjects""getObject" 都是对象查找的接口,他们直接返回已进行模型绑定的对象。它们的函数原型为:

func getObjects<Object: TableDecodable>(
    on propertyConvertibleList: [PropertyConvertible],
    fromTable table: String,
    where condition: Condition? = nil,
    orderBy orderList: [OrderBy]? = nil,
    limit: Limit? = nil,
    offset: Offset? = nil) throws -> [Object]

func getObject<Object: TableDecodable>(
    on propertyConvertibleList: [PropertyConvertible],
    fromTable table: String,
    where condition: Condition? = nil,
    orderBy orderList: [OrderBy]? = nil,
    offset: Offset? = nil) throws -> Object?
    private func getDataFromPromaykey() {
        do {
            self.dataArr.removeAll()
            self.dataArr = try WCDBDataBaseManager.defaultDatabase.getObjects(fromTable: PersonModel.tableName,
            where: PersonModel.Properties.identifier < 6 && PersonModel.Properties.identifier > 3)
            self.tableView.reloadData()
        } catch  {
            print("查询失败:\(error.localizedDescription)")
        }

    }

3.5 数据库语言集成查询

语言集成查询(WCDB Integrated Language Query ,简称 WINQ ),是 WCDB 的一项基础特性。它使得开发者能够通过 Swift 的语法特性去完成SQL 语句。

let objects: [Sample] = try database.getObjects(fromTable: PersonModel.tableName, where: PersonModel.Properties.idetifier > 1)

其中 where: 参数后的 PersonModel.Properties.idetifier > 1 就是语言集成查询的其中一个写法。其虽然是identifier 和数字 1 的比较,但其结果并不为Bool 值,而是Expression 。该 Expression 作为 SQLwhere 参数,用于数据库查询。
语言集成查询基于SQLiteSQL语法实现。只要是SQL支持的语句,都能使用语言集成查询完成。也因此,语言集成查询具有和 SQL语法一样的复杂性,具体的可以详见语言集成查询文档

3.6 单例化

class WCDBDataBaseManager: NSObject {

    static let shared = WCDBDataBaseManager()

    static let defaultDatabase: Database = {
        return Database.init(withFileURL: URL.init(fileURLWithPath: dbPath))
    }()

    var dataBase: Database?
    private override init() {
        super.init()
        dataBase = createDb()
    }

    /// 创建db
    private func createDb() -> Database {
        print("wcdb数据库路径==\(dbPath)")
        return Database(withFileURL: URL.init(fileURLWithPath: dbPath))
    }

    /// 创建表
    func createTable<T: TableDecodable>(table: String, of type: T.Type) -> Void {
        do {
            try dataBase?.create(table: table, of: type)
        } catch {
            print(error.localizedDescription)
        }
    }

    /// 插入
    func insertToDb<T: TableEncodable>(objects: [T], table: String) -> Void {
        do {
            /// 如果主键存在的情况下,插入就会失败
            /// 执行事务
            try dataBase?.run(transaction: {
                try dataBase?.insert(objects: objects, intoTable: table)
            })
        } catch {
            print(error.localizedDescription)
        }
    }
    /// 插入或更新
    func insertOrReplaceToDb<T: TableEncodable>(object: T, table: String) -> Void {
        do {
            /// 执行事务
            try dataBase?.run(transaction: {
                try dataBase?.insertOrReplace(objects: object, intoTable: table)
            })
        } catch {
            print(error.localizedDescription)
        }
    }

    /// 修改
    func updateToDb<T: TableEncodable>(table: String, on propertys: [PropertyConvertible], with object: T, where condition: Condition? = nil) -> Void {
        do {
            try dataBase?.update(table: table, on: propertys, with: object, where: condition)
        } catch {
            print(error.localizedDescription)
        }
    }

    /// 删除
    func deleteFromDb(fromTable: String, where condition: Condition? = nil, orderBy orderList: [OrderBy]? = nil, limit: Limit? = nil, offset: WCDBSwift.Offset? = nil) {
        do {
            try dataBase?.run(transaction: {
                try dataBase?.delete(fromTable: fromTable, where: condition, orderBy: orderList, limit: limit, offset: offset)
            })
        } catch {
            print(error.localizedDescription)
        }
    }

    /// 查询
    func qureyObjectsFromDb<T: TableDecodable>(fromTable: String, where condition: Condition? = nil, orderBy orderList: [OrderBy]? = nil, limit: Limit? = nil, offset: Offset? = nil) -> [T]? {
        do {
            let allObjects: [T] = try (dataBase?.getObjects(fromTable: fromTable, where: condition, orderBy: orderList, limit: limit, offset: offset))!
            return allObjects
        } catch {
            print(error.localizedDescription)
        }
        return nil
    }

    /// 查询单条数据
    func qureySingleObjectFromDb<T: TableDecodable>(fromTable: String, where condition: Condition? = nil, orderBy orderList: [OrderBy]? = nil) -> T? {
        do {
            let object: T? = try (dataBase?.getObject(fromTable: fromTable, where: condition, orderBy: orderList))
            return object
        } catch {
            print(error.localizedDescription)
        }
        return nil
    }
}

结语:

WCDB是目前性能体验最优的数据库。
最后,附上demo链接

上一篇 下一篇

猜你喜欢

热点阅读