使用Swift 5的新字符串插值API在SQL查询中自动插入占位
今天我们将看看Swift 5中的新字符串插值API,我们将通过使用占位符构建SQL查询来尝试它们。
我们依靠PostgreSQL通过正确转义我们传递给查询的参数来阻止SQL注入。我们写了一个初始化Query
,并且初始化自动建立的查询字符串的占位符-的形式$1
,$2
等等-对于每一个需要转义值。
构建查询字符串
为了说明这一点,让我们看一下用Swift 4.2编写的后端代码的简化版本:
typealias SQLValue = String
struct Query<A> {
let sql: String
let values: [SQLValue]
let parse: ([SQLValue]) -> A
typealias Placeholder = String
init(values: [SQLValue], build: ([Placeholder]) -> String, parse: @escaping ([SQLValue]) -> A) {
let placeholders = values.enumerated().map { "$\($0.0 + 1)" }
self.values = values
self.sql = build(placeholders)
self.parse = parse
}
}
让我们创建一个示例查询,通过其ID检索用户。初始化器接受一个值数组和一个build
从生成的占位符创建查询字符串的函数。此build
函数接收我们传入的每个值的占位符:
let id = "1234"
let sample = Query<String>(values: [id], build: { params in
"SELECT * FROM users WHERE id=\(params[0])"
}, parse: { $0[0] })
assert(sample.sql == "SELECT * FROM users WHERE id=$1")
assert(sample.values == ["1234"])
使用字符串插值
Swift 5使字符串插值公开,这意味着我们可以实现我们自己的插值类型,自动插入值的占位符。这将允许我们在不使用build
函数的情况下创建查询:
let sample = Query<String>("SELECT * FROM users WHERE id=\(param: id)", parse: { $0[0] })
struct QueryPart {
let sql: String
let values: [SQLValue]
}
struct Query<A> {
let query: QueryPart
let parse: ([SQLValue]) -> A
init(_ part: QueryPart, parse: @escaping ([SQLValue]) -> A) {
self.query = part
self.parse = parse
}
}
接下来,我们需要QueryPart
遵守两者 ExpressibleByStringLiteral
并且ExpressibleByStringInterpolation
:
extension QueryPart: ExpressibleByStringLiteral {
init(stringLiteral value: String) {
self.sql = value
self.values = []
}
}
extension QueryPart: ExpressibleByStringInterpolation {
}
最后一个扩展已经编译,因为协议有一个默认实现,即标准库中的插值类型:
public protocol ExpressibleByStringInterpolation : ExpressibleByStringLiteral {
/// The type each segment of a string literal containing interpolations
/// should be appended to.
associatedtype StringInterpolation : StringInterpolationProtocol = DefaultStringInterpolation where Self.StringLiteralType == Self.StringInterpolation.StringLiteralType
// ... }
我们想通过指定我们自己的符合的类型来覆盖这个默认实现StringInterpolationProtocol
,这将是我们追加到的每个段的类型QueryPart
:
struct QueryPartStringInterpolation: StringInterpolationProtocol {
// ... }
extension QueryPart: ExpressibleByStringInterpolation {
typealias StringInterpolation = QueryPartStringInterpolation
}
这个新的插值类型是我们在查询字符串中插入值时实现我们想要的自定义行为的地方。我们必须实现的第一件事是必需的初始化程序,在我们的例子中不需要做任何事情:
struct QueryPartStringInterpolation: StringInterpolationProtocol {
init(literalCapacity: Int, interpolationCount: Int) {
}
}
字符串插值的工作方式是我们将调用每个需要附加的段 - 即字符串文字和插值。为了跟踪我们收到的内容,我们需要具有以下相同的两个属性QueryPart
:
struct QueryPartStringInterpolation: StringInterpolationProtocol {
var sql: String = ""
var values: [SQLValue] = []
init(literalCapacity: Int, interpolationCount: Int) {
}
}
下一步是添加各种附加方法。第一个附加一个字符串文字:
struct QueryPartStringInterpolation: StringInterpolationProtocol {
var sql: String = ""
var values: [SQLValue] = []
// ...
mutating func appendLiteral(_ literal: String) {
sql += literal
}
}
第二种方法是附加SQL值,我们给它一个与我们的调用站点对应的参数标签。在方法内部,我们首先将接收到的值附加到我们的值数组中,然后在查询字符串中附加一个新的占位符:
struct QueryPartStringInterpolation: StringInterpolationProtocol {
var sql: String = ""
var values: [SQLValue] = []
// ...
mutating func appendInterpolation(param value: SQLValue) {
sql += "$\(values.count + 1)"
values.append(value)
}
}
在QueryPart
,我们必须添加初始化程序,它需要QueryPartStringInterpolation
:
extension QueryPart: ExpressibleByStringInterpolation {
typealias StringInterpolation = QueryPartStringInterpolation
init(stringInterpolation: QueryPartStringInterpolation) {
self.sql = stringInterpolation.sql
self.values = stringInterpolation.values
}
}
10:34现在代码编译,我们可以检查我们的示例查询是否正确构建:
let id = "1234"
let sample = Query<String>("SELECT * FROM users WHERE id=\(param: id)", parse: { $0[0] })
assert(sample.query.sql == "SELECT * FROM users WHERE id=$1")
assert(sample.query.values == ["1234"])
它有效!我们的查询字符串有一个ID值占位符,values
数组包含ID。让我们尝试添加另一个值:
let id = "1234"
let email = "mail@objc.io"
let sample = Query<String>("SELECT * FROM users WHERE id=\(param: id) AND email=\(email)", parse: { $0[0] })
这不会编译,因为我们忘记了param:
标签,这实际上是一件好事:我们不想插入任意字符串。在我们添加标签后,我们测试它Query
是按照我们期望的方式构建的:
assert(sample.query.sql == "SELECT * FROM users WHERE id=$1 AND email=$2")
assert(sample.query.values == [id, email])
插入原始字符串
在我们后端的实际代码库中,我们从Codable
类型动态生成查询,这些类型提供应该使用的表名。所以我们还希望能够在查询中动态插入表名:
let tableName = "users"
let sample = Query<String>("SELECT * FROM \(raw: tableName) WHERE id=\(param: id) AND email=\(param: email)", parse: { $0[0] })
此段不必转义,我们希望再次明确这一点,以避免意外地在查询中插入随机字符串。所以我们使用标签raw:
进行插值:
struct QueryPartStringInterpolation: StringInterpolationProtocol {
// ...
mutating func appendInterpolation(raw value: String) {
sql += value
}
}
简化类型
我们可以通过简化我们使用的类型来清理代码。我们已经QueryPart
符合ExpressibleByStringInterpolation
,然后我们引入QueryPartStringInterpolation
了字符串插值类型。但是,我们可以将QueryPart
自己用于字符串插值,而不是使用具有重复属性的两个单独类型 :
extension QueryPart: ExpressibleByStringInterpolation {
typealias StringInterpolation = QueryPart
init(stringInterpolation: QueryPart) {
self.sql = stringInterpolation.sql
self.values = stringInterpolation.values
}
}
这两个属性QueryPart
必须变得可变:
struct QueryPart {
var sql: String
var values: [SQLValue]
}
然后我们在所需的初始化程序中初始化它们:
extension QueryPart: StringInterpolationProtocol {
init(literalCapacity: Int, interpolationCount: Int) {
self.sql = ""
self.values = []
}
// ... }
这就是我们要做的就是消除对单独类型的需要,QueryPartStringInterpolation
。
附加条款
在我们的后端,我们可以构建一个基本查询,按ID查找记录,就像今天的示例查询一样,然后我们就可以在该基本查询中附加子句。这样,我们可以指定额外的过滤(使用其他条件)或排序(通过附加ORDER BY
子句),而无需编写两次基本查询。
为此,我们必须为自己添加一个附加方法Query
。让我们在下一集中添加该功能。
我们对字符串插值带给我们的可能性感到兴奋。这是一个全新的工具,作为一个社区,我们仍然需要弄清楚我们可以用它做的所有事情。