美文网首页iOS学习
FMDB、Realm、WCDB

FMDB、Realm、WCDB

作者: 梦回蓝桥 | 来源:发表于2020-01-11 18:28 被阅读0次

    FMDB、Realm、WCDB 区别联系

    本篇内容包含以下三部分

    1: 性能、遇到的问题等优缺点对比
    2: 基本(增、删、改、查、建库、建表、数据库迁移)等使用对比
    3: WCDB.Swift详细使用及项目中遇到的问题

    一: 性能、遇到的问题等优缺点对比

    FMDB:

    优点:较为轻量级的sqlite封装,API较原生使用方便许多,对SDK本省的学习成本较低,基本支持sqlite的所有能力,如事务等

    缺点:不支持ORM(模型绑定:Object-relational Mapping),需要每个编码人员写具体的sql语句,没有较多的性能优化,数据库操作相对复杂,关于数据加密、数据库升级等操作需要用户自己实现

    现实使用问题:
    1:不支持ORM,需要编写大量代码来实现具体sql
    2:没有较多的性能优化,数据库操作复杂
    3:没在消息这种模块使用,估计也会有很多问题

    WCDB.Swift:

    优点:跨平台,支持ORM,sqlite的深度封装,基类支持自己继承,不需要用户写sql,基本支持sqlite所有能力,内部较多的性能优化,文档较完善(参考资源稀缺)扩展实现了错误统计、性能统计、损坏修复、反注入、加密等功能
    缺点:内部基于c++实现,相关使用文档较为稀缺

    现实使用问题:
    相关使用文档稀缺,只能摸索前行
    如开发中遇到了以下问题:

    1:模型嵌套:官方只有一个错误的事例
    2:模糊查询(资源较少,api中没有注释,6、7十个相关方法中琢磨)
    样式:WCDBConversation.Properties.msgPreview.like("%(key!)%")
    3:多表切换(后期发现的问题)
    4:已解决想不起来的问题

    Realm.Swift:

    优点:跨平台,支持ORM,文档完善(怀疑),零拷贝提升性能,提供了配套可视化工具(Realm Browser)
    缺点:不是基于sqlite的关系型数据库,不能或很难建立表之间的关联关系,项目中遇到类似场景可能较难解决; 基类只能继承自RLMObject,不能自由继承,不方便实现类似JsonModel等属性绑定

    现实使用问题:
    1:基类只能继承自RLMObject(Swift版是Object),不能自由继承,不方便实现类似JsonModel等属性绑定
    2:字符串数组解析不了([String],枚举类型定义复杂)
    3:多线程崩溃频发
    4:枚举定义复杂
    5:其他一堆。。。
    6:突然想到的多表切换问题,貌似也不好处理,它是直接传入模型映射创建的表格

    性能剖析(截自网络):

    Realm&FMDB.png WCDB&Realm&Sqlite.png

    上述两张截图只是其中挑选出的两张,总体来说性能上WCDB&Realm是优于FMDB&Sqlite的,其中WCDB性能稍微领先Realm

    二:基本(增、删、改、查、建库、建表、数据库迁移)等使用对比

    FMDB
    git :https://github.com/ccgus/fmdb
    使用 :https://www.jianshu.com/p/42bb816fe422
    版本迁移:FMDBMigrationManager

    Realm.Swift
    git:https://github.com/realm/realm-cocoa.git
    中文地址:https://realm.io/cn/docs/swift/latest/

    使用:

    class RealmMsg: Object, ApiModel {
        @objc dynamic var id = ""
        @objc dynamic var memberId = "" // 发消息的人的id, hint类型的member_id 是0
        @objc dynamic var metaType = "" // 消息类型, Control(控制类),Text, Image, Audio...
        @objc dynamic var conversation: RealmConversation?
        @objc dynamic var conversationId = ""
        @objc dynamic var content = "" // 具体消息类型的详细内容的Json字符串
        @objc dynamic var createdAt: Date? // 消息的创建时间, 使用日期? 或者 时间戳?
        @objc dynamic var msgPreview = "" // 消息内容的文本描述, 用于更新conversation的 last_msg
        // 设置主键
        override static func primaryKey() -> String? {
            return "id"
        }
        //重写 Object.ignoredProperties() 可以防止 Realm 存储数据模型的某个属性
        override static func ignoredProperties() -> [String] {
            return ["conversation"]
        }
    
        enum CodingKeys: String, CodingKey {
            case id
            case memberId = "member_id"
            case metaType = "meta_type"
            case conversationId
            case content
            case createdAt = "created_at"
            case msgPreview = "msg_preview"
        }
    }
    
    class RealmConversation: Object, ApiModel {
        @objc dynamic var id = ""
        @objc dynamic var user : RealmMember?
        @objc dynamic var memberId = ""
        @objc dynamic var schema = "" // 会话点击跳转的schema
        @objc dynamic var iconSchema = "" // 头像点击跳转的schema
        @objc dynamic var rank = 0 // 排序值
        @objc dynamic var firstLevel = false // 是否一级会话
        @objc dynamic var conversationType = "" // 会话类型
        var conversation_type : ConversationType? {
            get {
                return ConversationType(rawValue: conversationType)
            }
            set {
                conversationType = newValue?.rawValue ?? ""
            }
        }
        // 会话类型
        @objc dynamic var targetReadAt: Date? // 对方最后一次阅读的时间, 用于标记会话的已读未读
        @objc dynamic var memberReadAt: Date? // 自己最后一次阅读的时间, 用于换设备标记会话的已读未读
        var tags = List<String>()
        @objc dynamic var unReadCount = 0  // 消息未读数,数据库用,用于展示会话列表未读消息数
        @objc dynamic var createdAt: Date? // 创建时间,数据库用,用于展示会话列表最后一条消息时间
        @objc dynamic var msgPreview = ""  // 消息内容,数据库用,用于展示会话列表最后一条消息
        @objc dynamic var isLiked = false  // 是否已喜欢,二级列表使用,判断点赞还是发消息
        @objc dynamic var nickName = ""    // 昵称,搜索用
        // 设置主键
        override static func primaryKey() -> String? {
            return "id"
        }
        //重写 Object.ignoredProperties() 可以防止 Realm 存储数据模型的某个属性
        override static func ignoredProperties() -> [String] {
            return ["user"]
        }
        enum CodingKeys: String, CodingKey {
            case id
            case memberId = "member_id"
            case schema
            case iconSchema = "icon_schema"
            case rank
            case firstLevel = "first_level"
            case conversationType = "conversation_type"
            //case conversation_type
            case targetReadAt = "target_read_at"
            case memberReadAt = "member_read_at"
            case unReadCount
            case createdAt
            case msgPreview
            case isLiked
            case nickName
            //case tags
        }
    }
    
    class RealmMember: Object, ApiModel {
        @objc dynamic var id = ""
        @objc dynamic var memberId = 0 //数字id 埋点使用
        @objc dynamic var nickName = ""
        @objc dynamic var sex = 0
        @objc dynamic var age = 0
        @objc dynamic var avatarUrl = ""
        @objc dynamic var isVip = false
        @objc dynamic var isCupid = false //红娘月老 邀请相亲使用
        @objc dynamic var online = 1 //是否在线
        var isOnline: OnlineStatus? {
            get {
                return OnlineStatus(rawValue: online)
            }
            set {
                online = newValue?.rawValue ?? 0
            }
        }
        @objc dynamic var location = ""
        // 设置主键
        override static func primaryKey() -> String? {
            return "id"
        }
        enum CodingKeys: String, CodingKey {
            case id
            case nickName = "nick_name"
            case sex
            case age
            case avatarUrl = "avatar_url"
            case isVip = "is_vip"
            case online
            case location
            case memberId
            case isCupid
        }
        func onlineStatusDesc() -> String {
            guard let onlineStatus = isOnline else {
                return ""
            }
            switch onlineStatus {
            case .Online:
                return "现在在线"
            case .Leave:
                return "刚刚在线"
            case .Offline:
                return "暂时不在线"
            default:
                return "未知状态"
            }
        }
        var statusColor: UIColor {
            guard let onlineStatus = isOnline else {
                return UIColor.clear
            }
            switch onlineStatus {
            case .Leave:
                return YDPointLeaveColor
            case .Online:
                return YDPointOnlineColor
            default:
                return UIColor.clear
            }
        }
    }
    
    class RealmManage: NSObject {
    
        static let shared = RealmManage()
    
        /// 线程可能会变,此处使用计算属性可以随时更改realm所处线程
        var realm: Realm {
             return try! Realm()
         }
    
        /// 增
        func addModel<T>(model: T) {
            do {
                try realm.write {
                    realm.add(model as! Object)
                    log.info("model:插入成功")
                }
            } catch {
                log.info("插入model失败:\(error)")
            }
        }
    
        func addModels<T>(models:[T]) {
            do {
                try realm.write {
                    realm.add(models as! [Object], update: .all)
                }
            } catch {
                log.info("插入models失败:\(error)")
            }
        }
    
        /// 删
        func deleteModel<T>(model: T) {
            do {
                try realm.write {
                    realm.delete(model as! Object)
                }
            } catch {}
        }
    
        /// 删除某张表
        func deleteModelList<T>(model: T) {
            do {
                try realm.write {
                    realm.delete(realm.objects((T.self as! Object.Type).self))
                }
            } catch {}
        }
    
        /// 改 根据主键(urlString)修改:调用此方法的模型必须具有主键
        func updateModel<T>(model: T) {
            do {
                try realm.write {
                    realm.add(model as! Object, update: .all)
                }
            } catch {}
        }
    
        func updateModels<T>(models: [T]) {
            do {
                try realm.write {
                    realm.add(models as! [Object], update: .all)
                }
            } catch {
                log.info("批量更新失败")
            }
        }
    
        /// 查
        func queryModel<T>(model: T, filter: String? = nil) -> [T] {        
            var results: Results<Object>
            if filter != nil {
                results = realm.objects((T.self as! Object.Type).self).filter(filter!)
            } else {
                results = realm.objects((T.self as! Object.Type).self)
            }
            guard results.count > 0 else { return [] }
            var modelArray = [T]()
            for model in results {
                modelArray.append(model as! T)
            }
            return modelArray
        }
    }
    

    总结:
    同样使用泛型增删改查的语法是这几个数据库中最简练的

    /// 插入或更新(与WCDB相同操作对比):
    Realm:RealmManage.shared.updateModel(model: model)
    WCDB:WCDBManage.shared.insertOrUpdateToDb(table: k_member_table, with: [model])
    


    查询:

    /// 根据id查询
    Realm:RealmManage.shared.queryModel(model: RealmConversation(), filter: "id = '\(msg.conversation?.id ?? "")'")
    WCDB:WCDBManage.shared.qureyObjectFromDb(fromTable: k_conversation_table, cls: WCDBConversation.self, where: WCDBConversation.Properties.id == msg.conversation?.id ?? 0)
    
    /// 搜索(`值得注意`:因为WCDB使用文档较少,基本查询不到)
    Realm:RealmManage.shared.queryModel(model: RealmConversation(), filter: "msgPreview CONTAINS '\(key!)' OR nickName CONTAINS '\(key!)'")
    WCDB:WCDBManage.shared.qureyFromDb(fromTable: k_conversation_table, cls: WCDBConversation.self, where: WCDBConversation.Properties.msgPreview.like("%\(key!)%") || WCDBConversation.Properties.nickName.like("%\(key!)%"), orderBy: nil, limit: nil, offset: nil)
    .
    .
    .
    


    删除:

    Realm:RealmManage.shared.deleteModel(model: deleteConversation)
    WCDB:WCDBManage.shared.deleteFromDb(fromTable: k_conversation_table, where: WCDBConversation.Properties.id == (deleteConversation.id ?? 0), orderBy: nil, limit: nil, offset: nil)
    .
    .
    .
    


    简单使用是没什么大问题的,使用成本的话可能有点高,后期会将关于多线程崩溃等问题解决

    数据迁移比较简单在这里就不说明了

    三:WCDB.Swift详细使用及项目中遇到的问题

    WCDB.Swift

    git:https://github.com/Tencent/wcdb/wiki

    介绍:
    模型绑定(Object-relational Mapping,简称 ORM),通过对 Swift 类或结构进行绑定,形成类或结构 - 表模型、类或结构对象 - 表的映射关系,从而达到通过对象直接操作数据库的目的。

    WCDB Swift 的模型绑定分为五个部分:

    1: 字段映射
    2: 字段约束
    3: 索引
    4: 表约束
    5: 虚拟表映射

    1: 字段映射

    WCDB.Swift 的字段映射基于 Swift 4.0 的 Codable 协议实现

    class WCDBMsg: TableCodable, ApiModel {
        var id: Int? = nil
        var memberId: String? = nil // 发消息的人的id, hint类型的member_id 是0
        var metaType: String? = nil // 消息类型, Control(控制类),Text, Image, Audio...
        var conversationId: Int? = nil
        var content: String? = nil // 具体消息类型的详细内容的Json字符串
        var createdAt: String? = nil // 消息的创建时间, 使用日期? 或者 时间戳?
        var msgPreview: String? = nil // 消息内容的文本描述, 用于更新conversation的 last_msg
    
        var conversation: WCDBConversation?
    
        enum CodingKeys: String, CodingKey, CodingTableKey {
            typealias Root = WCDBMsg
            static let objectRelationalMapping = TableBinding(CodingKeys.self)
            case id
            case memberId = "member_id"
            case metaType = "meta_type"
            case conversationId
            case content
            case createdAt = "created_at"
            case msgPreview = "msg_preview"
            case conversation /// 这里不加入conversation字段,就不会将嵌套的模型加进数据库存储
            /// 约束(主键约束、非空约束、唯一约束、默认值ColumnConstraintBinding)
            static var columnConstraintBindings: [CodingKeys: ColumnConstraintBinding]? {
                return [
                    // id: ColumnConstraintBinding(isPrimary: true),
                    id: ColumnConstraintBinding(isUnique: true),
                ]
            }
            /// 索引
            static var indexBindings: [IndexBinding.Subfix: IndexBinding]? {
                return [
                    "_uniqueIndex": IndexBinding(isUnique: true, indexesBy: id),
                    "_descendingIndex": IndexBinding(indexesBy: createdAt.asIndex(orderBy: .descending)),
                ]
            }
        }
    }
    

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

    字段映射支持类型:

    Bool, Int, Int8, Int16, Int32, UInt, UInt8, UInt16, UInt32, Int64, UInt64, Date, Float, Double, String, URL, Data, Array, Dictionary, Set
    支持自定义类型(模型嵌套,有坑)

    2: 字段约束:
    ColumnConstraintBinding

    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: 索引:
    索引是 TableEncodable 的一个可选函数,可根据需求选择实现或不实现。它用于定于针对单个或多个字段的索引,索引后的数据在能有更高的查询效率

     /// 索引
     static var indexBindings: [IndexBinding.Subfix: IndexBinding]? {
         return [
             "_uniqueIndex": IndexBinding(isUnique: true, indexesBy: id),
             "_descendingIndex": IndexBinding(indexesBy: createdAt.asIndex(orderBy: .descending)),
         ]
     }
    

    1: 对于具有唯一性的索引,可以通过isUnique:参数指定,如 IndexBinding(isUnique: true, indexesBy: id)
    2: 对于需要特别指明索引存储顺序的字段,可以通过 asIndex(orderBy:)函数指定,如 createdAt.asIndex(orderBy: .descending)

    4\5表约束和虚拟表映射:
    都是 TableEncodable 的一个可选函数,可根据需求选择实现或不实现,因为暂时没用到就暂不先介绍了,有兴趣可自行查看

    相关使用(增、删、改、查、建库、建表、切换账号表切换等):

    struct WCDBDataBasePath {
        let dbPath = YDFileManage.shared.getDocumentsStringPath() + "/WCDB/" + "Message.db"
    }
    
    class WCDBManage: NSObject {
        static let shared = WCDBManage()
    
        let dataBasePath = URL(fileURLWithPath: WCDBDataBasePath().dbPath)
        var dataBase: Database?
        private override init() {
            super.init()
            dataBase = createDb()
        }
    
        /// 创建db
        private func createDb() -> Database {
            debugPrint("数据库路径==\(dataBasePath.absoluteString)")
            return Database(withFileURL: dataBasePath)
        }
    
        /// 创建表
        func createTable<T: TableDecodable>(table: String, of type: T.Type) -> Void {
            do {
                try dataBase?.create(table: table, of: type)
            } catch let error { }
        }
    
        /// 插入
        func insertToDb<T: TableEncodable>(objects: [T], table: String) -> Void {
            do {
                try dataBase?.run(transaction: {
                    try dataBase?.insert(objects: objects, intoTable: table)
                })
            } catch let error {}
        }
    
        /// 修改
        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 let error {}
        }
    
        /// 插入或更新
        func insertOrUpdateToDb<T: TableEncodable>(table: String, on propertys: [PropertyConvertible]? = nil, with object: [T], where condition: Condition? = nil) -> Void {
            do {
                try dataBase?.insertOrReplace(objects: object, on: propertys, intoTable: table)
            } catch let error {}
        }
    
        /// 删除
        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 let error {}
        }
    
        /// 查询
        func qureyFromDb<T: TableDecodable>(fromTable: String, cls cName: T.Type, 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 let error {}
            return nil
        }
    
        /// 查询单条数据
        func qureyObjectFromDb<T: TableDecodable>(fromTable: String, cls cName: T.Type, where condition: Condition? = nil, orderBy orderList: [OrderBy]? = nil) -> T? {
            do {
                let object: T? = try (dataBase?.getObject(fromTable: fromTable, where: condition, orderBy: orderList))
                debugPrint("\(object)")
                return object
            } catch let error {}
            return nil
        }
    

    值得注意的问题

    1: 通过动态获取当前表名的方式实现切换账号数据表查找正确

    let results = WCDBManage.shared.qureyFromDb(fromTable: WCDBManage.shared.K_Conversation_Table(), cls: WCDBConversation.self, where: WCDBConversation.Properties.firstLevel == true, orderBy: [WCDBConversation.Properties.rank.asOrder(by: .ascending), WCDBConversation.Properties.createdAt.asOrder(by: .descending)], limit: nil, offset: nil)

    extension WCDBManage {
        /// 获取当前用户 member 表
        func K_Member_Table() -> String {
            return "WCDBMember" + (Member.getCurrent()?.id ?? "wcdb")
       }
    
        /// 获取当前用户 conversation 表
        func K_Conversation_Table() -> String {
            return "WCDBConversation" + (Member.getCurrent()?.id ?? "wcdb")
        }
    
        /// 获取当前用户 msg 表
        func K_Msg_Table() -> String {
            return "WCDBMsg" + (Member.getCurrent()?.id ?? "wcdb")
        }
    }
    

    2: 需要实现自定义字段映射需要实现ColumnCodable 协议、

    class WCDBMsg: TableCodable, ApiModel {
        var id: Int? = nil
        var conversation: WCDBConversation?
    
        enum CodingKeys: String, CodingKey, CodingTableKey {
            typealias Root = WCDBMsg
            static let objectRelationalMapping = TableBinding(CodingKeys.self)
            case id
            case conversation /// 这里不加入conversation字段,就不会将嵌套的模型加进数据库存储
            /// 约束(主键约束、非空约束、唯一约束、默认值ColumnConstraintBinding)
            static var columnConstraintBindings: [CodingKeys: ColumnConstraintBinding]? {
                return [
                    id: ColumnConstraintBinding(isUnique: true),
                ]
            }
        }
    }
    

    class WCDBConversation: TableCodable, ApiModel, ColumnCodable {
        var id: Int? = nil
        var memberId: String? = nil
    
        enum CodingKeys: String, CodingTableKey {
            typealias Root = WCDBConversation
            static let objectRelationalMapping = TableBinding(CodingKeys.self)
            case id
            case memberId = "member_id"
            static var columnConstraintBindings: [CodingKeys: ColumnConstraintBinding]? {
                return [
                     id: ColumnConstraintBinding(isUnique: true),
                ]
            }
            static var virtualTableBinding: VirtualTableBinding? {
                return VirtualTableBinding(with: .fts3, and: ModuleArgument(with: .WCDB))
            }
        }
    
        /// 此嵌套model以什么方式存储在上层model数据表中 
        static var columnType: ColumnType {
            return .text
        }
    
        /// 想在外部使用 WCDBConversation()初始化,需要自定义如下初始化方法
        init(id: Int? = nil, memberId: String? = nil ) {
            self.id = id
            self.memberId = memberId
        }
    
        required init?(with value: FundamentalValue) {
            let data = value.dataValue
            guard data.count > 0 else {
                return nil
            }
            guard let dictionary = try? JSONDecoder().decode(WCDBConversation.self, from: data) else {
                return nil
            }
            id = dictionary.id
            memberId = dictionary.memberId
        }
    
        func archivedValue() -> FundamentalValue {
            guard let data = try? JSONEncoder().encode(self) else {
                return FundamentalValue(nil)
            }
            return FundamentalValue(data)
        }
    }
    

    3: 数据库迁移:

    原始模型:

    class Sample: TableCodable {
        var identifier: Int? = nil
        var description: String? = nil
        var createDate: Date? = nil
    
        enum CodingKeys: String, CodingTableKey {
            typealias Root = Sample
            static let objectRelationalMapping = TableBinding(CodingKeys.self)
            case identifier
            case description
            case createDate
        }
    }
    try database.create(table: "sampleTable", of: Sample.self)
    


    class Sample: TableCodable {
        var identifier: Int? = nil
        var content: String? = nil
        var title: String? = nil
    
        enum CodingKeys: String, CodingTableKey {
            typealias Root = Sample
            static let objectRelationalMapping = TableBinding(CodingKeys.self)
            case identifier
            case content = "description"
            case title
        }
        static var indexBindings: [IndexBinding.Subfix: IndexBinding]? {
            return [
                "_index": IndexBinding(indexesBy: title)
            ]
        }
    }
    try database.create(table: "sampleTable", of: Sample.self)
    


    可以看到,通过修改模型绑定,并再次调用 create(table:of:)

    1: description 字段通过别名的特性,被重命名为了 content
    2: 已删除的 createDate 字段会被忽略
    3: 对于新增的 title 会被添加到表中

    4: 模糊查询:

    WCDBManage.shared.qureyFromDb(fromTable: k_conversation_table, cls: WCDBConversation.self, where: WCDBConversation.Properties.msgPreview.like("%\(key!)%") || WCDBConversation.Properties.nickName.like("%\(key!)%"), orderBy: nil, limit: nil, offset: nil)
    .
    .
    .
    

    5:其他问题。。。。。。。

    相关文章

      网友评论

        本文标题:FMDB、Realm、WCDB

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