struct PublicUser: Codable {
var name: String
var petName: String
var petType: String
var petToysQuantity: Int
}
try FQL()
.select(all: User.self)
.select(\Pet.name, as: "petName")
.select(\PetType.name, as: "petType")
.select(.count(\PetToy.id), as: "petToysQuantity")
.from(User.self)
.join(.left, Pet.self, where: \Pet.id == \User.idPet)
.join(.left, PetType.self, where: \PetType.id == \Pet.idType)
.join(.left, PetToy.self, where: \PetToy.idPet == \Pet.id)
.groupBy(\User.id, \Pet.id, \PetType.id, \PetToy.id)
.execute(on: conn)
.decode(PublicUser.self) // -> Future<[PublicUser]> 🔥🔥🔥
这是一个 Swift 库,它可以使用 KeyPaths 更轻松地构建复杂的原始 SQL 查询。我称它为 FQL 😎
专为 Vapor3 构建,并依赖于 Fluent
包,因为它使用 Model.reflectProperty(forKey:)
方法来解码 KeyPaths。
编辑你的 Package.swift
文件
//add this repo to dependencies
.package(url: "https://github.com/MihaelIsaev/FluentQuery.git", from: "0.4.30")
//and don't forget about targets
//"FluentQuery"
我喜欢编写原始 SQL 查询,因为它能够灵活地使用数据库引擎的所有功能。
Vapor 的 Fluent 允许你执行原始查询,但原始查询的最大问题是难以维护。
我遇到了这个问题,因此我开始开发这个库,以便使用 KeyPaths 以 Swift 的方式编写原始 SQL 查询。
让我们来看看我们有什么 :)
首先,你需要导入该库
import FluentQuery
然后创建 FQL
对象,使用下面描述的方法构建你的 SQL 查询,第一步只需将其作为原始字符串打印出来
let query = FQL()
//some building
print("rawQuery: \(query)")
// SELECT * FROM "User" WHERE age > 18
let fql = FQL().select(all: User.self)
.from(User.self)
.where(\User.age > 18)
.execute(on: conn)
.decode(User.self)
// SELECT u.*, r.name as region FROM "User" as u WHERE u.age > 18 LEFT JOIN "UserRegion" as r ON u.idRegion = r.id
let fql = FQL().select(all: User.self)
.select(\UserRegion.name)
.from(User.self)
.where(\User.age > 18)
.join(.left, UserRegion.self, where: \User.idRegion == \UserRegion.id)
.execute(on: conn)
.decode(UserWithRegion.self)
// SELECT (SELECT to_jsonb(u)) as user, (SELECT to_jsonb(r)) as region FROM "User" as u WHERE u.age > 18 LEFT JOIN "UserRegion" as r ON u.idRegion = r.id
let fql = FQL().select(.row(User.self), as: "user")
.select(.row(UserRegion.self), as: "region")
.from(User.self)
.where(\User.age > 18)
.join(.left, UserRegion.self, where: \User.idRegion == \UserRegion.id)
.execute(on: conn)
.decode(UserWithRegion.self)
// in this case UserWithRegion struct will look like this
struct UserWithRegion: Codable {
var user: User
var region: UserRegion
}
让我们来看看如何在一些示例请求中使用它
想象一下你有一个汽车列表
所以你有 Car
Fluent 模型
final class Car: Model {
var id: UUID?
var year: String
var color: String
var engineCapacity: Double
var idBrand: UUID
var idModel: UUID
var idBodyType: UUID
var idEngineType: UUID
var idGearboxType: UUID
}
以及相关的模型
final class Brand: Decodable {
var id: UUID?
var value: String
}
final class Model: Decodable {
var id: UUID?
var value: String
}
final class BodyType: Decodable {
var id: UUID?
var value: String
}
final class EngineType: Decodable {
var id: UUID?
var value: String
}
final class GearboxType: Decodable {
var id: UUID?
var value: String
}
好的,你想获得每辆汽车作为方便的 Codable 模型
struct PublicCar: Content {
var id: UUID
var year: String
var color: String
var engineCapacity: Double
var brand: Brand
var model: Model
var bodyType: BodyType
var engineType: EngineType
var gearboxType: GearboxType
}
这是针对这种情况的示例请求代码
func getListOfCars(_ req: Request) throws -> Future<[PublicCar]> {
return req.requestPooledConnection(to: .psql).flatMap { conn -> EventLoopFuture<[PublicCar]> in
defer { try? req.releasePooledConnection(conn, to: .psql) }
return FQL()
.select(distinct: \Car.id)
.select(\Car.year, as: "year")
.select(\Car.color, as: "color")
.select(\Car.engineCapacity, as: "engineCapacity")
.select(.row(Brand.self), as: "brand")
.select(.row(Model.self), as: "model")
.select(.row(BodyType.self), as: "bodyType")
.select(.row(EngineType.self), as: "engineType")
.select(.row(GearboxType.self), as: "gearboxType")
.from(Car.self)
.join(.left, Brand.self, where: \Brand.id == \Car.idBrand)
.join(.left, Model.self, where: \Model.id == \Car.idModel)
.join(.left, BodyType.self, where: \BodyType.id == \Car.idBodyType)
.join(.left, EngineType.self, where: \EngineType.id == \Car.idEngineType)
.join(.left, GearboxType.self, where: \GearboxType.id == \Car.idGearboxType)
.groupBy(\Car.id, \Brand.id, \Model.id, \BodyType.id, \EngineType.id, \GearboxType.id)
.orderBy(.asc(\Brand.value), .asc(\Model.value))
.execute(on: conn)
.decode(PublicCar.self)
}
}
哈哈,很酷,对吧? 😃
正如你所看到的,我们构建了一个复杂的查询来获取所有依赖的值,并将 Postgres 原始响应解码为我们的 Codable 模型。
SELECT
DISTINCT c.id,
c.year,
c.color,
c."engineCapacity",
(SELECT toJsonb(brand)) as "brand",
(SELECT toJsonb(model)) as "model",
(SELECT toJsonb(bt)) as "bodyType",
(SELECT toJsonb(et)) as "engineType",
(SELECT toJsonb(gt)) as "gearboxType"
FROM "Cars" as c
LEFT JOIN "Brands" as brand ON c."idBrand" = brand.id
LEFT JOIN "Models" as model ON c."idModel" = model.id
LEFT JOIN "BodyTypes" as bt ON c."idBodyType" = bt.id
LEFT JOIN "EngineTypes" as et ON c."idEngineType" = et.id
LEFT JOIN "GearboxTypes" as gt ON c."idGearboxType" = gt.id
GROUP BY c.id, brand.id, model.id, bt.id, et.id, gt.id
ORDER BY brand.value ASC, model.value ASC
如果你将来要更改你的模型,你必须记住你在哪里使用了指向此模型属性的链接,并手动重写它们。如果你忘记了一个,你将在生产中头疼。但是通过 KeyPaths,你将只有在所有指向模型属性的链接都是最新的时才能编译你的项目。 更好的是,你将能够使用 Xcode 的 refactor
功能!😄
使用 FQL
的查询构建器,你可以在任何需要的地方使用 if/else
。 与在使用 if/else
创建原始查询字符串时相比,它非常方便。😉
它比多个连续请求更快
你可以 join on join on join on join on join on join 😁😁😁
使用这个库,你可以进行真正复杂的查询!🔥 并且你仍然很灵活,因为你可以在构建时使用 if/else 语句,甚至可以使用 let separateQuery = FQL(copy: originalQuery)
🕺 创建两个具有相同基础的不同查询
FQL
提供的方法列表
这些方法将添加将在 SELECT
和 FROM
之间使用的字段
SELECT _这里是一些字段列表_ FROM
因此,要添加你想要选择的内容,请依次调用这些方法
方法 | SQL 等效项 |
---|---|
.select("*") | * |
.select(all: Car.self) | "Cars".* |
.select(all: someAlias) | "some_alias".* |
.select(\Car.id) | "Car".id |
.select(someAlias.k(.id)) | "some_alias".id |
.select(distinct: \Car.id) | DISTINCT "Car".id |
.select(distinct: someAlias.k(.id)) | DISTINCT "some_alias".id |
.select(.count(\Car.id), as: "count") | COUNT("Cars".id) as "count" |
.select(.sum(\Car.value), as: "sum") | SUM("Cars".value) as "sum" |
.select(.average(\Car.value), as: "average") | AVG("Cars".value) as "average" |
.select(.min(\Car.value), as: "min") | MIN("Cars".value) as "min" |
.select(.max(\Car.value), as: "max") | MAX("Cars".value) as "max" |
.select(.extract(.day, .timestamp, \Car.createdAt), as: "creationDay") | EXTRACT(DAY FROM "Cars".createdAt) as "creationDay" |
.select(.extract(.day, .interval, "40 days 1 minute"), as: "creationDay") | EXTRACT(DAY FROM INTERVAL '40 days 1 minute') as "creationDay" |
.select(by: .rowNumber, over: FQOver , as: "rowNumber") |
rowNumber() OVER (partition BY EXPRESSION ORDER BY SOMETHING ) as "rowNumber" |
顺便说一句,请阅读下面的别名和 FQOver
如果你需要使用像 rowNumber、rank、dense_rank 等窗口函数,像这样
rowNumber() OVER(partition BY "Record".title, "Record".tag ORDER BY "Record".priority ASC) as "rowNumber"
(参考:https://postgresql.ac.cn/docs/current/static/functions-window.html)
那么你可以像这样构建它
let fqo = FQOver(.partition)
.by(\Record.title, \Record.tag)
.orderBy(.asc(\Record.priority))
然后像这样在你的查询中使用它
let FQL()
.select(\Record.id)
.select(by: .rowNumber, over: fqo, as: "rowNumber")
.from(Record.self)
方法 | SQL 等效项 |
---|---|
.from("Table") | FROM "Table" |
.from(raw: "Table") | FROM Table |
.from(Car.self) | FROM "Cars" as "_cars_" |
.from(someAlias) | FROM "SomeAlias" as "someAlias" |
.join(FQJoinMode, Table, where: FQWhere)
enum FQJoinMode {
case left, right, inner, outer
}
作为 Table
你可以放置 Car.self
或 someAlias
关于 FQWhere
请阅读下面
.where(FQWhere)
第一种是面向对象的
FQWhere(predicate).and(predicate).or(predicate).and(FQWhere).or(FQWhere)
第二种是面向谓词的
AND 语句的示例
\User.email == "sam@example.com" && \User.password == "qwerty" && \User.active == true
OR 语句的示例
\User.email == "sam@example.com" || \User.email == "james@example.com" || \User.email == "bob@example.com"
AND 和 OR 语句的示例
\User.email == "sam@example.com" && FQWhere(\User.role == .admin || \User.role == .staff)
FQWhere()
在这里做什么?它将 OR 语句分组到圆括号中以实现 a AND (b OR c)
sql 代码。
它可以是 KeyPath operator KeyPath
或 KeyPath operator Value
KeyPath
可能是 \Car.id
或 someAlias.k(\.id)
Value
可能是任何值,如 int、string、uuid、array,甚至是一些 optional 或 nil
你在上面的速查表看到的可用运算符列表
一些例子
FQWhere(someAlias.k(\.deletedAt) == nil)
FQWhere(someAlias.k(\.id) == 12).and(\Car.color ~~ ["blue", "red", "white"])
FQWhere(\Car.year == "2018").and(\Brand.value !~ ["Chevrolet", "Toyota"])
FQWhere(\Car.year != "2005").and(someAlias.k(\.engineCapacity) > 1.6)
如果你需要像这样对谓词进行分组
"Cars"."engineCapacity" > 1.6 AND ("Brands".value LIKE '%YO%' OR "Brands".value LIKE '%ET')
那么就这样做
FQWhere(\Car.engineCapacity > 1.6).and(FQWhere(\Brand.value ~~ "YO").or(\Brand.value ~= "ET"))
运算符 | SQL 等效项 | 描述 |
---|---|---|
== | == / IS | 等于 |
!= | != / IS NOT | 不等于 |
> | > | > |
< | < | < |
>= | >= | >= |
<= | <= | <= |
~~ | IN () | 在数组中 |
!~ | NOT IN () | 不在数组中 |
~= | LIKE '%str' | 区分大小写的文本搜索 |
~~ | LIKE '%str%' | |
=~ | LIKE 'str%' | |
~% | ILIKE '%str' | 不区分大小写的文本搜索 |
%% | ILIKE '%str%' | |
%~ | ILIKE 'str%' | |
!~= | NOT LIKE '%str' | 区分大小写的文本搜索,其中文本不像字符串 |
!~~ | NOT LIKE '%str%' | |
!=~ | NOT LIKE 'str%' | |
!~% | NOT ILIKE '%str' | 不区分大小写的文本搜索,其中文本不像字符串 |
!%% | NOT ILIKE '%str%' | |
!%~ | NOT ILIKE 'str%' | |
~~~ | @@ 'str' | 全文搜索 |
.having(FQWhere)
关于 FQWhere
你已经在上面阅读过,但由于 having 在数据聚合之后调用,你可以额外使用聚合函数(如 SUM、COUNT、AVG、MIN、MAX
)来过滤你的结果
.having(FQWhere(.count(\Car.id) > 0))
//OR
.having(FQWhere(.count(someAlias.k(\.id)) > 0))
//and of course you an use .and().or().groupStart().groupEnd()
.groupBy(\Car.id, \Brand.id, \Model.id)
或
.groupBy(FQGroupBy(\Car.id).and(\Brand.id).and(\Model.id))
或
let groupBy = FQGroupBy(\Car.id)
groupBy.and(\Brand.id)
groupBy.and(\Model.id)
.groupBy(groupBy)
.orderBy(FQOrderBy(\Car.year, .asc).and(someAlias.k(\.name), .desc))
或
.orderBy(.asc(\Car.year), .desc(someAlias.k(\.name)))
方法 | SQL 等效项 |
---|---|
.offset(0) | OFFSET 0 |
方法 | SQL 等效项 |
---|---|
.limit(30) | LIMIT 30 |
你可以通过创建 FQJSON
实例来构建 json
或 jsonb
对象
实例 | SQL 等效项 |
---|---|
FQJSON(.normal) | build_json_object() |
FQJSON(.binary) | build_jsonb_object() |
创建实例后,你应该通过调用 .field(key, value)
方法来填充它,像这样
FQJSON(.binary).field("brand", \Brand.value).field("model", someAlias.k(\.value))
正如你所看到的,它接受 keyPaths 和别名 keypaths
但它也接受函数作为值,这是可用函数的列表
函数 | SQL 等效项 |
---|---|
row(Car.self) | SELECT row_to_json("Cars") |
row(someAlias) | SELECT row_to_json("some_alias") |
extractEpochFromTime(\Car.createdAt) | extract(epoch from "Cars"."createdAt") |
extractEpochFromTime(someAlias.k(.createdAt)) | extract(epoch from "some_alias"."createdAt") |
count(\Car.id) | COUNT("Cars".id) |
count(someAlias.k(.id)) | COUNT("some_alias".id) |
countWhere(\Car.id, FQWhere(\Car.year == "2012")) | COUNT("Cars".id) filter (where "Cars".year == '2012') |
countWhere(someAlias.k(.id), FQWhere(someAlias.k(.id) > 12)) | COUNT("some_alias".id) filter (where "some_alias".id > 12) |
FQAlias<OriginalClass>(aliasKey)
或 OriginalClass.alias(aliasKey)
如果你只需要它的一个变体,你也可以使用静态别名 OriginalClass.alias
你可以生成随机别名 OriginalClass.randomAlias
,但请记住,每次调用 randomAlias
都会生成一个新的别名,因为它是一个计算属性
当你编写复杂的查询时,你可能需要对同一个表进行多个 joins 或子查询,你需要使用别名,例如 "Cars" as c
使用 FQL 你可以像这样创建别名
//"CarBrand" as b
let aliasBrand = CarBrand.alias("b")
//"CarModel" as m
let aliasModel = CarModel.alias("m")
//"EngineType" as e
let aliasEngineType = EngineType.alias("e")
你可以像这样使用引用到这些别名的原始表的 KeyPaths
aliasBrand.k(\.id)
aliasBrand.k(\.value)
aliasModel.k(\.id)
aliasModel.k(\.value)
aliasEngineType.k(\.id)
aliasEngineType.k(\.value)
.execute(on: PostgreSQLConnection)
try FQL().select(all: User.self).execute(on: conn)
.decode(Decodable.Type, dateDecodingstrategy: JSONDecoder.DateDecodingStrategy?)
try FQL().select(all: User.self).execute(on: conn).decode(PublicUser.self)
默认情况下,日期解码策略是 yyyy-MM-dd'T'HH:mm:ss.SSS'Z'
,它与 postgres timestamp
兼容
但你可以像这样指定自定义 DateDecodingStrategy
try FQL().select(all: User.self).execute(on: conn).decode(PublicUser.self, dateDecodingStrategy: .secondsSince1970)
或者像这样
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"
try FQL().select(all: User.self).execute(on: conn).decode(PublicUser.self, dateDecodingStrategy: .formatted(formatter))
或者,如果你在同一个模型中有两个或多个具有不同日期格式的列,那么你可以创建你自己的日期格式化程序,如 issue #3 中所述
我希望这对某些人有用。
非常感谢你的反馈!
不要犹豫问我问题,我准备在 Vapor 的 discord chat 中提供帮助,找到我 @iMike 昵称。