-
Notifications
You must be signed in to change notification settings - Fork 1.4k
Swift 语言集成查询
语言集成查询(WCDB Integrated Language Query,简称 WINQ),是 WCDB 的一项基础特性。它使得开发者能够通过 Swift 的语法特性去完成 SQL 语句。
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
代表数据库内的一个字段,如 Insert
语句中的 column-name
,指定了需要插入的数据所属的字段。它可通过字段名创建。
let identifierColumn = Column(named: "identifier")
let statementInsert = StatementInsert().insert(intoTable: "sampleTable")
.columns(identifierColumn)
.values(1)
print(statementInsert.description) // 输出 "INSERT INTO sampleTable(identifier) VALUES(1)"
ResultColumn
通常代表数据库查询中的结果,如 Select
语句中的 result-column
,指定了期望查询的结果。
let identifierColumn = Column(named: "identifier")
let identifierResultColumn = ResultColumn(with: identifierColumn)
let statementSelect = StatementSelect().select(identifierResultColumn).from("sampleTable")
print(statementSelect.description) // 输出 "SELECT identifier FROM sampleTable"
细心观察语法中的描述可以发现,许多节点的参数可以不止一种。以刚才提到的 ResultColumn
为例。
可以看到,Expression
也可以转换为 ResultColumn
。
我们再回到 StatementSelect
语句的 select
函数,倘若它只接受 ResultColumn
类作为参数,那么每次调用时,都需要将 Expression
转换为 ResultColumn
。
// 以下为示例代码,并非 WCDB Swift 真正的实现
class StatementSelect {
func select(_ ResultColumn: ResultColumn...) -> StatementSelect
// ...
}
let identifierColumn = Column(named: "identifier")
let identifierExpression = Expression(identifierColumn)
let identifierResultColumn = ResultColumn(with: identifierExpression)
let statementSelect = StatementSelect().select(identifierResultColumn).from("sampleTable")
print(statementSelect.description) // 输出 "SELECT identifier FROM sampleTable"
可以看到,需要 3 重转换,才能将 Column
转换为我们需要的 ResultColumn
。
为了解决这个问题,WCDB Swift 定义了 Convertible
协议,用于语法中可互相转换的类型。
// StatementSelect.swift
func select(distinct: Bool = false,
_ resultColumnConvertibleList: ResultColumnConvertible...
) -> StatementSelect
基于 Convertible
协议,select
接口的参数为 ResultColumnConvertible
,即所有可转换为 ResultColumn
的类型,都能作为 select
函数的参数。
在 SQL 语法中,Expression
是能转换为 ResultColumn
的类型;而 Column
是能转换为 Expression
的类型,因此其也同时是能转换为 ResultColumn
的类型。
// WCDB Swift 内部的代码示例
protocol ExpressionConvertible: ResultColumnConvertible { /* ... */ }
class Column: ExpressionConvertible { /* ... */ }
class Expression: ResultColumnConvertible { /* ... */ }
因此,原来的 select
语句可以直接简写为:
let identifierColumn = Column(named: "identifier")
let statementSelect = StatementSelect().select(identifierColumn).from("sampleTable")
print(statementSelect.description) // 输出 "SELECT identifier FROM sampleTable"
WCDB Swift 内的 Convertible
接口协议较多,这里不一一赘述。开发者也无需逐一了解,在使用时再查阅接口即可。
除了通过接口参数的 Convertible
协议转换,也可以手动调用 asXXX
接口转换。如 ResultColumnConvertible
的协议为:
protocol ResultColumnConvertible {
func asResultColumn() -> ResultColumn
}
对于符合 ResultColumnConvertible
接口的类,直接调用即可:
let identifierColumn = Column(named: "identifier")
let identifierResultColumn = identifierColumn.asResultColumn()
CodingKeys
和 Property
严格来说不属于语言集成查询的一部分,它们是语言集成查询和模型绑定结合的产物。
当需要通过对象来操作数据库时,如"getObject"或者"update with object"等,WCDB Swift 不仅需要知道查找数据的哪个字段(即 Column
所完成的事情),还需要知道这个字段对应模型绑定中的哪个变量。
let property = Sample.Properties.identifier.asProperty()
let objects: [Sample] = try database.getObjects(on: property, fromTable: "sampleTable")
而 CodingKeys
和 Property
就是存储了数据库字段和模型绑定字段的映射关系,而且还可以分离这个映射,具体用法可参考高级用法一章的查询重定向。
基于模型绑定,开发者可以完全摆脱通过字符串创建 Column
,更便捷地操作数据库。Property
实现了ColumnConvertible
协议,WINQ中所有使用Column
的地方都可以使用Field
。
let statementSelect = StatementSelect().select(Sample.Properties.identifier).from("sampleTable")
print(statementSelect.description) // 输出 "SELECT identifier FROM sampleTable"
Sample.Properties.identifier
和Sample.CodingKeys.identifier
的写法没有区别。Properties
只是CodingKeys
的别名 -typealias Properties = CodingKeys
。这是为了让阅读代码时语义更加清晰。
Expression
可以算是 SQL 里最复杂的一个了。它在许多语句中都出现,比如查询语句的 where
、groupBy
、having
、limit
、offset
,更新语句的 value
、where
、limit
、offset
等等。同时,它的完整定义也很长,甚至还包含了递归。
从单个 Expression
的语法角度来看,它支持从数字、字符串、二进制、字段创建。而 WCDB Swift 将其扩展为支持所有字段映射类型,包括内建的类型和自定义的类型。
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
类型。
多个 Expression
之间可以通过函数或运算符的方式进行操作,如:
let expression = expressionColumn.between(expressionInt, expressionDouble)
print(expression.description) // 输出 "identifier BETWEEN 1 AND 2.0"
Expression
语法所支持的运算符有十多个,WCDB Swift 基本都支持,但在语义上改为更符合 Swift 的习惯。例如
-
||
运算符在 SQL 语法中用于字符串链接,而在 WCDB Swift 中则是用于"或"的逻辑运算。 -
<>
运算符在 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)))"
显然,每个类都要转换成 Expression
来进行这些操作,虽然也可以,但这就太麻烦了。
let expression = Sample.Properties.identifier.asExpression() > 1
print(expression)
因此,WCDB Swift 定义了 ExpressionOperable
协议。遵循该协议的类都可以与 Expression
的类似,使用函数或运算符进行语言集成查询,包括 Column
、Expression
、Property
和 CodingKeys
等。也因此,基于模型绑定,开发者可以完全摆脱通过拼装 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))"
Statement
在前文已经接触到不少了,如查询 StatementSelect
、插入 StatementInsert
等。它是一个最基本的完整可被执行的 SQL 语句。
SQLite 共包含 27 种 Statement
,WCDB Swift 基本都支持。根据语法规则创建的 Statement
,可以通过基础类的 exec(:)
函数直接执行,也可以使用Database
和Table
下面的这些接口获取执行Statement
的结果,
-
getValue(from:)
执行Statement
并获取一个结果。 -
getRow(from:)
执行Statement
并获取一行结果,结果为一维数组。 -
getColumn(from:)
系列接口执行Statement
并获取一列结果,结果为一维数组,支持返回多种基础类型的数组。 -
getRows(from:)
执行Statement
并获取多行结果,以二维数组的方式返回。
试考虑,表中的数据可以想象为一个矩阵的存在,假设其数据如下:
identifier | content |
---|---|
1 | "sample1" |
2 | "sample1" |
3 | "sample2" |
4 | "sample2" |
5 | "sample2" |
以下是使用Statement
进行查询操作的示例代码:
// 获取所有内容
let allRows = try database.getRows(from:
StatementSelect().select(Sample.Properties.all).from("sampleTable"))
print(allRows[2][0].intValue) //输出3
// 获取第二行
let secondRow = try database.getRow(from:
StatementSelect().select(Sample.Properties.all).from("sampleTable"))
print(secondRow![0].intValue) //输出2
// 获取第二行 content 列的值
let contentColumn = try database.getColumn(from:
StatementSelect().select(Sample.Properties.content).from("sampleTable"))
print(contentColumn[3]) //输出 "sample2"
// 获取 identifier 的最大值
let maxId = try database.getValue(from:
StatementSelect().select(Sample.Properties.identifier.max()).from("sampleTable"))
print(maxId!.intValue) //输出5
// 获取不同的 content 数
let distinctContentCount = try database.getValue(from:
StatementSelect().select(Sample.Properties.content.count().distinct()).from("sampleTable"))
print(distinctContentCount!.intValue) //输出2
还可以通过 getOrCreatePreparedStatement(:)
创建 PreparedStatement
对象做进一步的操作。我们会在高级接口的Handle一节详细介绍。
如前文所说,SQL 的复杂性决定了不可能介绍每一个类型及语句。因此,这里将介绍如何将一个已有的 SQL,转写为语言集成查询。开发者可以以此为例子,触类旁通。
对于已有的 SQL:
- 在语法中确定其所属的
Statement
。 - 对照对应
Statement
的语法,根据关键字对已有的 SQL 进行断句。 - 逐个通过语言集成查询的函数调用进行实现。
以如下 SQL 为例:
SELECT min(identifier) FROM sampleTable WHERE (identifier > 0 || identifier / 2 == 0 ) && description NOT NULL ORDER BY identifier ASC LIMIT 1, 100
根据 Statement
的列表,显然这个 SQL 属于 StatementSelect
。
let statementSelect = StatementSelect()
根据 StatementSelect
的语法规则,按关键词(即语法中的大写字母)进行断句:
SELECT min(identifier)
FROM sampleTable
WHERE (identifier > 0 || identifier / 2 == 0 ) && description NOT NULL
ORDER BY identifier ASC
LIMIT 1, 100
根据每个关键词,找到语言集成查询中对应的函数,并完成其参数。
如 SELECT
关键词对应 StatementSelect
的 select(distinct:_:)
函数:
// distinct 参数的默认值为 false,也可以忽略不写
statementSelect.select(distinct: false, Sample.Properties.identifier.min())
statementSelect.from("sampleTable")
WHERE
的参数虽然较复杂,但也都能找到对应的函数:
statementSelect.where((Sample.Properties.identifier > 0 || Sample.Properties.identifier / 2 == 0) && Sample.Properties.description.notNull())
其他语句也同理:
statementSelect.order(by: Sample.Properties.identifier.asOrder(by: .ascending))
statementSelect.limit(from: 1, to: 100)
SQLite的官网给出了它支持的全部SQL语法的结构图,还是以最复杂的select语句为例:
WINQ的整体设计思路是把整个select语句包装成一个StatementSelect
对象,把图中的圆角方框的连接点包装成StatementSelect
对象的方法,如:with
、select
、from
、where
、groupby
、having
、orderBy
、limit
等等这些方法。
图中直角方框的内容则是设计成StatementSelect
对象的方法的入参。一些简单的入参则是支持使用基础类型,比如from
方法可以传入一个字符串表示表名,一些复杂的入参则是继续包装成一个对象,比如前面提到的ResultColumn
和Expression
,其他的还有CommonTableExpression
、Join
、WindowDef
等等这些对象。
所以在编写复杂SQL语句时,可以先找到对应的Statement
对象,然后找到需要调用的方法,最后再根据方法需要的入参传入具体值。按照这个思路,可以根据SQL的语法结构图编写出所有SQLite支持的SQL语句。
[Swift-Object-Relational-Mapping-Builtin-Column-Codable-Type]:
- 欢迎使用 WCDB
- 基础教程
- 进阶教程
- 欢迎使用 WCDB
- 基础教程
- 进阶教程
- 欢迎使用 WCDB
- 基础教程
- 进阶教程
- 欢迎使用 WCDB
- 基础教程
- 进阶教程