IOS数据存储(一) DB技术框架对比

8,622 阅读22分钟

IOS数据存储(一) DB技术框架对比

IOS数据存储(二)之Realm.swift (一) 使用篇

IOS数据存储(三)之Realm.swift (二) 使用详解

IOS数据存储(五)WCDB (二)WCDB.swift使用篇

1. 数据库简介

  • 目前移动端数据库方案按其实现可分为两类:
  1. 关系型数据库,代表有CoreData、FMDB等。
  2. key-value数据库,代表有Realm、LevelDB、RocksDB等。
  • CoreData

它是苹果内建框架,和Xcode深度结合,可以很方便进行ORM;但其上手学习成本较高,不容易掌握。稳定性也堪忧,很容易crash;多线程的支持也比较鸡肋。

  • FMDB

它基于SQLite封装,对于有SQLite和ObjC基础的开发者来说,简单易懂,可以直接上手;而缺点也正是在此,FMDB只是将SQLite的C接口封装成了ObjC接口,没有做太多别的优化,即所谓的胶水代码(Glue Code)。使用过程需要用大量的代码拼接SQL、拼装Object,并不方便。

因其在各平台封装、优化的优势,比较受移动开发者的欢迎。对于iOS开发者,key-value的实现直接易懂,可以像使用NSDictionary一样使用Realm。并且ORM彻底,省去了拼装Object的过程。但其对代码侵入性很强,Realm要求类继承RLMObject的基类。这对于单继承的ObjC,意味着不能再继承其他自定义的子类。同时,key-value数据库对较为复杂的查询场景也比较无力。

  • 可见,各个方案都有其独特的优势及劣势,没有最好的,只有最适合的。
  • 在选型上,FMDB的SQL拼接、难以防止的SQL注入;CoreData虽然可以方便ORM,但学习成本高,稳定性堪忧,而且多线程鸡肋;另外基于C语言的sqlite我想用的人也应该不多;除了上述关系型数据库之外然后还有一些其他的Key-Value型数据库,如我用过的Realm,对于ObjC开发者来说,上手倒是没什么难度,但缺点显而易见,需要继承,入侵性强,对于单继承的OC来说这并不理想,而且对于集合类型不完全支持,复杂查询也比较无力。
  • 下面介绍一下微信中使用的WCDB数据库,它满足了下面要求:
    • 高效;增删改查的高效是数据库最基本的要求。除此之外,我们还希望能够支持多个线程高并发地操作数据库,以应对微信频繁收发消息的场景。
    • 易用;这是微信开源的原则,也是WCDB的原则。SQLite本不是一个易用的组件:为了完成一个查询,往往我们需要写很多拼接字符串、组装Object的胶水代码。这些代码冗长繁杂,而且容易出错,我们希望组件能统一完成这些任务。
    • 完整;数据库操作是一个复杂的场景,我们希望数据库组件能完整覆盖各种场景。包括数据库损坏、监控统计、复杂的查询、反注入等。

1.1 WCDB-iOS/Mac

WCDB-iOS/Mac(以下简称WCDB](github.com/Tencent/wcd…),均指代WCDB的iOS/Mac版本),是一个基于SQLite封装的Objective-C++数据库组件,提供了如下功能:

  • 便捷的ORM和CRUD接口:通过WCDB,开发者可以便捷地定义数据库表和索引,并且无须写一坨胶水代码拼装对象。
  • WINQ(WCDB语言集成查询):通过WINQ,开发者无须拼接字符串,即可完成SQL的条件、排序、过滤等等语句。
  • 多线程高并发:基本的增删查改等接口都支持多线程访问,开发者无需操心线程安全问题。
    • 线程间读与读、读与写操作均支持并发执行。
    • 写与写操作串行执行,并且有基于SQLite源码优化的性能提升。可参考另一篇文章《微信iOS SQLite源码优化实践
  • 损坏修复:数据库损坏一直是个难题,WCDB内置了我们自研的修复工具WCDBRepair。同样可参考另一篇文章《微信 SQLite 数据库修复实践
  • 统计分析:WCDB提供接口直接获取SQL的执行耗时,可用于监控性能。
  • 反注入:WCDB框架层防止了SQL注入,以避免恶意信息危害用户数据。

WCDB覆盖了数据库使用的绝大部分场景,且经过微信海量用户的验证,并将持续不断地增加新的能力。

具体WCDB使用可以参考这两篇博客:

  1. WCDB OC使用
  2. WCDB.swift使用

2. 数据库 Realm、WCDB, SQLite性能对比

2.1 测试数据表结构

Student表。

字段:ID、name、age、money。

ID name age money
主键 姓名 年龄 存款(建索引)

其中age为0~100随机数字,money为每一万条数据中,0~10000各个数字只出现一次。

2.2 测试数据

对于以下测试数据,只是给出一次测试后的具体数值供参考,经过反复测试后的,基本都在这个时间量级上。

这里测试用的是纯SQLite,没有用FMDB。

2.2.1 SQLite3

  • 9万条数据基础上连续单条插入一万条数据耗时:1462ms。
  • 已经建立索引,需要注意的是,如果是检索有大量重复数据的字段,不适合建立索引,反而会导致检索速度变慢,因为扫描索引节点的速度比全表扫描要慢。比如当我对age这个经常重复的数据建立索引再对其检索后,反而比不建立索引查询要慢一倍多。
  • 已经设置WAL模式。
  • 简单查询一万次耗时:331ms
  • dispatch 100个block来查询一万次耗时:150ms

2.2.2 realm

  • 9万条数据基础上连续单条插入一万条数据耗时:32851ms。
  • 注意,Realm似乎必须通过事务来插入,所谓的单条插入即是每次都开关一次事务,耗时很多,如果在一次事务中插入一万条,耗时735ms。
  • 已经建立索引。
  • 简单查询一万次耗时:699ms。
  • dispatch 100个block来查询一万次耗时:205ms。

2.2.3 WCDB

  • 9万条数据基础上连续单条插入一万条数据耗时:750ms。
  • 此为不用事务操作的时间,如果用事务统一操作,耗时667ms。
  • 已经建立索引。
  • 简单查询一万次耗时:690ms。
  • dispatch 100个block来查询一万次耗时:199ms。

2.2.4 三者对比

测试内容 Realm WCDB SQLite 用例数量
单条插入一万条 32851ms 750ms 1462ms 90000+10000
循环查询一万次 699ms 690ms 331ms 100000
100个block查询一万次 205ms 199ms 186ms 100000
  • 由于Realm单次事务操作一万次耗时过长,图表中显示起来也就没有了意义,因此下面图中Realm的耗时是按照事务批量操作耗时来记录的,实际上WCDB的插入操作是优于Realm的。

Realm、WCDB与SQLite移动数据库性能对比测试
对比2

  • 从结果来看,Realm似乎必须用事务,单条插入的性能会差很多,但是用事务来批量操作就会好一些。按照参考资料[3]中的测试结果,Realm在插入速度上比SQLite慢,比用FMDB快,而查询是比SQLite快的。
  • 而WCDB的表现很让人惊喜,其插入速度非常快,以至于比SQLite都快了一个量级,要知道WCDB也是基于SQLite扩展的。WCDB的查询速度也还可以接受,这个结果其实跟其官方给出的结果差不多:读操作基本等于FMDB速度,写操作比FMDB快很多。

3. WCDB, FMDB性能对比

WCDB.swift和fmdb做对比

WCDB.swift和fmdb做对比

4. 数据库框架优缺点对比

4.1 SQLite 优缺点

优点
  1. SQLite是轻量级的,没有客户端和服务器端之分,并且是跨平台的关系型数据库。
  2. SQLite是一个单文件的,可以copy出来在其他地方用。
  3. 有一个SQLite.swift框架非常好用。
缺点
  1. SQLite在并发的读写方面性能不是很好,数据库有时候可能会被某个读写操作独占,可能会导致其他的读写操作被阻塞或者出错。
  2. 不支持SQL92标准,有时候语法不严格也可以通过,会养成不好习惯,导致不会维护。
  3. 需要写很多SQL拼接语句,写很多胶水代码,容易通过SQL注入恶意代码。
  4. 效率很低:SQL基于字符串,命令行爱好者甚喜之。但对于基于现代IDE的移动开发者,却是一大痛。字符串得不到任何编译器的检查,业务开发往往心中一团热火,奋笔疾书下几百行代码,满心欢喜点下Run后才发现:出错了!静心下来逐步看log、断点后才发现,噢,SELECT敲成SLEECT了。改正,再等待编译完成,此时已过去十几分钟。

4.2 FMDB 优缺点

优点
  1. 它基于SQLite封装,对于有SQLite和ObjC基础的开发者来说,
  2. 简单易懂,可以直接上手;
缺点
  1. FMDB只是将SQLite的C接口封装成了ObjC接口,没有做太多别的优化,即所谓的胶水代码(Glue Code)。
  2. 使用过程需要用大量的代码拼接SQL、拼装Object,并不方便。
  3. 容易通过SQL代码注入。
  4. 直接暴露字符串接口,让业务开发自己拼接字符串,取出数据后赋值给对应的Object. 这种方式过于简单粗暴。
官方文档

4.3 CoreData 优缺点

优点

它是苹果内建框架,和Xcode深度结合,可以很方便进行ORM;

缺点
  1. 其上手学习成本较高,不容易掌握。
  2. 稳定性也堪忧,很容易crash;多线程的支持也比较鸡肋。

4.4 Realm优缺点

优点
  1. Realm在使用上和Core Data有点像,直接建立我们平常的对象Model类就是建立一个表了,确定主键、建立索引也在Model类里操作,几行代码就可以搞定,在操作上也可以很方便地增删改查,不同于SQLite的SQL语句(即使用FMDB封装的操作依然有点麻烦),Realm在日常使用上非常简单,起码在这次测试的例子中两个数据库同样的一些操作,Realm的代码只有SQLite的一半。
  2. 其实Realm的“表”之间也可以建立关系,对一、对多关系都可以通过创建属性来解决。
  3. 在.m方法中给“表”确定主键、属性默认值、加索引的字段等。
  4. 修改数据时,可以直接丢进去一条数据,Realm会根据主键判断是否有这个数据,有则更新,没有则添加。
  5. 查询操作太简单了,一行代码根据查询目的来获取查询结果的数组。
  6. 支持KVC和KVO。
  7. 支出数据库加密。
  8. 支持通知。
  9. 方便进行数据库变更(版本迭代时可能发生表的新增、删除、结构变化),Realm会自行监测新增加和需要移除的属性,然后更新硬盘上的数据库架构,Realm可以配置数据库版本,进行判断。
  10. 一般来说Realm比SQLite在硬盘上占用的空间更少。
缺点
  1. Realm也有一些限制,需要考虑是否会影响。
  2. 类名长度最大57个UTF8字符。
  3. 属性名长度最大63个UTF8字符。
  4. NSData及NSString属性不能保存超过16M数据,如果有大的可以分块。
  5. 对字符串进行排序以及不区分大小写查询只支持“基础拉丁字符集”、“拉丁字符补充集”、“拉丁文扩展字符集 A” 以及”拉丁文扩展字符集 B“(UTF-8 的范围在 0~591 之间)。
  6. 多线程访问时需要新建新的Realm对象。
  7. Realm没有自增属性。。也就是说对于我们习惯的自增主键,如果确实需要,我们要自己去赋值,如果只要求独一无二, 那么可以设为[[NSUUID UUID] UUIDString],如果还要求用来判断插入的顺序,那么可以用Date。
  8. Realm支持以下的属性类型:BOOL、bool、int、NSInteger、long、long long、float、double、NSString、NSDate、NSData以及 被特殊类型标记的NSNumber,注意,不支持集合类型,只有一个集合RLMArray,如果服务器传来的有数组,那么需要我们自己取数据进行转换存储。
官方文档

4.5 WCDB优缺点

优点
  1. 实际体验后,WCDB的代码体验非常好,代码量基本等于Realm,都是SQLite的一半,
  2. 在风格上比Realm更接近于OC原本的风格,基本已经感受不到是在写数据库的SQL操作。并且其查询语句WINQ也写的很符合逻辑,基本都可以一看就懂,甚至不需要你了解SQL语句。
  3. 整个开发流程下来非常流畅,除了配置环境时出了问题并且没有资料参考只能自己猜着解决外,代码基本是一气呵成写完完美运行的。
  4. WCDB通过ORM和WINQ,体现了其易用性上的优势,使得数据库操作不再繁杂。同时,通过链式调用,开发者也能够方便地获取数据库操作的耗时等性能信息。
  • 易用性
  1. one line of code 是它坚持的原则,大多数操作只需要一行代码即可完成.
  2. 使用WINQ 语句查询,不用为拼接SQL语句而烦恼了,模型绑定映射也是按照规定模板去实现方便快捷。
  • 高效性:上面已经做过性能对比,WCDB对比其他框架效率和性能高很多。

  • 完整性

  1. 支持基于SQLCipher 加密
  2. 持全文搜索
  3. 支持反注入,可以避免第三方从输入框注入 SQL,进行预期之外的恶意操作。
  4. 用户不用手动管理数据库字段版本,升级方便自动.
  5. 提供数据库修复工具。
缺点
  1. 最明显的缺点是其相关资料太少了

贴一份评论

贴一份评论

官方文档

5. 总结

  1. 个人比较推荐使用微信的WCDB框架,这个框架是开源的,如果有需要一定要自己拼接SQL语句,要实现SQL语句的扩展也是很容易的事情。
  2. 在选型上,每个框架都有自己的优缺点,并没有却对的优劣性,只有适不适合项目需求。其实对于小型项目直接使用Sqlite或者用FMDB 都可以满足要求,但是如果遇到安全性问题,需要自己重复造很多轮子实现加密等功能。在使用上面如果直接使用SQL 语句,像在Jimu1.0里面SQL语句到处散乱,出了问题不好定位,需要写很多重复的拼接SQL语句的胶水代码。而且SQL语句如果写错了编译并不会报错或警告,如果出现了因为SQL语句的bug,到项目后期很难定位。
  3. FMDB的SQL拼接、难以防止的SQL注入;CoreData虽然可以方便ORM,但学习成本高,稳定性堪忧,而且多线程鸡肋;另外基于C语言的sqlite我想用的人也应该不多;除了上述关系型数据库之外然后还有一些其他的Key-Value型数据库,如我用过的Realm,对于ObjC开发者来说,上手倒是没什么难度,但缺点显而易见,需要继承,入侵性强,对于单继承的OC来说这并不理想,而且对于集合类型不完全支持,复杂查询也比较无力。
  4. WCDB是微信团队于2017年6月9日开源的。开源时间不长。可能相关资料比较少,只能靠查看官方文档。
  5. SQLite3直接使用比较麻烦,而FMDB是OC编写的,如果使用Swift版本推荐使用:SQLite.swift这是很好的框架比使用FMDB简单,代码简介很多。SQLite.swift对SQLite进行了全面的封装,拥有全面的纯swift接口,即使你不会SQL语句,也可以使用数据库。作者采用了链式编程的写法,让数据库的管理变得优雅,可读性也很强。
  • 综合上述,我给出的建议是:
  1. 最佳方案A是使用WCDB.swift框架。
  2. 方案B是使用SQLite.swift
  3. 方案C是使用 Realm 框架
  4. 方案D是使用 FMDB 框架

6. 简单对比WCDB.swift,Realm.swift,SQLite.swift的用法

6.1 WCDB.swift基本用法

6.1.0 新建一个模型

import Foundation
import WCDBSwift

class Sample: TableCodable {
    var identifier: Int? = nil
    var description: String? = nil
    
    enum CodingKeys: String, CodingTableKey {
        typealias Root = Sample
        static let objectRelationalMapping = TableBinding(CodingKeys.self)
        case identifier
        case description
        
        static var columnConstraintBindings: [CodingKeys: ColumnConstraintBinding]? {
            return [
                identifier: ColumnConstraintBinding(isPrimary: true),
            ]
        }
    }
}

6.1.1 创建数据库

private lazy var db : Database? = {
        //1.创建数据库
        let docPath = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true).first! + "/wcdb.db"
        let database = Database(withPath: docPath + "/wcdb.db")
        return database
    }()

6.1.2 创建数据库表

private func testCreateTable() {
        guard let db = db else {
            return
        }
        do {
            //创建数据库表
            try db.create(table: TB_Sample, of: Sample.self)
            
        } catch {
            print(error)
        }
    }

6.1.3 插入操作

private func testInsert() {
        guard let db = db else {
            return
        }
        do {
            //插入数据库
            let object = Sample()
            object.identifier = 1
            object.description = "insert"
            try db.insert(objects: object, intoTable: TB_Sample) // 插入成功
            
            try db.insert(objects: object, intoTable: TB_Sample) // 插入失败,因为主键 identifier = 1 已经存在
            
            object.description = "insertOrReplace"
            try db.insertOrReplace(objects: object, intoTable: TB_Sample) // 插入成功,且 description 的内容会被替换为 "insertOrReplace"
            
        } catch {
            print(error)
        }
    }

6.1.4 删除操作

private func testDelete() {
        guard let db = db else {
            return
        }
        do {
            //删除操作
            // 删除 sampleTable 中所有 identifier 大于 1 的行的数据
            try db.delete(fromTable: TB_Sample,
                          where: Sample.Properties.identifier > 1)
            
            // 删除 sampleTable 中的所有数据
            try db.delete(fromTable: TB_Sample)
            
        } catch {
            print(error)
        }
    }

6.1.5 更新操作

private func testUpdate() {
        guard let db = db else {
            return
        }
        do {
            //更新数据
            let object = Sample()
            object.description = "update"
            
            // 将 sampleTable 中前三行的 description 字段更新为 "update"
            try db.update(table: TB_Sample,
                          on: Sample.Properties.description,
                          with: object,
                          limit: 3)
            
        } catch {
            print(error)
        }
    }

6.1.6 查询操作

 private func testQuery() {
        guard let db = db else {
            return
        }
        do {
            //查询操作
            // 返回 sampleTable 中的所有数据
            let allObjects: [Sample] = try db.getObjects(fromTable: TB_Sample)
            
            print(allObjects)
            
            // 返回 sampleTable 中 identifier 小于 5 或 大于 10 的行的数据
            let objects: [Sample] = try db.getObjects(fromTable: TB_Sample,
                                                            where: Sample.Properties.identifier < 5 || Sample.Properties.identifier > 10)
            print(objects)
            
            // 返回 sampleTable 中 identifier 最大的行的数据
//            let object: Sample? = try db.getObject(fromTable: TB_Sample,
//                                                         orderBy: Sample.Properties.identifier.asOrder(by: .descending))
            
            // 获取所有内容
            let allRows = try db.getRows(fromTable: TB_Sample)
            print(allRows[row: 2, column: 0].int32Value) // 输出 3
            
            // 获取第二行
            let secondRow = try db.getRow(fromTable: TB_Sample, offset: 1)
            print(secondRow[0].int32Value) // 输出 2
            
            // 获取 description 列
            let descriptionColumn = try db.getColumn(on: Sample.Properties.description, fromTable: TB_Sample)
            print(descriptionColumn) // 输出 "sample1", "sample1", "sample1", "sample2", "sample2"
            
            // 获取不重复的 description 列的值
            let distinctDescriptionColumn = try db.getDistinctColumn(on: Sample.Properties.description, fromTable: TB_Sample)
            print(distinctDescriptionColumn) // 输出 "sample1", "sample2"
            
            // 获取第二行 description 列的值
            let value = try db.getValue(on: Sample.Properties.description, fromTable: TB_Sample, offset: 1)
            print(value.stringValue) // 输出 "sample1"
            
            // 获取 identifier 的最大值
            let maxIdentifier = try db.getValue(on: Sample.Properties.identifier.max(), fromTable: TB_Sample)
            print(maxIdentifier.stringValue)
            
            // 获取不重复的 description 的值
            let distinctDescription = try db.getDistinctValue(on: Sample.Properties.description, fromTable: TB_Sample)
            print(distinctDescription.stringValue) // 输出 "sample1"
            
        } catch {
            print(error)
        }
    }

6.2 Realm.swift基本用法

6.2.1 创建数据库

import RealmSwift

static let sharedInstance = try! Realm()
    
    static func initRealm() {
        
        var config = Realm.Configuration()
        //使用默认的目录,但是可以使用用户名来替换默认的文件名
        config.fileURL = config.fileURL!.deletingLastPathComponent().appendingPathComponent("Bilibili.realm")
        //获取我们的Realm文件的父级目录
        let folderPath = config.fileURL!.deletingLastPathComponent().path
        //解除这个目录的保护
        try! FileManager.default.setAttributes([FileAttributeKey.protectionKey: FileProtectionType.none], ofItemAtPath: folderPath)
        //创建Realm
        Realm.Configuration.defaultConfiguration = config
    }

6.2.2 创建数据库表

static func add<T: Object>(_ object: T) {
        try! sharedInstance.write {
            sharedInstance.add(object)
        }
    }

6.2.3 插入操作

/// 添加一条数据
    static func addCanUpdate<T: Object>(_ object: T) {
        try! sharedInstance.write {
            sharedInstance.add(object, update: true)
        }
    }
  • 添加一组数据
static func addListData<T: Object>(_ objects: [T]) {
        autoreleasepool {
            // 在这个线程中获取 Realm 和表实例
            let realm = try! Realm()
            // 批量写入操作
            realm.beginWrite()
            // add 方法支持 update ,item 的对象必须有主键
            for item in objects {
                realm.add(item, update: true)
            }
            // 提交写入事务以确保数据在其他线程可用
            try! realm.commitWrite()
        }
    }
  • 后台单独进程写入一组数据
static func addListDataAsync<T: Object>(_ objects: [T]) {
        
        let queue = DispatchQueue.global(qos: DispatchQoS.QoSClass.default)
        // Import many items in a background thread
        queue.async {
            // 为什么添加下面的关键字,参见 Realm 文件删除的的注释
            autoreleasepool {
                // 在这个线程中获取 Realm 和表实例
                let realm = try! Realm()
                // 批量写入操作
                realm.beginWrite()
                // add 方法支持 update ,item 的对象必须有主键
                for item in objects {
                    realm.add(item, update: true)
                }
                // 提交写入事务以确保数据在其他线程可用
                try! realm.commitWrite()
            }
        }
    }

6.2.4 删除操作

/// 删除某个数据
    static func delete<T: Object>(_ object: T) {
        try! sharedInstance.write {
            sharedInstance.delete(object)
        }
    }

6.2.5 更新操作

  • 更新操作同添加操作,
 /// 添加一条数据
    static func addCanUpdate<T: Object>(_ object: T) {
        try! sharedInstance.write {
            sharedInstance.add(object, update: true)
        }
    }
    static func addListData<T: Object>(_ objects: [T]) {
        autoreleasepool {
            // 在这个线程中获取 Realm 和表实例
            let realm = try! Realm()
            // 批量写入操作
            realm.beginWrite()
            // add 方法支持 update ,item 的对象必须有主键
            for item in objects {
                realm.add(item, update: true)
            }
            // 提交写入事务以确保数据在其他线程可用
            try! realm.commitWrite()
        }
    }

6.2.6 查询操作

/// 根据条件查询数据
    static func selectByNSPredicate<T: Object>(_: T.Type , predicate: NSPredicate) -> Results<T>{
        return sharedInstance.objects(T.self).filter(predicate)
    }
    /// 后台根据条件查询数据
    static func BGselectByNSPredicate<T: Object>(_: T.Type , predicate: NSPredicate) -> Results<T>{
        return try! Realm().objects(T.self).filter(predicate)
    }
    /// 查询所有数据
    static func selectByAll<T: Object>(_: T.Type) -> Results<T>{
        return sharedInstance.objects(T.self)
    }
    
    /// 查询排序后所有数据,关键词及是否升序
    static func selectScoretByAll<T: Object>(_: T.Type ,key: String, isAscending: Bool) -> Results<T>{
        return sharedInstance.objects(T.self).sorted(byKeyPath: key, ascending: isAscending)
    }

6.3 FMDB基本用法

  • FMDB的用法应该比较熟悉,这里不论述

6.4 SQLite.swift基本用法

  • 如果使用SQL语句的方式,推荐使用这个框架。

SQLite.swift对SQLite进行了全面的封装,拥有全面的纯swift接口,即使你不会SQL语句,也可以使用数据库。作者采用了链式编程的写法,让数据库的管理变得优雅,可读性也很强。

  1. Carthage:
github "stephencelis/SQLite.swift"
  1. CocoaPods:
pod 'SQLite.swift'
  1. Swift Package Manager:
dependencies: [
    .package(url: "https://github.com/stephencelis/SQLite.swift.git", from: "0.11.5")
]

6.4.1 创建数据库

  • 这里我们设置好数据库文件的路径和名称,作为参数初始化一个Connection对象就可以了,如果路径下文件不存在的话,会自动创建。
import SQLite

let path = NSSearchPathForDirectoriesInDomains(
                .documentDirectory, .userDomainMask, true
                ).first!
let db = try! Connection("\(path)/db.sqlite3")
  • 初始化方法:
  1. 这只是最简单的方式,我们来深入看一下Connection的初始化方法和可以设置的参数: public init(_ location: SQLite.Connection.Location = default, readonly: Bool = default) throws
  2. 第一个参数Location指的是数据库的位置,有三种情况: inMemory数据库存在内存里;temporary临时数据库,使用完会被释放掉;filename (or path)存在硬盘中,我们上面用的就是这种。前两种使用完毕会被释放不会保存,第三种可以保存下来;第一种数据库存在内存中,后两种存在硬盘里。
  3. readonly数据库是否为只读不可修改,默认为false。只读的情况一般是我们复制一个数据库文件到我们的项目,只读取数据使用,不做修改。
  • 线程安全设置:

使用数据库避免不了多线程操作,SQLite.swift中我们有两个选项可以设置

db.busyTimeout = 5.0

db.busyHandler({ tries in
    if tries >= 5 {
        return false
    }
    return true
})

6.4.2 创建数据库表

let users = Table("users")
let id = Expression<Int64>("id")
let name = Expression<String?>("name")
let email = Expression<String>("email")

try db.run(users.create { t in
    t.column(id, primaryKey: true)
    t.column(name)
    t.column(email, unique: true)
})

等价于执行SQL:

// CREATE TABLE "users" (
//     "id" INTEGER PRIMARY KEY NOT NULL,
//     "name" TEXT,
//     "email" TEXT NOT NULL UNIQUE
// )

此外还可以这样创建:

let users = Table("users")
let id = Expression<Int64>("id")
let name = Expression<String?>("name")
let email = Expression<String>("email")

try db.run(users.create(temporary: false, ifNotExists: true, withoutRowid: false, block: { (t) in
                    
    t.column(id, primaryKey: true)
    t.column(name)
    t.column(email, unique: true)
                    
   })
)
/*
temporary:是否是临时表
ifNotExists:是否不存在的情况才会创建,记得设置为true
withoutRowid: 是否自动创建自增的rowid
*/

6.4.3 插入操作

let insert = users.insert(name <- "Alice", email <- "alice@mac.com")
if let rowId = try? db.run(insert) {
            print("插入成功:\(rowId)")
        } else {
            print("插入失败")
        }
//等价于执行下面SQL
// INSERT INTO "users" ("name", "email") VALUES ('Alice', 'alice@mac.com')

插入成功会返回对应的rowid

6.4.4 删除操作

let alice = users.filter(id == rowid)
if let count = try? db.run(alice.delete()) {
    print("删除的条数为:\(count)")
} else {
    print("删除失败")
}
//等价于执行下面SQL
// DELETE FROM "users" WHERE ("id" = 1)

删除成功会返回删除的行数int值

6.4.5 更新操作

let alice = users.filter(id == rowid)

try db.run(alice.update(email <- email.replace("mac.com", with: "me.com")))
//等价于执行下面SQL
// UPDATE "users" SET "email" = replace("email", 'mac.com', 'me.com')
// WHERE ("id" = 1)

//可以直接这样
if let count = try? db.run(alice. update()) {
    print("修改的条数为:\(count)")
} else {
    print("修改失败")
}

6.4.6 查询操作

let query = users.filter(name == "Alice").select(email).order(id.desc).limit(l, offset: 1)
for user in try db.prepare(query) {
    print("email: \(user[email])")
    //email: alice@mac.com
}


for user in try db.prepare(users) {
    print("id: \(user[id]), name: \(user[name]), email: \(user[email])")
    // id: 1, name: Optional("Alice"), email: alice@mac.com
}
//等价于执行下面SQL
// SELECT * FROM "users"

let stmt = try db.prepare("INSERT INTO users (email) VALUES (?)")
for email in ["betty@icloud.com", "cathy@icloud.com"] {
    try stmt.run(email)
}

db.totalChanges    // 3
db.changes         // 1
db.lastInsertRowid // 3

for row in try db.prepare("SELECT id, email FROM users") {
    print("id: \(row[0]), email: \(row[1])")
    // id: Optional(2), email: Optional("betty@icloud.com")
    // id: Optional(3), email: Optional("cathy@icloud.com")
}

try db.scalar("SELECT count(*) FROM users") // 2

6.4.7 封装代码

import UIKit
import SQLite
import SwiftyJSON

let type_column = Expression<Int>("type")
let time_column = Expression<Int>("time")
let year_column = Expression<Int>("year")
let month_column = Expression<Int>("month")
let week_column = Expression<Int>("week")
let day_column = Expression<Int>("day")
let value_column = Expression<Double>("value")
let tag_column = Expression<String>("tag")
let detail_column = Expression<String>("detail")
let id_column = rowid

class SQLiteManager: NSObject {
    
    static let manager = SQLiteManager()
    private var db: Connection?
    private var table: Table?
    
    func getDB() -> Connection {
        
        if db == nil {
            
            let path = NSSearchPathForDirectoriesInDomains(
                .documentDirectory, .userDomainMask, true
                ).first!
            db = try! Connection("\(path)/db.sqlite3")
            db?.busyTimeout = 5.0
            
        }
        return db!
        
    }
    
    func getTable() -> Table {
        
        if table == nil {
            
            table = Table("records")
            
            try! getDB().run(
                table!.create(temporary: false, ifNotExists: true, withoutRowid: false, block: { (builder) in
                    
                    builder.column(type_column)
                    builder.column(time_column)
                    builder.column(year_column)
                    builder.column(month_column)
                    builder.column(week_column)
                    builder.column(day_column)
                    builder.column(value_column)
                    builder.column(tag_column)
                    builder.column(detail_column)
                    
                })
            )
            
        }
        return table!
        
    }
    
    //增
    func insert(item: JSON) {
        
        let insert = getTable().insert(type_column <- item["type"].intValue, time_column <- item["time"].intValue, value_column <- item["value"].doubleValue, tag_column <- item["tag"].stringValue , detail_column <- item["detail"].stringValue, year_column <- item["year"].intValue, month_column <- item["month"].intValue, week_column <- item["week"].intValue, day_column <- item["day"].intValue)
        if let rowId = try? getDB().run(insert) {
            print_debug("插入成功:\(rowId)")
        } else {
            print_debug("插入失败")
        }
        
    }
    
    //删单条
    func delete(id: Int64) {
        
        delete(filter: rowid == id)
        
    }
    
    //根据条件删除
    func delete(filter: Expression<Bool>? = nil) {
        
        var query = getTable()
        if let f = filter {
            query = query.filter(f)
        }
        if let count = try? getDB().run(query.delete()) {
            print_debug("删除的条数为:\(count)")
        } else {
            print_debug("删除失败")
        }
        
    }
    
    //改
    func update(id: Int64, item: JSON) {
        
        let update = getTable().filter(rowid == id)
        if let count = try? getDB().run(update.update(value_column <- item["value"].doubleValue, tag_column <- item["tag"].stringValue , detail_column <- item["detail"].stringValue)) {
            print_debug("修改的结果为:\(count == 1)")
        } else {
            print_debug("修改失败")
        }
        
    }
    
    //查
    func search(filter: Expression<Bool>? = nil, select: [Expressible] = [rowid, type_column, time_column, value_column, tag_column, detail_column], order: [Expressible] = [time_column.desc], limit: Int? = nil, offset: Int? = nil) -> [Row] {
        
        var query = getTable().select(select).order(order)
        if let f = filter {
            query = query.filter(f)
        }
        if let l = limit {
            if let o = offset{
                query = query.limit(l, offset: o)
            }else {
                query = query.limit(l)
            }
        }
        
        let result = try! getDB().prepare(query)
        return Array(result)
        
    }
    
}
  • 封装后使用更加方便
let inV = SQLiteManager.manager.search(filter: year_column == year && month_column == month && type_column == 1, 
select: [value_column.sum]).first?[value_column.sum] ?? 0.0
//计算year年month月type为1的所有value的和