iOS学习

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

2019-10-18  本文已影响0人  孔雨露

@[TOC](IOS数据存储 之WCDB (二)WCDB.swift使用篇)

1.WCDB.Swfit基础使用

1.1 WCDB.Swfit 简介

  1. 模型绑定
  2. 创建数据库与表
  3. 操作数据

1.1.1 模型绑定

class Sample {
    var identifier: Int? = nil
    var description: String? = nil
}
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
    }
}

1.1.2 创建数据库与表

let database = Database(withPath: "~/Intermediate/Directories/Will/Be/Created/sample.db")
// 以下代码等效于 SQL:CREATE TABLE IF NOT EXISTS sampleTable(identifier INTEGER, description TEXT)
try database.create(table: "sampleTable", of: Sample.self)

1.1.3 操作数据

1.1.3.1 插入操作

//Prepare data
let object = Sample()
object.identifier = 1
object.description = "sample_insert"
//Insert
try database.insert(objects: object, intoTable: "sampleTable")

1.1.3.2 查找操作

let objects: [Sample] = try database.getObjects(fromTable: "sampleTable")

1.1.3.3 更新操作

//Prepare data
let object = Sample()
object.description = "sample_update"
//Update
try database.update(table: "sampleTable",
                       on: Sample.Properties.description,
                     with: object,
                    where: Sample.Properties.identifier > 0)
                    //类似 Sample.Properties.identifier > 0 的语法是 WCDB 的特性,它能通过 Swift 语法来进行 SQL 操作

1.1.3.4 删除操作

try database.delete(fromTable: "sampleTable")

1.2. 模型绑定

1.2.1 Swift 模型绑定

  1. 字段映射
  2. 字段约束
  3. 索引
  4. 表约束
  5. 虚拟表映射

1.2.2 字段映射

class Sample: TableCodable {
    var identifier: Int? = nil
    var description: String? = nil
    var offset: Int = 0
    var debugDescription: String? = nil
        
    enum CodingKeys: String, CodingTableKey {
        typealias Root = Sample
        static let objectRelationalMapping = TableBinding(CodingKeys.self)
        case identifier = "id"
        case description
        case offset = "db_offset"
    }
}
  1. 在类内定义 CodingKeys 的枚举类,并遵循 String 和 CodingTableKey。
  2. 枚举列举每一个需要定义的字段。
  3. 对于变量名与表的字段名不一样的情况,可以使用别名进行映射,如 case identifier = "id"
  4. 对于不需要写入数据库的字段,则不需要在 CodingKeys 内定义,如 debugDescription
  5. 对于变量名与 SQLite 的保留关键字冲突的字段,同样可以使用别名进行映射,如 offset 是 SQLite 的关键字。

与 Swift 的 Codable 协议不同的是,即便是所有字段都需要绑定,这里也必须列举每一个需要绑定的字段。因为 CodingKeys 除了用于模型绑定,还将用于语言集成查询,我们会在后面章节中介绍。

// 以下代码等效于 SQL:CREATE TABLE IF NOT EXISTS sampleTable(id INTEGER, description TEXT, db_offset INTEGER)
try database.create(table: "sampleTable", of: Sample.self)

1.2.2.1 字段映射的类型

数据库中的类型 类型
32 位整型 Bool, Int, Int8, Int16, Int32, UInt, UInt8, UInt16, UInt32
64 位整型 Int64, UInt64, Date
浮点型 Float, Double
字符串类型 String, URL
二进制类型 Data, Array, Dictionary, Set

其中 Date 以时间戳的形式存储, Array、Dictionary、Set 以 JSON 的形式存储。

1.2.3 字段约束

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),
                description: ColumnConstraintBinding(isNotNull: true, defaultTo: "defaultDescription"),
            ]
        }
    }

    var isAutoIncrement: Bool = false // 用于定义是否使用自增的方式插入
    var lastInsertedRowID: Int64 = 0 // 用于获取自增插入后的主键值
}
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 // 该字段在数据库内使用什么默认值
)
// 以下代码等效于 SQL:CREATE TABLE IF NOT EXISTS sampleTable(identifier INTEGER PRIMARY KEY, description TEXT NOT NULL DEFAULT 'defaultDescription')
try database.create(table: "sampleTable", of: Sample.self)

1.2.3.1 自增属性

let autoIncrementObject = Sample()
autoIncrementObject.isAutoIncrement = true

// 插入自增数据
try database.insert(objects: autoIncrementObject, intoTable: "sampleTable")
print(autoIncrementObject.lastInsertedRowID) // 输出 1

// 再次插入自增数据
try database.insert(objects: autoIncrementObject, intoTable: "sampleTable")
print(autoIncrementObject.lastInsertedRowID) // 输出 2

// 插入非自增的指定数据
let specificObject = Sample()
specificObject.identifier = 10
try database.insert(objects: specificObject, intoTable: "sampleTable")

若类只会使用自增的方式插入,而不需要指定值的方式插入,可以在定义时直接设置 isAutoIncrementtrue。如:var isAutoIncrement: Bool { return true }

1.2.4 索引

class Sample: TableCodable {
    var identifier: Int? = nil
    var description: String? = nil
    var multiIndexPart1: Int = 0
    var multiIndexPart2: Int = 0
        
    enum CodingKeys: String, CodingTableKey {
        typealias Root = Sample
        static let objectRelationalMapping = TableBinding(CodingKeys.self)
        case identifier
        case description
        case multiIndexPart1
        case multiIndexPart2

        static var indexBindings: [IndexBinding.Subfix: IndexBinding]? {
            return [
                "_uniqueIndex": IndexBinding(isUnique: true, indexesBy: identifier),
                "_descendingIndex": IndexBinding(indexesBy: description.asIndex(orderBy: .descending)),
                "_multiIndex": IndexBinding(indexesBy: multiIndexPart1, multiIndexPart2.asIndex(orderBy: .ascending))
            ]
        }
    }
}
  1. 对于需要特别指明索引存储顺序的字段,可以通过 asIndex(orderBy:) 函数指定,如 description.asIndex(orderBy: .descending)。
  2. 对于具有唯一性的索引,可以通过 isUnique: 参数指定,如 IndexBinding(isUnique: true, indexesBy: identifier)
  3. 对于由多个字段组成的联合索引,可以通过 indexesBy: 进行指定,如 (indexesBy: multiIndexPart1, multiIndexPart2.asIndex(orderBy: .ascending))
// 以下代码等效于 SQL:
// CREATE TABLE IF NOT EXISTS sampleTable(identifier INTEGER, description TEXT, multiIndexPart1 INTEGER, multiIndexPart2 INTEGER)
// CREATE UNIQUE INDEX IF NOT EXISTS sampleTable_uniqueIndex on sampleTable(identifier)
// CREATE INDEX IF NOT EXISTS sampleTable_descendingIndex on sampleTable(description DESC)
// CREATE INDEX IF NOT EXISTS sampleTable_multiIndex on sampleTable(multiIndexPart1, multiIndexPart2 ASC)
try database.create(table: "sampleTable", of: Sample.self)

1.2.5 表约束

class Sample: TableCodable {
    var identifier: Int? = nil
    var multiPrimaryKeyPart1: Int = 0
    var multiPrimaryKeyPart2: Int = 0
    var multiUniquePart1: Int = 0
    var multiUniquePart2: Int = 0
        
    enum CodingKeys: String, CodingTableKey {
        typealias Root = Sample
        static let objectRelationalMapping = TableBinding(CodingKeys.self)
        case identifier
        case multiPrimaryKeyPart1
        case multiPrimaryKeyPart2
        case multiUniquePart1
        case multiUniquePart2

        static var tableConstraintBindings: [TableConstraintBinding.Name: TableConstraintBinding]? {
            let multiPrimaryBinding =
                MultiPrimaryBinding(indexesBy: multiPrimaryKeyPart1.asIndex(orderBy: .descending), multiPrimaryKeyPart2)
            let multiUniqueBinding =
                MultiUniqueBinding(indexesBy: multiUniquePart1, multiUniquePart2.asIndex(orderBy: .ascending))
            return [
                "MultiPrimaryConstraint": multiPrimaryBinding,
                "MultiUniqueConstraint": multiUniqueBinding
            ]
        }
    }
}
  1. MultiPrimaryBinding: 联合主键约束
  2. MultiUniqueBinding: 联合唯一约束
  3. CheckBinding: 检查约束
  4. ForeignKeyBinding: 外键约束
// 以下代码等效于 SQL:
//  CREATE TABLE IF NOT EXISTS sampleTable(
//      identifier INTEGER, 
//      multiPrimaryKeyPart1 INTEGER, 
//      multiPrimaryKeyPart2 INTEGER, 
//      multiUniquePart1 INTEGER, 
//      multiUniquePart1 INTEGER,
//      CONSTRAINT MultiPrimaryConstraint PRIMARY KEY(multiPrimaryKeyPart1 DESC, multiPrimaryKeyPart2),
//      CONSTRAINT MultiUniqueConstraint UNIQUE(multiUniquePart1, multiUniquePart2 ASC)
//  )
try database.create(table: "sampleTable", of: Sample.self)

1.2.6 虚拟表映射

1.2.7 数据库升级

  1. 表已存在但模型绑定中未定义的字段,会被忽略。这可以用于删除字段。
  2. 表不存在但模型绑定中有定义的字段,会被新增到表中。这可以用于新增字段。
  3. 对于需要重命名的字段,可以通过别名的方式重新映射。

忽略字段并不会删除字段。对于该字段旧内容,会持续存在在表中,因此文件不会因此变小。实际上,数据库作为持续增长的二进制文件,只有将其数据导出生成另一个新的数据库,才有可能回收这个字段占用的空间。对于新插入的数据,该字段内容为空,不会对性能产生可见的影响。

对于数据库已存在但模型绑定中未定义的索引,create(table:of:) 接口不会自动将其删除。如果需要删除,开发者需要调用 drop(index:) 接口。

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)

到了第二个版本,sampleTable 表进行了升级。

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)
  1. description 字段通过别名的特性,被重命名为了 content。
  2. 已删除的 createDate 字段会被忽略。
  3. 对于新增的 title 会被添加到表中。

1.2.8 文件与代码模版

未获取 WCDB 的 Github 仓库的开发者,可以在命令执行 curl https://raw.githubusercontent.com/Tencent/wcdb/master/tools/templates/install.sh -s | sh

已获取 WCDB 的 Github 仓库的开发者,可以手动执行 cd path-to-your-wcdb-dir/tools/templates; sh install.sh; 。

1.2.8.1 文件模版

文件模版安装完成后,在 Xcode 的菜单 File -> New -> File... 中创建新文件,选择 TableCodable。 在弹出的菜单中输入文件名,并选择 Language 为 Swift 即可。

模板

在代码文件中的任意位置,输入 TableCodableClass 后选择代码模版即可。

image

文件和代码模版都是以 class 作为例子的,实际上 struct 甚至 enum 都可以进行模型绑定的。

1.3. 增删改查

1.3.1 插入操作

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),
            ]
        }
    }
}

try database.create(table: "sampleTable", of: Sample.self)

let object = Sample()
sample.identifier = 1
sample.description = "insert"
try database.insert(objects: object, intoTable: "sampleTable") // 插入成功

try database.insert(objects: object, intoTable: "sampleTable") // 插入失败,因为主键 identifier = 1 已经存在

sample.description = "insertOrReplace"
try database.insertOrReplace(objects: object, intoTable: "sampleTable") // 插入成功
// insert 和 insertOrReplace 函数只有函数名不同,其他参数都一样。
func insert<Object: TableEncodable>(
    objects: [Object], // 需要插入的对象。WCDB Swift 同时实现了可变参数的版本,因此可以传入一个数组,也可以传入一个或多个对象。
    on propertyConvertibleList: [PropertyConvertible]? = nil, // 需要插入的字段
    intoTable table: String // 表名
) throws

这里需要特别注意的是 propertyConvertibleList 参数,它是 遵循 PropertyConveritble 协议的对象的数组。我们会在语言集成查询进一步介绍。这里只需了解,它可以传入模型绑定中定义的字段,如 Sample.Properties.identifier

当不传入 propertyConvertibleList 参数时,"insert" 或 "insertOrReplace" 接口会使用所有定义的字段进行插入。而 propertyConvertibleList 不为空时,"insert" 或 "insertOrReplace" 只会插入指定的字段,这就构成了部分插入。

let object = Sample()
sample.identifier = 1
sample.description = "insert"
try database.insert(objects: object, on: Sample.Properties.identifier, intoTable: "sampleTable") // 部分插入,没有指定 description。

这个例子中,指定了只插入 identifier 字段,因此其他没有指定的字段,会使用 模型绑定中定义的默认值 或 空 来代替。这里 description 没有定义默认值,因此其数据为空。

插入是最常用且比较容易操作卡顿的操作,因此 WCDB Swift 对其进行了特殊处理。 当插入的对象数大于 1 时,WCDB Swift 会自动开启事务,进行批量化地插入,以获得更新的性能。

1.3.2 删除操作

func delete(fromTable table: String, // 表名
            where condition: Condition? = nil, // 符合删除的条件
            orderBy orderList: [OrderBy]? = nil, // 排序的方式
            limit: Limit? = nil, // 删除的个数
            offset: Offset? = nil // 从第几个开始删除
) throws

删除接口会删除表内的数据,并通过 conditionorderListlimitoffset 参数来确定需要删除的数据的范围。

这四个组合起来可以理解为:将 table 表内,满足 condition 的数据,按照 orderList 的方式进行排序,然后从头开始第 offset 行数据后的 limit 行数据删除。

// 删除 sampleTable 中所有 identifier 大于 1 的行的数据
try database.delete(fromTable: "sampleTable", 
                    where: Sample.Properties.identifier > 1)

// 删除 sampleTable 中 identifier 降序排列后的前 2 行数据
try database.delete(fromTable: "sampleTable", 
                    orderBy: Sample.Properties.identifier.asOrder(by: .descending), 
                    limit: 2)

// 删除 sampleTable 中 description 非空的数据,按 identifier 降序排列后的前 3 行的后 2 行数据
try database.delete(fromTable: "sampleTable", 
                    where: Sample.Properties.description.isNotNull(), 
                    orderBy: Sample.Properties.identifier.asOrder(by: .descending), 
                    limit: 2,
                    offset: 3)

// 删除 sampleTable 中的所有数据
try database.delete(fromTable: "sampleTable")

1.3.3 更新操作

func update<Object: TableEncodable>(
    table: String,
    on propertyConvertibleList: [PropertyConvertible],
    with object: Object,
    where condition: Condition? = nil,
    orderBy orderList: [OrderBy]? = nil,
    limit: Limit? = nil,
    offset: Offset? = nil) throws

func update(
    table: String,
    on propertyConvertibleList: [PropertyConvertible],
    with row: [ColumnEncodableBase],
    where condition: Condition? = nil,
    orderBy orderList: [OrderBy]? = nil,
    limit: Limit? = nil,
    offset: Offset? = nil) throws
let object = Sample()
object.description = "update"

// 将 sampleTable 中所有 identifier 大于 1 且 description 字段不为空 的行的 description 字段更新为 "update"
try database.update(table: "sampleTable"
                    on: Sample.Properties.description,
                    with: object,
                    where: Sample.Properites.identifier > 1 && Sample.Properties.description.isNotNull())

// 将 sampleTable 中前三行的 description 字段更新为 "update"
try database.update(table: "sampleTable"
                    on: Sample.Properties.description,
                    with: object,
                    limit: 3)
let row: [ColumnCodableBase] = ["update"]

// 将 sampleTable 中所有 identifier 大于 1 且 description 字段不为空 的行的 description 字段更新为 "update"
try database.update(table: "sampleTable"
                    on: Sample.Properties.description,
                    with: row,
                    where: Sample.Properites.identifier > 1 && Sample.Properties.description.isNotNull())

// 将 sampleTable 中前三行的 description 字段更新为 "update"
try database.update(table: "sampleTable"
                    on: Sample.Properties.description,
                    with: row,
                    limit: 3)

1.3.4 查找操作

  1. getObjects
  2. getObject
  3. getRows
  4. getRow
  5. getColumn
  6. getDistinctColumn
  7. getValue
  8. getDistinctValue

虽然接口较多,但大部分都是为了简化操作而提供的便捷接口。实现上其实与 update 类似,只有 "object" 和 "row" 两种方式。

1.3.4.1 对象查找操作

func getObjects<Object: TableDecodable>(
    on propertyConvertibleList: [PropertyConvertible],
    fromTable table: String,
    where condition: Condition? = nil,
    orderBy orderList: [OrderBy]? = nil,
    limit: Limit? = nil,
    offset: Offset? = nil) throws -> [Object]

func getObject<Object: TableDecodable>(
    on propertyConvertibleList: [PropertyConvertible],
    fromTable table: String,
    where condition: Condition? = nil,
    orderBy orderList: [OrderBy]? = nil,
    offset: Offset? = nil) throws -> Object?
// 返回 sampleTable 中的所有数据
let allObjects: [Sample] = try database.getObjects(fromTable: "sampleTable")

// 返回 sampleTable 中 identifier 小于 5 或 大于 10 的行的数据
let objects: [Sample] = try database.getObjects(fromTable: "sampleTable", 
                                                where: Sample.Properties.identifier < 5 || Sample.Properties.identifier > 10)

// 返回 sampleTable 中 identifier 最大的行的数据
let object: Sample? = try database.getObject(fromTable: "sampleTable", 
                                             orderBy: Sample.Properties.identifier.asOrder(by: .descending))

由于对象查找操作使用了范型,因此需要显式声明返回值的类型以匹配范型。否则会报错 let allObjects = try database.getObjects(fromTable: "sampleTable") // 没有显式声明 allObjects 类型,范型无法匹配,无法编译通过。

1.3.4.2 对象部分查询

let objects: [Sample] = try database.getObjects(fromTable: "sampleTable", 
                                                on: Sample.Properties.identifier)

class PartialSample: TableCodable {
    var identifier: Int? = nil
    var description: String = ""
    
    enum CodingKeys: String, CodingTableKey {
        typealias Root = PartialSample
        static let objectRelationalMapping = TableBinding(CodingKeys.self)
        case identifier
        case description
    }
}

// 由于 description 是 String 类型,"getObject" 过程无法对其进行初始化,因此以下调用会出错。
// 正确的方式应将 `var description: String` 改为 `var description: String?`
let partialObjects: [PartialSample] = try database.getObjects(fromTable: "sampleTable", on: Sample.Properties.identifier)

1.3.4.3 值查询操作

func getRows(on columnResultConvertibleList: [ColumnResultConvertible],
             fromTable table: String,
             where condition: Condition? = nil,
             orderBy orderList: [OrderBy]? = nil,
             limit: Limit? = nil,
             offset: Offset? = nil) throws -> FundamentalRowXColumn
identifier description
1 "sample1"
2 "sample1"
3 "sample2"
4 "sample2"
5 "sample2"
  1. "getRows" 接口获取整个矩阵的所有内容,即返回值为二维数组。
  2. "getRow" 接口获取某一横行的数据,即返回值为一维数组。
  3. "getColumn" 接口获取某一纵列的数据,即返回值为一维数组。
  4. "getDistinctColumn" 与 "getColumn" 类似,但它会过滤掉重复的值。
  5. "getValue" 接口获取矩阵中某一个格的内容。
  6. "getDistinctValue" 与 "getValue" 类似,但它会过滤掉重复的值。
// 获取所有内容
let allRows = try database.getRows(fromTable: "sampleTable")
print(allRows[row: 2, column: 0].int32Value) // 输出 3

// 获取第二行
let secondRow = try database.getRow(fromTable: "sampleTable", offset: 1)
print(secondRow[0].int32Value) // 输出 2

// 获取 description 列
let descriptionColumn = try database.getColumn(on: Sample.Properties.description, fromTable: "sampleTable")
print(descriptionColumn) // 输出 "sample1", "sample1", "sample1", "sample2", "sample2" 

// 获取不重复的 description 列的值
let distinctDescriptionColumn = try database.getDistinctColumn(on: Sample.Properties.description, fromTable: "sampleTable")
print(distinctDescriptionColumn) // 输出 "sample1", "sample2"

// 获取第二行 description 列的值
let value = try database.getValue(on: Sample.Properties.description, offset: 1)
print(value.stringValue) // 输出 "sample1"

// 获取 identifier 的最大值
let maxIdentifier = try database.getValue(on: Sample.Properties.identifier.max(), fromTable: "sampleTable")

// 获取不重复的 description 的值
let distinctDescription = try database.getDistinctValue(on: Sample.Properties.description, fromTable: "sampleTable")
print(distinctDescription.stringValue) // 输出 "sample1"

1.4. 数据库,表,事务

  1. 支持增删查改的便捷接口
  2. 支持链式接口
  3. 数据和状态共享
  4. 线程安全
let myTag = 1
let database = Database(withPath: filePath)
database.tag = myTag
print(database.tag) // 输出 1

let databaseAlias = Database(withPath: filePath)
print(databaseAlias.tag) // 输出 1

let table = try database.getTable(named: "sampleTable", of: Sample.self)
print(table.tag) // 输出 1

let transaction = try database.getTransaction()
print(transaction.tag) // 输出 1

database.tag = 2
print(database.tag) // 输出 2
print(databaseAlias.tag) // 输出 2
print(table.tag) // 输出 2
print(transaction.tag) // 输出 2

基础类共享数据和状态的本质是,它们共享同一个 Core,而所有操作都在这个 Core 上发生。

WCDB Swift 支持 多线程读操作 或 单线程写多线程读 并发执行。

1.4.1 数据库

1.4.1.1 初始化

let filePath = "~/Intermediate/Directories/Will/Be/Created/sample.db"
let databaseWithPath = Database(withPath: filePath)

let fileURL = URL(fileURLWithPath: filePath)
let databaseWithFileURL = Database(withFileURL: fileURL)
let myTag = 1
databaseWithPath.tag = myTag

// 若该 tag 不存在,则初始化会失败
let databaseWithTag = try Database(withExistingTag: myTag)

1.4.1.2 标签

let myTag1 = 1
let database1 = Database(withPath: path1)
database1.tag = myTag1

let myTag2 = 2
let database2 = Database(withPath: path2)
database2.tag = myTag2
print(database1.tag) // 输出 1

let anotherDatabase1 = Database(withPath: path1)
print(anotherDatabase1.tag) // 输出 1

1.4.1.3 打开数据库

let database = Database(withPath: filePath)
print(database.isOpened) // 输出 false
try database.create(table: "sampleTable", of: Sample.self) // 数据库此时会被自动打开
print(database.isOpened) // 输出 true
let database1 = Database(withPath: filePath)
print(database1.isOpened) // 输出 false
print(database1.canOpen) // 输出 true。仅当数据库无法打开时,如路径无法创建等,该接口会返回 false
print(database1.isOpened) // 输出 true
let database2 = Database(withPath: filePath)
print(database2.isOpened) // 输出 true。WCDB Swift 同一路径的数据库共享数据和状态等。

1.4.1.4 关闭数据库

do {
    let database1 = Database(withPath: filePath)
    try database1.create(table: "sampleTable", of: Sample.self) // 数据库此时会被自动打开
    print(database1.isOpened) // 输出 true
} // 作用域结束,database1 deinit、关闭数据库并回收内存
let database2 = Database(withPath: filePath)
print(database2.isOpened) // 输出 false
let database1 = Database(withPath: filePath)
{
    let database2 = Database(withPath: filePath)
    try database2.create(table: "sampleTable", of: Sample.self) // 数据库此时会被自动打开
    print(database2.isOpened) // 输出 true
} // 作用域结束,database2 deinit,但 database1 仍持有该路径的数据库,因此不会被关闭。
print(database1.isOpened) // 输出 true
let database = Database(withPath: filePath)
print(database.canOpen) // 输出 true
print(database.isOpened) // 输出 true
database.close()
print(database.isOpened) // 输出 false

WCDB Swift 也提供了 blockade、unblockade 和 isBlockaded 接口用于分步执行关闭数据库操作,可参考相关接口文档

1.4.1.5 关闭数据库与线程安全

某些情况下,开发者需要确保数据库完全关闭后才能进行操作,如移动文件操作。

数据库是二进制文件,移动文件的过程中若数据发生了变化,则移动后的文件数据可能会不完整、损坏。因此,WCDB Swift 提供了 close: 接口。

try database.close(onClosed: {
    try database.moveFiles(toDirectory: otherDirectory)
})

onClosed 参数内,可确保数据库完全关闭,不会有其他线程的数据访问、操作数据库,因此可以安全地操作文件。

1.4.1.6 内存回收

// 回收 database 数据库中暂不使用的内存
database.purge() 
// 回收所有已创建的数据库中暂不使用的内存
Database.purge() 

1.4.1.7 文件操作

// 获取所有与该数据库相关的文件路径
print(database.paths) 
// 获取所有与该数据库相关的文件占用的大小
try database.close(onClosed: {
    // 数据库未关闭状态下也可获取文件大小,但不够准确,开发者可自行选择是否关闭
    let filesSize = try database.getFilesSize()
    print(filesSize)
})
// 删除所有与该数据库相关的文件
try database.close(onClosed: {
    try database.removeFiles()
})
// 将所有与该数据库相关的文件移动到另一个目录
try database.close(onClosed: {
    try database.moveFiles(toDirectory: otherDirectory)
})

1.4.2 表

let table = try database.getTable(named: "sampleTable", of: Sample.self) //  表不存在时会出错
// 返回值需指定为 [Sample] 类型以匹配范型
let objectsFromDatabase: [Sample] = try database.getObjects(fromTable: "sampleTable")

// table 已经指定了表名和模型绑定的类,因此可以直接获取
let objectsFromTable = try table.getObjects()

1.4.3 事务

try database.run(transaction: {
    try database.insert(objects: object, intoTable: "sampleTable")
})

let table = try database.getTable(named: "sampleTable", of: Sample.self)
table.run(transaction: {
    try database.insert(objects: object)
})

// 与 Database、Table 类似,开发者可以保存 Transaction 变量
let transaction = try database.getTransaction()
transacton.run(transaction: {
    print(transaction.isInTransction) // 输出 true
    try transaction.insert(objects: object)
})

1.4.3.1 性能

let object = Sample()
object.isAutoIncrement = true
let objects = Array(repeating: object, count: 100000)

// 单独插入,效率很差
for object in objects {
    try database.insert(objects: object, intoTable: "sampleTable")
}

// 事务插入,性能较好
try database.run(transaction: {
    for object in objects {
        try database.insert(objects: object, intoTable: "sampleTable")
    }
})

// insert(objects:intoTable:) 接口内置了事务,并对批量数据做了针对性的优化,性能更好
try database.insert(objects: objects, intoTable: "sampleTable")

1.4.3.2 原子性

DispatchQueue(label: "other thread").async {
    try database.delete(fromTable: "sampleTable")
}

try database.insert(object: object, intoTable: "sampleTable")
let objects = try database.getObjects(fromTable: "sampleTable")
print(objects.count) // 可能输出 0 或 1
DispatchQueue(label: "other thread").async {
    try database.delete(fromTable: "sampleTable")
}

try database.run(transaction: {
    try database.insert(objects: object, intoTable: "sampleTable")
    let objects = try database.getObjects(fromTable: "sampleTable")
    print(objects.count) // 输出 1
})

1.4.3.3 执行事务

  1. 普通事务
  2. 可控事务
  3. 嵌入事务
  1. 普通事务
// 普通事务
try database.run(transaction: {
    try database.insert(objects: object, intoTable: "sampleTable")
})
// 可控事务
try database.run(controllableTransaction: {
    try database.insert(objects: object, intoTable: "sampleTable")
    return true // 返回 true 以提交事务,返回 false 以回滚事务
})
  1. 可控事务

可控事务在普通事务的基础上,可以通过返回值控制提交或回滚事务

// 普通事务
try database.run(transaction: {
    try database.run(transaction: { // 出错,事务不能嵌套
        try database.insert(objects: object, intoTable: "sampleTable")
    })
})
// 嵌入事务
try database.run(transaction: {
    try database.run(embeddedTransaction: { // 嵌入事务可以嵌套
        try database.insert(objects: object, intoTable: "sampleTable")
    })
})
  1. 嵌入事务
  1. 嵌入事务在普通事务的基础上,支持嵌套调用。
  2. 当外层不存在事务时,嵌入事务和普通事务没有区别。
  3. 当外层存在事务时,嵌入事务会跟随外层事务的行为提交或回滚事务。

insert(objects:intoTable:)、insertOrReplace(objects:intoTable:)、create(table:of:) 等 WCDB Swift 自带的接口都使用了嵌入事务
WCDB Swift 也提供了 begin、commit 和 rollback 接口用于分步执行事务,可参考相关接口文档

1.5. 语言集成查询

let objects: [Sample] = try database.getObjects(fromTable: "sampleTable", where: Sample.Properties.idetifier > 1)

1.5.1 Column

let identifierColumn = Column(named: "identifier")
let statementInsert = StatementInsert().insert(intoTable: "sampleTable", 
                                               with: identifierColumn)
                                       .values(1)
print(statementInsert.description) // 输出 "INSERT INTO sampleTable(identifier) VALUES(1)"

1.5.2 ColumnResult

let identifierColumn = Column(named: "identifier")
let identifierColumnResult = ColumnResult(with: identifierColumn)
let statementSelect = StatementSelect().select(identifierColumnResult).from("sampleTable")
print(statementSelect.description) // 输出 "SELECT identifier FROM sampleTable"

1.5.3 Convertible

可以看到,Expression 也可以转换为 ColumnResult。

我们再回到 StatementSelect 语句的 select 函数,倘若它只接受 ColumnResult 类作为参数,那么每次调用时,都需要将 Expression 转换为 ColumnResult

// 以下为示例代码,并非 WCDB Swift 真正的实现
class StatementSelect {
    func select(_ columnResult: ColumnResult...) -> StatementSelect
    // ...
}

let identifierColumn = Column(named: "identifier")
let identifierExpression = Expression(identifierColumn)
let identifierColumnResult = ColumnResult(with: identifierExpression)
let statementSelect = StatementSelect().select(identifierColumnResult).from("sampleTable")
print(statementSelect.description) // 输出 "SELECT identifier FROM sampleTable"

可以看到,需要 3 重转换,才能将 Column 转换为我们需要的 ColumnResult。

为了解决这个问题,WCDB Swift 定义了 Convertible 协议,用于语法中可互相转换的类型。

// StatementSelect.swift
func select(distinct: Bool = false,
            _ columnResultConvertibleList: ColumnResultConvertible...
) -> StatementSelect 

基于 Convertible 协议,select 接口的参数也为 ColumnResultConvertible,即所有可转换为 ColumnResult 的类型,都能作为 select 函数的参数。

在 SQL 语法中,Expression 是能转换为 ColumnResult 的类型;而 Column 是能转换为 Expression 的类型,因此其也同时是能转换为 ColumnResult 的类型。

// WCDB Swift 内部的代码示例
protocol ExpressionConvertible: ColumnResultConvertible { /* ... */ }

struct Column: ExpressionConvertible { /* ... */ }
struct Expression: ColumnResultConvertible { /* ... */ }

因此,原来的 select 语句可以直接简写为:

let identifierColumn = Column(named: "identifier")
let statementSelect = StatementSelect().select(identifierColumn).from("sampleTable")
print(statementSelect.description) // 输出 "SELECT identifier FROM sampleTable"

WCDB Swift 内的 Convertible 接口协议较多,这里不一一赘述。开发者也无需逐一了解,在使用时再查阅接口即可。

1.5.4 手动转换

protocol ColumnResultConvertible {
    func asColumnResult() -> ColumnResult
} 
let identifierColumn = Column(named: "identifier")
let identifierColumnResult = identifierColumn.asColumnResult()

1.5.5 CodingKeys 和 Property

let property = Sample.Properties.identifier.asProperty()
let objects: [Sample] = try database.getObjects(on: property, fromTable: "sampleTable")
let statementSelect = StatementSelect().select(Sample.Properties.identifier).from("sampleTable")
print(statementSelect.description) // 输出 "SELECT identifier FROM sampleTable"

1.5.6 Expression

let expressionInt = Expression(with: 1)
let expressionDouble = Expression(with: 2.0)
let expressionString = Expression(with: "3")
let expressionData = Expression(with: "4".data(using: .ascii)!)
let expressionColumn = Expression(with: Column(named: "identifier"))

除此之外,还有一个内建的绑定参数 Expression.bindParameter 也是 Expression 类型。

let expression = expressionColumn.between(expressionInt, expressionDouble) 
print(expression.description) // 输出 "identifier BETWEEN 1 AND 2.0"
  1. || 运算符在 SQL 语法中用于字符串链接,而在 WCDB Swift 中则是用于"或"的逻辑运算。
  2. <> 运算符在 SQL 语法中用于不等比较,而在 WCDB Swift 中则是直接使用较为习惯的 != 运算符。
let expression1 = expressionInt + expressionDouble
print(expression1.description) // 输出 "(1 + 2.0)"

let expression2 = expressionColumn >= expression1
print(expression2.description) // 输出 "(identifier >= (1 + 2.0))"

// 基础类型 -1 可以直接转换为 Expression
let expression3 = expressionColumn < -1 || expression2
print(expression3.description) // 输出 "((identifier < -1) OR (identifier >= (1 + 2.0)))"

let statementSelect = StatementSelect().select(Sample.Properties.identifier)
                                       .from("sampleTable")
                                       .where(expression3)
print(statementSelect.description) // 输出 "SELECT identifier FROM sampleTable WHERE ((identifier < -1) OR (identifier >= (1 + 2.0)))"

1.5.7 ExpressionOperable

let expression = Sample.Properties.identifier.asExpression() > 1
print(expression)
let statementSelect = StatementSelect().select(Sample.Properties.identifier)
                                       .from("sampleTable")
                                       .where(Sample.Properties.identifier < -1 || Sample.Properties.identifier >= 3.0)
print(statementSelect.description) // 输出 "SELECT identifier FROM sampleTable WHERE ((identifier < -1) OR (identifier >= 3.0))"

1.5.8 Statement

1.5.9 SQL 到语言集成查询

  1. 在语法中确定其所属的 Statement。
  2. 对照对应 Statement 的语法,根据关键字对已有的 SQL 进行断句。
  3. 逐个通过语言集成查询的函数调用进行实现。
SELECT identifier.min() FROM sampleTable WHERE (identifier > 0 || identifier / 2 == 0 ) && description NOT NULL ORDER BY identifier ASC LIMIT 1, 100

1.5.9.1 归类

let statementSelect = StatementSelect()

1.5.9.2 断句

SELECT identifier.min() 
FROM sampleTable 
WHERE (identifier > 0 || identifier / 2 == 0 ) && description NOT NULL 
ORDER BY identifier ASC 
LIMIT 1, 100

1.5.9.3 调用语言集成查询

// distinct 参数的默认值为 false,也可以忽略不写
statementSelect.select(distinct: false, Sample.Properties.identifier.min())
statementSelect.from("sampleTable")
statementSelect.where((Sample.Properties.identifier > 0 || Sample.Properties.identifier / 2 == 0) && Sample.Properties.description.isNotNull())
statementSelect.order(by: Sample.Properties.identifier.asOrder(by: .ascending))
statementSelect.limit(from: 1, to: 100)

2.WCDB.Swfit 高级功能

2.1 加密与配置

2.1.1 默认配置

  1. PRAGMA locking_mode="NORMAL"
  2. PRAGMA synchronous="NORMAL"
  3. PRAGMA journal_mode="WAL"
  4. PRAGMA fullfsync=1

2.1.2 加密

// 该接口等同于配置 PRAGMA cipher="YourPassword",PRAGMA cipher_page_size=4096
func setCipher(key optionalKey: Data?, // 密码
               pageSize: Int = 4096) // 加密页大小

其中,pageSize 是加密的页大小参数,SQLCipher 在 iOS 上默认使用 4096,macOS 上默认使用 1024。而 WCDB Swift 则在所有平台上都适用 4096,以获得更好的性能。开发者一般不需要做特别的修改。

值得注意的是,设置密码是一件很慎重的事情。对于已经创建且存在数据的数据库,无论是原本未加密想要改为加密,还是已经加密想要修改密码,都是成本非常高的操作,因此不要轻易使用。更多相关信息可以参考官方文档

2.1.3 自定义配置

func setConfig(named name: String, // 配置名称,同名的配置会互相覆盖
               with callback: @escaping Config, // 配置的函数
               orderBy order: ConfigOrder) // 配置执行的顺序,顺序较小的配置优先执行。WCDB Swift 自带的配置从 0 开始。

database.setConfig(named: "secure_delete", with: { (handle: Handle) throws in 
    let statementPragmaSecureDelete = StatementPragma().pragma(.secureDelete, to: true)
    try handle.exec()
}, orderBy: 10)

2.2 高级接口

2.2.1 链式调用

let select: Select = try database.prepareSelect(of: Sample.self, fromTable: "sampleTable")
let objects: [Sample] = select.where(Sample.Properties.identifier > 1).limit(from: 2, to: 3).allObjects()

let delete: Delete = try database.prepareDelete(fromTable: "sampleTable")
                                 .where(Sample.Properties.identifier != 0)
try delete.execute()
print(delete.changes) // 获取该操作删除的行数

2.2.2 遍历查询

let select: Select = try database.prepareSelect(of: Sample.self, fromTable: "sampleTable")
                                 .where(Sample.Properties.identifier > 1)
                                 .limit(from: 2, to: 3)
while let object = try select.nextObject() {
    print(object)
}

2.2.3 联表查询

try database.create(table: "sampleTable", of: Sample.self)
try database.create(table: "sampleTableMulti", of: SampleMulti.self)

let multiSelect = try database.prepareMultiSelect(
        on:
            Sample.Properties.identifier.in(table: "sampleTable"),
            Sample.Properties.description.in(table: "sampleTable"),
            SampleMulti.Properties.identifier.in(table: "sampleTableMulti"),
            SampleMulti.Properties.description.in(table: "sampleTableMulti"),
        fromTables: tables,
        where: Sample.Properties.identifier.in(table: "sampleTable") == SampleMulti.Properties.identifier.in(table: "sampleTableMulti")
)
while let multiObject = try multiSelect.nextMultiObject() {
    let sample = multiObject["sampleTable"] as? Sample
    let sampleMulti = multiObject["sampleTableMulti"] as? SampleMulti
    // ...
}

2.2.4 查询重定向

let object = try database.getObject(on: Sample.Properties.identifier.max().as(Sample.Properties.identifier),
                                    fromTable: "sampleTable")
print(object.identifier)

2.2.5 核心层接口

// 1. 创建 Statement
let statement = StatementSelect().select(Column.any).from("sampleTable")

// 2. 创建 CoreStatement
let coreStatement = try database.prepare(statement)

// 3. 逐步执行 CoreStatment
while try coreStatement.step() {
    // 4. 获取值
    let identifier: Int? = coreStatement.value(atIndex: 0)
    let description: String? = coreStatement.value(atIndex: 1)
    // ...
}
// 5. 释放 CoreStatement。其 deinit 时也会自动释放
// try coreStatement.finalize()

2.3 监控与错误处理

2.4 自定义字段映射类型

2.5 全文搜索

2.6 损坏,备份,修复

上一篇 下一篇

猜你喜欢

热点阅读