• Column
  • ColumnResult
  • Convertible
    • 手动转换
  • CodingKeys 和 Property
  • Expression
  • ExpressionOperable
  • Statement
  • SQL 到语言集成查询
    • 归类
    • 断句
    • 调用语言集成查询

    语言集成查询(WCDB Integrated Language Query,简称 WINQ),是 WCDB 的一项基础特性。它使得开发者能够通过 Swift 的语法特性去完成 SQL 语句。

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

    其中 where: 参数后的 Sample.Properties.idetifier > 1 就是语言集成查询的其中一个写法。其虽然是 identifier 和数字 1 的比较,但其结果并不为 Bool 值,而是 Expression。该 Expression 作为 SQL 的 where 参数,用于数据库查询。

    语言集成查询基于 SQLite 的 SQL 语法实现。只要是 SQL 支持的语句,都能使用语言集成查询完成。也因此,语言集成查询具有和 SQL 语法一样的复杂性,本文不可能将所有涉及的接口和类都介绍一遍。而是结合 WCDB Swift,介绍常用的类型,并在最后说明如何将已有 SQL 转换为语言集成查询的写法。

    Column

    Column 代表数据库内的一个字段,如 Insert 语句中的 column-name,指定了需要插入的数据所属的字段。它可通过字段名创建。

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

    ColumnResult

    ColumnResult 通常代表数据库查询中的结果,如 Select 语句中的 result-column,指定了期望查询的结果。

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

    Convertible

    细心观察语法中的描述可以发现,许多节点的参数可以不止一种。以刚才提到的 ColumnResult 为例。

    WINQ-Column-Result

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

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

    1. // 以下为示例代码,并非 WCDB Swift 真正的实现
    2. class StatementSelect {
    3. func select(_ columnResult: ColumnResult...) -> StatementSelect
    4. // ...
    5. }
    6.  
    7. let identifierColumn = Column(named: "identifier")
    8. let identifierExpression = Expression(identifierColumn)
    9. let identifierColumnResult = ColumnResult(with: identifierExpression)
    10. let statementSelect = StatementSelect().select(identifierColumnResult).from("sampleTable")
    11. print(statementSelect.description) // 输出 "SELECT identifier FROM sampleTable"

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

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

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

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

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

    1. // WCDB Swift 内部的代码示例
    2. protocol ExpressionConvertible: ColumnResultConvertible { /* ... */ }
    3.  
    4. struct Column: ExpressionConvertible { /* ... */ }
    5. struct Expression: ColumnResultConvertible { /* ... */ }

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

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

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

    手动转换

    除了通过接口参数的 Convertible 协议转换,也可以手动调用 asXXX 接口转换。如 ColumnResultConvertible 的协议为:

    1. protocol ColumnResultConvertible {
    2. func asColumnResult() -> ColumnResult
    3. }

    对于符合 ColumnResultConvertible 接口的类,直接调用即可:

    1. let identifierColumn = Column(named: "identifier")
    2. let identifierColumnResult = identifierColumn.asColumnResult()

    CodingKeys 和 Property

    CodingKeysProperty 严格来说不属于语言集成查询的一部分,它们是语言集成查询和模型绑定结合的产物。

    当需要通过对象来操作数据库时,如"getObject"或者"update with object"等,WCDB Swift 不仅需要知道查找数据的哪个字段(即 Column 所完成的事情),还需要知道这个字段对应模型绑定中的哪个变量。

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

    CodingKeysProperty 就是存储了数据库字段和模型绑定字段的映射关系。不同的是,后者可以分离这个映射,具体用法可参考高级用法一章的查询重定向。

    基于模型绑定,开发者可以完全摆脱通过字符串创建 Column,更便捷地操作数据库。

    1. let statementSelect = StatementSelect().select(Sample.Properties.identifier).from("sampleTable")
    2. print(statementSelect.description) // 输出 "SELECT identifier FROM sampleTable"
    Sample.Properties.identifierSample.CodingKeys.identifier 的写法没有区别。Properties 只是 CodingKeys 的别名 - typealias Properties = CodingKeys。这是为了让阅读代码时语义更加清晰。

    Expression

    Expression 可以算是 SQL 里最复杂的一个了。它在许多语句中都出现,比如查询语句的 wheregroupByhavinglimitoffset,更新语句的 valuewherelimitoffset等等。同时,它的完整定义也很长,甚至还包含了递归。

    从单个 Expression 的语法角度来看,它支持从数字、字符串、二进制、字段创建。而 WCDB Swift 将其扩展为支持所有字段映射类型,包括[内建的类型][Swift-Object-Relational-Mapping-Builtin-Column-Codable-Type]和自定义的类型。

    1. let expressionInt = Expression(with: 1)
    2. let expressionDouble = Expression(with: 2.0)
    3. let expressionString = Expression(with: "3")
    4. let expressionData = Expression(with: "4".data(using: .ascii)!)
    5. let expressionColumn = Expression(with: Column(named: "identifier"))
    除此之外,还有一个内建的绑定参数 Expression.bindParameter 也是 Expression 类型。

    多个 Expression 之间可以通过函数或运算符的方式进行操作,如:

    1. let expression = expressionColumn.between(expressionInt, expressionDouble)
    2. print(expression.description) // 输出 "identifier BETWEEN 1 AND 2.0"

    Expression 语法所支持的运算符有十多个,WCDB Swift 基本都支持,但在语义上改为更符合 Swift 的习惯。例如

    • || 运算符在 SQL 语法中用于字符串链接,而在 WCDB Swift 中则是用于"或"的逻辑运算。
    • <> 运算符在 SQL 语法中用于不等比较,而在 WCDB Swift 中则是直接使用较为习惯的 != 运算符。
    1. let expression1 = expressionInt + expressionDouble
    2. print(expression1.description) // 输出 "(1 + 2.0)"
    3.  
    4. let expression2 = expressionColumn >= expression1
    5. print(expression2.description) // 输出 "(identifier >= (1 + 2.0))"
    6.  
    7. // 基础类型 -1 可以直接转换为 Expression
    8. let expression3 = expressionColumn < -1 || expression2
    9. print(expression3.description) // 输出 "((identifier < -1) OR (identifier >= (1 + 2.0)))"
    10.  
    11. let statementSelect = StatementSelect().select(Sample.Properties.identifier)
    12. .from("sampleTable")
    13. .where(expression3)
    14. print(statementSelect.description) // 输出 "SELECT identifier FROM sampleTable WHERE ((identifier < -1) OR (identifier >= (1 + 2.0)))"

    ExpressionOperable

    显然,每个类都要转换成 Expression 来进行这些操作,虽然也可以,但这就太麻烦了。

    1. let expression = Sample.Properties.identifier.asExpression() > 1
    2. print(expression)

    因此,WCDB Swift 定义了 ExpressionOperable 协议。遵循该协议的类都可以与 Expression 的类似,使用函数或运算符进行语言集成查询,包括 ColumnExpressionPropertyCodingKeys 等。也因此,基于模型绑定,开发者可以完全摆脱通过拼装 Expression,更便捷地操作数据库。

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

    Statement

    Statement 在前文已经接触到不少了,如查询 StatementSelect、插入 StatementInsert 等。它是一个最基本的完整可被执行的 SQL 语句。

    SQLite 共包含 27 种 Statement,WCDB Swift 基本都支持。根据语法规则创建的 Statement,可以通过基础类的 exec(:) 函数直接执行,可以通过 prepare(:) 创建 CoreStatement 对象做进一步的操作。我们会在高级接口的核心层接口一节详细介绍。

    SQL 到语言集成查询

    如前文所说,SQL 的复杂性决定了不可能介绍每一个类型及语句。因此,这里将介绍如何将一个已有的 SQL,转写为语言集成查询。开发者可以以此为例子,触类旁通。

    对于已有的 SQL:

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

    归类

    根据 Statement 的列表,显然这个 SQL 属于 StatementSelect

    1. let statementSelect = StatementSelect()

    断句

    根据 StatementSelect 的语法规则,按关键词(即语法中的大写字母)进行断句:

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

    调用语言集成查询

    根据每个关键词,找到语言集成查询中对应的函数,并完成其参数。

    SELECT 关键词对应 StatementSelectselect(distinct:_:) 函数:

    1. // distinct 参数的默认值为 false,也可以忽略不写
    2. statementSelect.select(distinct: false, Sample.Properties.identifier.min())
    3. statementSelect.from("sampleTable")

    WHERE 的参数虽然较复杂,但也都能找到对应的函数:

    1. statementSelect.where((Sample.Properties.identifier > 0 || Sample.Properties.identifier / 2 == 0) && Sample.Properties.description.isNotNull())

    其他语句也同理:

    1. statementSelect.order(by: Sample.Properties.identifier.asOrder(by: .ascending))
    2. statementSelect.limit(from: 1, to: 100)