Mihael Isaev

MIT License Swift 4.1 Swift.Stream


⚠️这个库已经**过时**了⚠️请使用 SwifQLBridges

快速介绍

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。

通过 Swift Package Manager 安装

编辑你的 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)")

一些例子

1. 简单

// 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)

2. 简单的带 join

// 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)

3. 中等难度 🙂,带有查询到 jsonB 对象

// 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
}

4. 复杂

让我们来看看如何在一些示例请求中使用它

想象一下你有一个汽车列表

所以你有 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 模型。

顺便说一句,这是一个原始 SQL 等效项
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

那么,为什么你需要使用这个库来进行复杂的查询呢?

原因 #1 是 KeyPaths!

如果你将来要更改你的模型,你必须记住你在哪里使用了指向此模型属性的链接,并手动重写它们。如果你忘记了一个,你将在生产中头疼。但是通过 KeyPaths,你将只有在所有指向模型属性的链接都是最新的时才能编译你的项目。 更好的是,你将能够使用 Xcode 的 refactor 功能!😄

原因 #2 是 if/else 语句

使用 FQL 的查询构建器,你可以在任何需要的地方使用 if/else。 与在使用 if/else 创建原始查询字符串时相比,它非常方便。😉

原因 #3

它比多个连续请求更快

原因 #4

你可以 join on join on join on join on join on join 😁😁😁

使用这个库,你可以进行真正复杂的查询!🔥 并且你仍然很灵活,因为你可以在构建时使用 if/else 语句,甚至可以使用 let separateQuery = FQL(copy: originalQuery) 🕺 创建两个具有相同基础的不同查询

方法

FQL 提供的方法列表

Select (选择)

这些方法将添加将在 SELECTFROM 之间使用的字段

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)

From (来自)

方法 SQL 等效项
.from("Table") FROM "Table"
.from(raw: "Table") FROM Table
.from(Car.self) FROM "Cars" as "_cars_"
.from(someAlias) FROM "SomeAlias" as "someAlias"

Join (连接)

.join(FQJoinMode, Table, where: FQWhere)

enum FQJoinMode {
    case left, right, inner, outer
}

作为 Table 你可以放置 Car.selfsomeAlias

关于 FQWhere 请阅读下面

Where (条件)

.where(FQWhere)

你可以用两种方式编写 where 谓词

第一种是面向对象的

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 代码。

predicate 是什么?

它可以是 KeyPath operator KeyPathKeyPath operator Value

KeyPath 可能是 \Car.idsomeAlias.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)
Where 分组示例

如果你需要像这样对谓词进行分组

"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 (分组条件)

.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()

Group by (分组)

.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)

Order by (排序)

.orderBy(FQOrderBy(\Car.year, .asc).and(someAlias.k(\.name), .desc))

.orderBy(.asc(\Car.year), .desc(someAlias.k(\.name)))

Offset (偏移量)

方法 SQL 等效项
.offset(0) OFFSET 0

Limit (限制)

方法 SQL 等效项
.limit(30) LIMIT 30

JSON

你可以通过创建 FQJSON 实例来构建 jsonjsonb 对象

实例 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)

别名 (Aliases)

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)

自定义 DateDecodingStrategy (日期解码策略)

默认情况下,日期解码策略是 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 昵称。