Skip to content

Swift 增删查改

qiuwenchen edited this page Mar 7, 2024 · 9 revisions

增删查改是数据库最常用的功能,因此 WCDB Swift 对其进行了特别的封装,使其通过一行代码即可完成操作。

插入操作

插入操作有 insertinsertOrReplaceinsertOrIgnore 三个接口。故名思义,第一个只是单纯的插入数据,当数据出现冲突时会失败,而后两个在主键冲突等约束冲突出现时,insertOrReplace会把新数据会覆盖旧数据,insertOrIgnore则是会忽略冲突数据,而不产生错误。

以已经完成模型绑定的类 Sample 为例:

class Sample: TableCodable {
    var identifier: Int? = nil
    var description: String? = nil
    
    enum CodingKeys: String, CodingTableKey {
        typealias Root = Sample
        case identifier
        case description
        
        static let objectRelationalMapping = TableBinding(CodingKeys.self) {
            BindColumnConstraint(identifier, 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") // 插入成功,且 description 的内容会被替换为 "insertOrReplace"

sample.description = "insertOrIgnore"
try database.insertOrIgnore(objects: object, intoTable: "sampleTable") // 插入成功,但 description 的内容不会变更,还是"insertOrReplace"

关于自增插入,可参考模型绑定 - 自增属性一章。

"insert" 函数的原型为:

// 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 会自动开启事务,进行批量化地插入,以获得更好的性能。

删除操作

删除操作只有一个接口,其函数原型为:

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")

这里的 conditionlimitoffset 本质都是遵循 ExpressionConvertible 的对象,可以是数字、字符串、字段或其他更多的组合。同样地,我们会在语言集成查询进一步介绍。

删除接口不会删除表本身,开发者需要调用 drop(table:) 接口删除表。

更新操作

更新操作有 "update with object" 和 "update with row" 两个接口。它们的原型分别

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

其中 propertyConvertibleListconditionlimitoffset 都在前文介绍过了,这里不再赘述。 两个接口除了 with 之后的参数,其他都一致。

"with object" 故名思义,通过 object 对象进行更新。以下是更新操作的示例代码:

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)

而 "with row" 接口则是通过 row 来对数据进行更新。row 是遵循 ColumnEncodable 协议的类型的数组。ColumnEncodable 协议会在后续自定义字段映射类型中进一步介绍。这里只需了解,能够进行字段映射的类型基本都遵循 ColumnEncodable 协议。

因此,与 "with object" 对应的示例代码为:

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)

查找操作

查找接口对应的操作有 8 个,分别为

  • getObjects
  • getObject
  • getRows
  • getRow
  • getColumn
  • getDistinctColumn
  • getValue
  • getDistinctValue

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

对象查找操作

getObjectsgetObject 都是对象查找的接口,他们直接返回已进行模型绑定的对象。它们的函数原型为:

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?

其中 propertyConvertibleListconditionlimitoffset 都在前文介绍过了,这里不再赘述。 而 "getObject" 等价于 limit: 1 时的 "getObjects" 接口。不同的是,它直接返回 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 类型,范型无法匹配,无法编译通过。

对象部分查询

与 "insert"、"update" 类似,对象查找操作也支持指定字段,例如:

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

这里只获取了 identifier 字段,而没有获取 description 的值。这就可能与 Swift 本身存在冲突。 Swift 规定了对象创建时,必须初始化所有成员变量。而进行对象部分查询时,则可能出现某部分变量没有变查询,因此无法初始化的情况。因此,对于可能不被查询的成员变量,应将其类型定义为可选值。 对于 Sample 类中,上述 "getObjects" 接口虽然没有获取 description 的值,但由于 descriptionString? 类型,因此不会出错。 而以下则是会出错的例子:

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)

倘若开发者不确定哪些字段可能会被进行对象部分查询,可以将所有字段都定义为可选。

值查询操作

其余的 6 个查询接口都是值查询操作,它们都属于 "getRows" 接口的简化接口。其接口声明如下:

func getRows(on columnResultConvertibleList: [ColumnResultConvertible],
             fromTable table: String,
             where condition: Condition? = nil,
             orderBy orderList: [OrderBy]? = nil,
             limit: Limit? = nil,
             offset: Offset? = nil) throws -> FundamentalRowXColumn

其中 conditionorderListlimitoffset,前文已经介绍,这里不再赘述。 columnResultConvertibleList 是遵循 ColumnResultConvertible 协议的对象数组,我们会在语言集成查询进一步介绍。

这里只需了解 Sample.Properties.identifier.max() 是遵循 ColumnResultConvertible 协议的对象,用于查找 identifier 列的最大值。

试考虑,表中的数据可以想象为一个矩阵的存在,假设其数据如下:

identifier description
1 "sample1"
2 "sample1"
3 "sample2"
4 "sample2"
5 "sample2"

在不考虑 conditionorderListlimitoffset 参数的情况下:

  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"
Clone this wiki locally