今天咱們將看看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
。讓咱們在下一集中添加該功能。
咱們對字符串插值帶給咱們的可能性感到興奮。這是一個全新的工具,做爲一個社區,咱們仍然須要弄清楚咱們能夠用它作的全部事情。