Today we will take a look at the new Swift 5 string interpolation API, we will try them by building SQL queries using placeholders.
We rely on our PostgreSQL escaping through the correct parameters passed to the query to prevent SQL injection. We write an initialization Query
, and initialization of the query string automatically create placeholders - forms $1
, $2
and so on - for every need escape value.
Construction of the query string
To illustrate this point, let's look at a simplified version of the back-end code written by 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
}
}
复制代码
Let's create a sample query, retrieve the ID by its users. Initializing an array of values and accepts a build
create function generated from the query string placeholder. This build
function receives a placeholder we pass each value:
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"])
复制代码
String interpolation
Swift 5 the string interpolation public, which means that we can achieve our own interpolation type, automatically insert a placeholder value. This will allow us to use without build
creating a query function in the case of:
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
}
}
复制代码
Next, we need QueryPart
to comply with both ExpressibleByStringLiteral
and ExpressibleByStringInterpolation
:
extension QueryPart: ExpressibleByStringLiteral {
init(stringLiteral value: String) {
self.sql = value
self.values = []
}
}
extension QueryPart: ExpressibleByStringInterpolation {
}
复制代码
The last extension has been compiled since the protocol has a default implementation that type of interpolation in the standard library:
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
// ... }
复制代码
We want to override this default by specifying our own type of accord StringInterpolationProtocol
, which will be added to our type of each segment QueryPart
:
struct QueryPartStringInterpolation: StringInterpolationProtocol {
// ... }
extension QueryPart: ExpressibleByStringInterpolation {
typealias StringInterpolation = QueryPartStringInterpolation
}
复制代码
This new type of interpolation is that we implement custom behavior we want in the query string value is inserted into place. The first thing that is required to initialize the program we have implemented in our case does not need to do anything:
struct QueryPartStringInterpolation: StringInterpolationProtocol {
init(literalCapacity: Int, interpolationCount: Int) {
}
}
复制代码
String interpolation works is that we will need additional calls each segment - the string literals and interpolation. To track what we received, we need to have the same two properties QueryPart
:
struct QueryPartStringInterpolation: StringInterpolationProtocol {
var sql: String = ""
var values: [SQLValue] = []
init(literalCapacity: Int, interpolationCount: Int) {
}
}
复制代码
The next step is to add a variety of additional methods. First attach a string literal:
struct QueryPartStringInterpolation: StringInterpolationProtocol {
var sql: String = ""
var values: [SQLValue] = []
// ...
mutating func appendLiteral(_ literal: String) {
sql += literal
}
}
复制代码
The second method is to attach SQL value, we give it a corresponding parameter of our call site label. Within a method, we first received value to the value of our array, and then attach a new placeholder in the query string:
struct QueryPartStringInterpolation: StringInterpolationProtocol {
var sql: String = ""
var values: [SQLValue] = []
// ...
mutating func appendInterpolation(param value: SQLValue) {
sql += "$\(values.count + 1)"
values.append(value)
}
}
复制代码
In QueryPart
, we must add the initialization procedure, it requires QueryPartStringInterpolation
:
extension QueryPart: ExpressibleByStringInterpolation {
typealias StringInterpolation = QueryPartStringInterpolation
init(stringInterpolation: QueryPartStringInterpolation) {
self.sql = stringInterpolation.sql
self.values = stringInterpolation.values
}
}
复制代码
10:34 Now compile the code, we can check our sample query is constructed correctly:
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"])
复制代码
It works! We have a query string ID value placeholder values
array containing the ID. Let's try to add another value:
let id = "1234"
let email = "[email protected]"
let sample = Query<String>("SELECT * FROM users WHERE id=\(param: id) AND email=\(email)", parse: { $0[0] })
复制代码
This does not compile because we forget the param:
label, which is actually a good thing: we do not want to insert any string. After we add tags, we test it Query
in accordance with the way we expect construction:
assert(sample.query.sql == "SELECT * FROM users WHERE id=$1 AND email=$2")
assert(sample.query.values == [id, email])
复制代码
Insert the original string
In practice, our back-end code base, we have the Codable
dynamically generated query types, which provide a table name should be used. So we also want to be able to dynamically insert a table name in the query:
let tableName = "users"
let sample = Query<String>("SELECT * FROM \(raw: tableName) WHERE id=\(param: id) AND email=\(param: email)", parse: { $0[0] })
复制代码
This section does not have to escape, we want to make this clear once again, to avoid accidentally insert a random string in the query. So we use labels raw:
to interpolate:
struct QueryPartStringInterpolation: StringInterpolationProtocol {
// ...
mutating func appendInterpolation(raw value: String) {
sql += value
}
}
复制代码
Simplified type
We can clean up the code by simplifying the type we use. We have been QueryPart
in line with ExpressibleByStringInterpolation
, and then we introduced QueryPartStringInterpolation
a string interpolation type. However, we can QueryPart
own a string for interpolation, instead of having two separate types duplicate attributes:
extension QueryPart: ExpressibleByStringInterpolation {
typealias StringInterpolation = QueryPart
init(stringInterpolation: QueryPart) {
self.sql = stringInterpolation.sql
self.values = stringInterpolation.values
}
}
复制代码
These two properties QueryPart
have become variable:
struct QueryPart {
var sql: String
var values: [SQLValue]
}
复制代码
Then we initialize them in the desired initialization routine:
extension QueryPart: StringInterpolationProtocol {
init(literalCapacity: Int, interpolationCount: Int) {
self.sql = ""
self.values = []
}
// ... }
复制代码
This is what we have to do is eliminate the need for a separate type QueryPartStringInterpolation
.
additional terms
In our back-end, we can build a basic query, to find records by ID, the same as today's example query, and then we can be attached to the basic query clause. In this way, we can specify additional filtering (using different criteria) or sort (by additional ORDER BY
clause), without having to write basic queries twice.
To do this, we must add an additional method for themselves Query
. Let's focus on adding this feature in the next.
We bring our string interpolation excited about the possibility. This is a brand new tool, as a community, we still need to figure out all the things we can do with it.
Reprinted from: www.jianshu.com/p/a9d29bf11...
Reproduced in: https: //juejin.im/post/5cfe4d156fb9a07eee5ec321