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:突然想到的多表切换问题,貌似也不好处理,它是直接传入模型映射创建的表格
性能剖析(截自网络):
上述两张截图只是其中挑选出的两张,总体来说性能上WCDB&Realm是优于FMDB&Sqlite的,其中WCDB性能稍微领先Realm
二:基本(增、删、改、查、建库、建表、数据库迁移)等使用对比
FMDB
git :https://github.com/ccgus/fmdb
使用 ://www.greatytc.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:其他问题。。。。。。。