FMDB、Realm、WCDB

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
使用 ://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:其他问题。。。。。。。

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

推荐阅读更多精彩内容

  • @[TOC](IOS DB技术框架对比) 1. 数据库简介 目前移动端数据库方案按其实现可分为两类: 关系型数据库...
    孔雨露阅读 576评论 0 5
  • Realm是由Y Combinator公司孵化出来的一款可以用于iOS(同样适用于Swift&Objective-...
    小歪子go阅读 2,208评论 6 9
  • 跨平台:现在很多应用都是要兼顾iOS和Android两个平台同时开发。如果两个平台都能使用相同的数据库,那就不用考...
    CoderZS阅读 2,470评论 2 16
  • 每日打卡20190609姓名:潘光辉 公司:安徽蒙城爱梓教育【日精进打卡第53天】 【知~学习】《六项精进大纲》 ...
    faquirx阅读 188评论 0 1
  • 越是困难的时候,越要挺住、激流勇进,否则如逆水行舟,不进则退。 说白了,就是当我们遇到难整的问题时,脑海里不要一直...
    顺顺顺阅读 255评论 0 1