今天咱们将看看Swift 5中的新字符串插值API,咱们将经过使用占位符构建SQL查询来尝试它们。sql
咱们依靠PostgreSQL经过正确转义咱们传递给查询的参数来阻止SQL注入。咱们写了一个初始化Query
,而且初始化自动创建的查询字符串的占位符-的形式$1
,$2
等等-对于每个须要转义值。swift
为了说明这一点,让咱们看一下用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
函数接收咱们传入的每一个值的占位符:api
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
:bash
extension QueryPart: ExpressibleByStringLiteral {
init(stringLiteral value: String) {
self.sql = value
self.values = []
}
}
extension QueryPart: ExpressibleByStringInterpolation {
}
复制代码
最后一个扩展已经编译,由于协议有一个默认实现,即标准库中的插值类型:app
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
。让咱们在下一集中添加该功能。
咱们对字符串插值带给咱们的可能性感到兴奋。这是一个全新的工具,做为一个社区,咱们仍然须要弄清楚咱们能够用它作的全部事情。