Foreword:
Commonly used databases in iOS include CoreData
, SQLite
and , FMDB
etc., among which CoreData
is deeply integrated with Xcode, and the ease of use is relatively poor; SQLite
it is a C language itself, and you need to understand the C language interface to use it; FMDB
it is SQLite
a layer of encapsulation of , and many glue codes still need to be written by yourself SQL
statement, but WCDB
is an easy-to-use, efficient, and complete mobile database framework developed by the WeChat team. It is based on SQLite
and SQLCipher
developed to support encryption, damage detection, data backup, and data repair. It is widely used in WeChat and supports in C++
, Swift
, and Objc
three used in a language environment.
WCDB basic calls
The most basic calling process is roughly divided into three steps:
- model binding
- Create database and tables
- operating data
1. Model binding
Suppose there exists a Person
class:
class Person {
var identifier: Int = 0
var name: String? = nil
var age: Int = 0
}
复制代码
To store data of this type into the database, you can use WCDB
the file template of :
1. Files and code templates
If you have not obtained WCDB
the Github
warehouse, you can execute the command in the terminal:
curl https://raw.githubusercontent.com/Tencent/wcdb/master
/tools/templates/install.sh -s | sh
复制代码
Open cmd + n
and pull to the bottom:
Choose swift to create, since the template is old code, you need to
import WCDB
change it toimport WCDBSwift
2. The model binding of WCDB Swift is divided into five parts:
- field mapping
- field constraints
- index
- table constraints
- virtual table mapping
1) Field mapping
CodingKeys
An enumeration class defined within a class followsString
andCodingTableKey
;- Write the variables you want to store behind
CodingKeys
in the enumerationcase
, indicating that they are bound to the fields in the database table; - 把需要在数据库重命名的字段,进行别名映射,
case identifier = "id"
表示把identifier
在数据库表中重命名为id
; - 如果使用的字段与
SQLite
字段关键字相同,也需要做别名映射。
2)字段约束
字段约束,它用于针对单个字段的约束,例如主键约束、非空约束、唯一约束,默认值等等,可以根据自己的需求选择实现或者不实现。方法是 columnConstraintBindings:
,Github
上和 TableCodable
模板默认生成的是以前的代码,新代码如下:
static var columnConstraintBindings: [CodingKeys:
ColumnConstraintBinding]? {
return [
identifier: ColumnConstraintBinding(isPrimary: true),
name: ColumnConstraintBinding(isNotNull: true, defaultTo:"空"),
age: ColumnConstraintBinding(isNotNull: true, defaultTo: 0)
]
}
复制代码
3)自增属性
isAutoIncrement
表示是否自增,约束定义 isPrimary
为 true
,支持自增,但是仍然可以支持非自增方式插入。
当需要自增插入,需要设置 isAutoIncrement
为 true
,数据库会将主键最大值 + 1
作为新的最大主键值。
索引,表约束,虚拟表映射相对复杂,一般表用不到,这里就不写。
4)swift6 错误警告
由于 Github上WCDB
的是 swift4.0
的代码,现在使用有些需要修改,会提示:
在 class Person
前面添加 final
消除警告。
最终模型绑定代码:
final class Person: TableCodable {
var identifier: Int = 0
var name: String? = nil
var age: Int = 0
enum CodingKeys: String, CodingTableKey {
typealias Root = Person
case identifier = "id"
case name
case age
static let objectRelationalMapping =
TableBinding(CodingKeys.self)
static var columnConstraintBindings:
[CodingKeys: ColumnConstraintBinding]? {
return [
identifier: ColumnConstraintBinding
(isPrimary: true),
name: ColumnConstraintBinding
(isNotNull: true, defaultTo: "空"),
age: ColumnConstraintBinding
(isNotNull: true, defaultTo: 0)
]
}
}
var isAutoIncrement: Bool = true
}
复制代码
二、创建数据库与表
1. 创建数据库
var database = Database(withPath: NSSearchPathForDirectoriesInDomains(
.documentDirectory,
.userDomainMask,
true).last!+”/Person/person.db")
复制代码
2. 创建表
一行代码就创建表:
try database?.create(table: "personTable", of: Person.self)
复制代码
由于 WCDB
推荐用表操作数据,所以可以获取表对象:
var personTable = try database.getTable(named: "personTable")
复制代码
三、操作数据
1. 插入操作
向表中插入一条数据,id
前面已经定义了自增:
try database?.insert(objects: p, intoTable: "personTable")
复制代码
WCDB
推荐操作表,因为操作的对象更明确,更简洁,后面代码都是表操作:
try personTable?.insert(objects: p)
复制代码
2. 删除操作
示例代码,删除 id
为 2 的数据:
try personTable?.delete(where: Person.Properties.identifier == 2)
复制代码
3. 更新操作
示例代码,更新 id
为 2 的数据的 name
字段:
try personTable?.update(on: Person.Properties.name, with: p,
where: Person.Properties.identifier == 2 )
复制代码
4. 查找操作
示例代码,查找年龄大于 25 的数据:
let persons: [Person]! = try personTable?.getObjects(where:
Person.Properties.age > 25)
复制代码
主要功能代码都是一行代码搞定,并且让增删改查的语法一致,使用非常方便。
四、数据库、表
1. 打开数据库
由于 WCDB
是采用延迟初始化,使用时候才会创建并且初始化,所以不需要手动调用 open
,但可以使用 database.canOpen
测试数据是否可以正常打开,另外 database.isOpened
需要创建表之后才会 true
。
2. 关闭数据库
WCDB
一般情况下不需要开发者手动调用关闭,如果控制器被回收,数据库会自动关闭,并且自动回收内存。当然,也可以手动调用,一般都是基于文件操作,比如移动文件影响到了数据库的数据,才需要手动关闭,接口是:
try database.close(onClosed: {
try database.moveFiles(toDirectory: otherDirectory)
})
复制代码
3. 表
通过 getTable
接口获取数据库中的一个表:
let table = database.getTable(named: "sampleTable", of: Sample.self)
复制代码
WCDB
中 Table
具备了 database
的所有增删改查接口,并且更简洁,以表为单位来管理数据读写逻辑更合理方便,所以尽量使用 Table
来进行数据读写操作,上面代码已经演示。
五、事务
事务一般用来提升性能和保证操作的原子性,通过 transaction
控制事务。
1. 性能
假设给数据库插入 100 万条数据,看耗费时间和使用事务的优化情况,先准备 100 万条数据:
print("startTime ------------------" + startTime)
var persons:[Person] = [];
for i in 1...1000000{
let p = Person()
p.age = Int(arc4random_uniform(20)) + 10;
p.name = String(format: "张%d", i)
persons.append(p)
}
复制代码
1)普通插入操作
// 单独插入,效率很差
for p in persons {
do{
try personTable!.insert(objects: p)
}catch let error{
debugPrint("插入数据失败 \(error.localizedDescription)")
}
}
let endTime = Self.getCurrentTime(timeFormat: TimeFormat.HHMMSS)
print("endTime ------------------" + endTime)
复制代码
数据库表中如下:
运行打印:
startTime ------------------ 12:05:43
endTime ------------------ 12:06:23
复制代码
普通操作,插入 100 万条数据,耗时差不多 41 秒。
2) Transaction insert operation
// 事务插入,性能较好
do{
try database.run(transaction: {
for object in persons {
try personTable?.insert(objects: object)
}
})}catch let error{
debugPrint("插入数据失败 \(error.localizedDescription)")
}
let endTime = Self.getCurrentTime(timeFormat: TimeFormat.HHMMSS)
print("endTime ------------------" + endTime)
复制代码
Running prints:
startTime ------------------ 12:14:42
endTime ------------------ 12:14:55
复制代码
Transaction operation, inserting 1 million pieces of data takes about 13 seconds, and the performance has been significantly improved.
3) Built-in transaction insert operation
insert(objects:)
The interface has built-in transactions, and targeted optimization of batch data, with better performance
do{
try personTable?.insert(objects: persons)
}catch let error{
debugPrint("插入数据失败 \(error.localizedDescription)")
}
let endTime = Self.getCurrentTime(timeFormat: TimeFormat.HHMMSS)
print("endTime ------------------" + endTime)
复制代码
Running prints:
startTime ------------------ 12:23:05
endTime ------------------ 12:23:08
复制代码
It can be seen that the optimization of the built-in transaction interface is very obvious, and it only takes 3 seconds to insert 1 million pieces of data .
2. Atomicity
Under multi-threading, deleting data and inserting a piece of data at the same time, the operation is instantaneous, and it is difficult to determine which one will be executed first:
1) Non-transactional operations
DispatchQueue(label: "other thread").async {
do{
try self.personTable?.delete()
}catch let error{
debugPrint("事务操作失败 \(error.localizedDescription)")
}
}
do{
let p = Person()
p.age = Int(arc4random_uniform(20)) + 10;
p.name = "马可bro"
try personTable?.insert(objects: p)
let objects = try personTable?.getObjects()
print(objects?.count ?? "出错") // 可能输出 0 或 1
}catch let error{
debugPrint("事务操作失败 \(error.localizedDescription)")
}
复制代码
Result: Output 0 or 1 for a single call , 0 or 1 or 2 for multiple calls .
2) Transaction operation
DispatchQueue(label: "other thread").async {
do{
try self.personTable?.delete()
}catch let error{
debugPrint("事务操作失败 \(error.localizedDescription)"
}
}
do {
try database.run(transaction: {
let p = Person()
p.age = Int(arc4random_uniform(20)) + 10;
p.name = "大小姐"
try personTable?.insert(objects: p)
let objects = try personTable?.getObjects()
print(objects?.count ?? "出错") // 输出 1
})
}catch let error{
debugPrint("事务操作失败 \(error.localizedDescription)")
}
复制代码
Result: only 1 will be output .