@[TOC](IOS DB技术框架对比)
1. 数据库简介
- 目前移动端数据库方案按其实现可分为两类:
- 关系型数据库,代表有CoreData、FMDB等。
- 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](https://github.com/Tencent/wcdb),均指代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使用可以参考这两篇博客:
2. 数据库 Realm、WCDB, SQLite性能对比
2.1 测试数据表结构
Student表。
字段:ID、name、age、money。
ID | name | age | money |
---|---|---|---|
主键 | 姓名 | 年龄 | 存款(建索引) |
其中age为0100随机数字,money为每一万条数据中,010000各个数字只出现一次。
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的。
对比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 优缺点
优点
- SQLite是轻量级的,没有客户端和服务器端之分,并且是跨平台的关系型数据库。
- SQLite是一个单文件的,可以copy出来在其他地方用。
- 有一个SQLite.swift框架非常好用。
缺点
- SQLite在并发的读写方面性能不是很好,数据库有时候可能会被某个读写操作独占,可能会导致其他的读写操作被阻塞或者出错。
- 不支持SQL92标准,有时候语法不严格也可以通过,会养成不好习惯,导致不会维护。
- 需要写很多SQL拼接语句,写很多胶水代码,容易通过SQL注入恶意代码。
- 效率很低:SQL基于字符串,命令行爱好者甚喜之。但对于基于现代IDE的移动开发者,却是一大痛。字符串得不到任何编译器的检查,业务开发往往心中一团热火,奋笔疾书下几百行代码,满心欢喜点下Run后才发现:出错了!静心下来逐步看log、断点后才发现,噢,SELECT敲成SLEECT了。改正,再等待编译完成,此时已过去十几分钟。
4.2 FMDB 优缺点
优点
- 它基于SQLite封装,对于有SQLite和ObjC基础的开发者来说,
- 简单易懂,可以直接上手;
缺点
- FMDB只是将SQLite的C接口封装成了ObjC接口,没有做太多别的优化,即所谓的胶水代码(Glue Code)。
- 使用过程需要用大量的代码拼接SQL、拼装Object,并不方便。
- 容易通过SQL代码注入。
- 直接暴露字符串接口,让业务开发自己拼接字符串,取出数据后赋值给对应的Object. 这种方式过于简单粗暴。
官方文档
4.3 CoreData 优缺点
优点
它是苹果内建框架,和Xcode深度结合,可以很方便进行ORM;
缺点
- 其上手学习成本较高,不容易掌握。
- 稳定性也堪忧,很容易crash;多线程的支持也比较鸡肋。
4.4 Realm优缺点
优点
- Realm在使用上和Core Data有点像,直接建立我们平常的对象Model类就是建立一个表了,确定主键、建立索引也在Model类里操作,几行代码就可以搞定,在操作上也可以很方便地增删改查,不同于SQLite的SQL语句(即使用FMDB封装的操作依然有点麻烦),Realm在日常使用上非常简单,起码在这次测试的例子中两个数据库同样的一些操作,Realm的代码只有SQLite的一半。
- 其实Realm的“表”之间也可以建立关系,对一、对多关系都可以通过创建属性来解决。
- 在.m方法中给“表”确定主键、属性默认值、加索引的字段等。
- 修改数据时,可以直接丢进去一条数据,Realm会根据主键判断是否有这个数据,有则更新,没有则添加。
- 查询操作太简单了,一行代码根据查询目的来获取查询结果的数组。
- 支持KVC和KVO。
- 支出数据库加密。
- 支持通知。
- 方便进行数据库变更(版本迭代时可能发生表的新增、删除、结构变化),Realm会自行监测新增加和需要移除的属性,然后更新硬盘上的数据库架构,Realm可以配置数据库版本,进行判断。
- 一般来说Realm比SQLite在硬盘上占用的空间更少。
缺点
- Realm也有一些限制,需要考虑是否会影响。
- 类名长度最大57个UTF8字符。
- 属性名长度最大63个UTF8字符。
- NSData及NSString属性不能保存超过16M数据,如果有大的可以分块。
- 对字符串进行排序以及不区分大小写查询只支持“基础拉丁字符集”、“拉丁字符补充集”、“拉丁文扩展字符集 A” 以及”拉丁文扩展字符集 B“(UTF-8 的范围在 0~591 之间)。
- 多线程访问时需要新建新的Realm对象。
- Realm没有自增属性。。也就是说对于我们习惯的自增主键,如果确实需要,我们要自己去赋值,如果只要求独一无二, 那么可以设为[[NSUUID UUID] UUIDString],如果还要求用来判断插入的顺序,那么可以用Date。
- Realm支持以下的属性类型:BOOL、bool、int、NSInteger、long、long long、float、double、NSString、NSDate、NSData以及 被特殊类型标记的NSNumber,注意,不支持集合类型,只有一个集合RLMArray,如果服务器传来的有数组,那么需要我们自己取数据进行转换存储。
官方文档
4.5 WCDB优缺点
优点
- 易用性
- one line of code 是它坚持的原则,大多数操作只需要一行代码即可完成.
- 使用WINQ 语句查询,不用为拼接SQL语句而烦恼了,模型绑定映射也是按照规定模板去实现方便快捷。
-
高效性:上面已经做过性能对比,WCDB对比其他框架效率和性能高很多。
-
完整性
- 支持基于SQLCipher 加密
- 持全文搜索
- 支持反注入,可以避免第三方从输入框注入 SQL,进行预期之外的恶意操作。
- 用户不用手动管理数据库字段版本,升级方便自动.
- 提供数据库修复工具。
缺点
- 最明显的缺点是其相关资料太少了
贴一份评论
贴一份评论
官方文档
5. 总结
- 个人比较推荐使用微信的WCDB框架,这个框架是开源的,如果有需要一定要自己拼接SQL语句,要实现SQL语句的扩展也是很容易的事情。
- 在选型上,每个框架都有自己的优缺点,并没有却对的优劣性,只有适不适合项目需求。其实对于小型项目直接使用Sqlite或者用FMDB 都可以满足要求,但是如果遇到安全性问题,需要自己重复造很多轮子实现加密等功能。在使用上面如果直接使用SQL 语句,像在Jimu1.0里面SQL语句到处散乱,出了问题不好定位,需要写很多重复的拼接SQL语句的胶水代码。而且SQL语句如果写错了编译并不会报错或警告,如果出现了因为SQL语句的bug,到项目后期很难定位。
- FMDB的SQL拼接、难以防止的SQL注入;CoreData虽然可以方便ORM,但学习成本高,稳定性堪忧,而且多线程鸡肋;另外基于C语言的sqlite我想用的人也应该不多;除了上述关系型数据库之外然后还有一些其他的Key-Value型数据库,如我用过的Realm,对于ObjC开发者来说,上手倒是没什么难度,但缺点显而易见,需要继承,入侵性强,对于单继承的OC来说这并不理想,而且对于集合类型不完全支持,复杂查询也比较无力。
- WCDB是微信团队于2017年6月9日开源的。开源时间不长。可能相关资料比较少,只能靠查看官方文档。
- SQLite3直接使用比较麻烦,而FMDB是OC编写的,如果使用Swift版本推荐使用:SQLite.swift这是很好的框架比使用FMDB简单,代码简介很多。SQLite.swift对SQLite进行了全面的封装,拥有全面的纯swift接口,即使你不会SQL语句,也可以使用数据库。作者采用了链式编程的写法,让数据库的管理变得优雅,可读性也很强。
- 综合上述,我给出的建议是:
- 最佳方案A是使用WCDB.swift框架。
- 方案B是使用SQLite.swift
- 方案C是使用 Realm 框架
- 方案D是使用 FMDB 框架
6. 简单对比WCDB.swift,Realm.swift,SQLite.swift的用法
- 简单对比WCDB.swift,Realm.swift,FMDB,SQLite.swift的用法
6.1 WCDB.swift基本用法
- 完整demo下载地址:WCDB.swift使用Demo
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语句,也可以使用数据库。作者采用了链式编程的写法,让数据库的管理变得优雅,可读性也很强。
- Swift的SQLite框架:SQLite.swift
- 集成:
- Carthage:
github "stephencelis/SQLite.swift"
- CocoaPods:
pod 'SQLite.swift'
- 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")
- 初始化方法:
- 这只是最简单的方式,我们来深入看一下Connection的初始化方法和可以设置的参数:
public init(_ location: SQLite.Connection.Location = default, readonly: Bool = default) throws- 第一个参数Location指的是数据库的位置,有三种情况:
inMemory数据库存在内存里;temporary临时数据库,使用完会被释放掉;filename (or path)存在硬盘中,我们上面用的就是这种。前两种使用完毕会被释放不会保存,第三种可以保存下来;第一种数据库存在内存中,后两种存在硬盘里。- 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的和
网友评论