MIT License Swift 5.2 Github Actions Swift.Stream

桥梁 (Bridges)

通过纯 NIO 驱动程序,使用 SwifQL 处理 Postgres 和 MySQL。

给一个 ⭐️ 来支持 Bridges 的开发

安装

它可以用在纯 NIO2 应用程序上,但我目前没有示例。

您可以参考 VaporBridges 的实现,使其在您的纯 NIO2 应用程序中工作。

Vapor4 + PostgreSQL

.package(url: "https://github.com/SwifQL/PostgresBridge.git", from:"1.0.0-rc"),
.package(url: "https://github.com/SwifQL/VaporBridges.git", from:"1.0.0-rc"),
.target(name: "App", dependencies: [
    .product(name: "Vapor", package: "vapor"),
    .product(name: "PostgresBridge", package: "PostgresBridge"),
    .product(name: "VaporBridges", package: "VaporBridges")
]),

Vapor4 + MySQL

.package(url: "https://github.com/SwifQL/MySQLBridge.git", from:"1.0.0-rc"),
.package(url: "https://github.com/SwifQL/VaporBridges.git", from:"1.0.0-rc"),
.target(name: "App", dependencies: [
    .product(name: "Vapor", package: "vapor"),
    .product(name: "MySQLBridge", package: "MySQLBridge"),
    .product(name: "VaporBridges", package: "VaporBridges")
]),

文档

以下所有示例都将针对 Vapor 4 和 PostgreSQL,但您可以采用相同的方式为 MySQL 和任何其他框架实现它。

日志 (Logger)

您可以设置日志级别,例如在 configure.swift

// optionally set global application log level before setting bridges log level
app.logger.logLevel = .notice
app.bridges.logger.logLevel = .debug

配置 (Configuration)

在您的应用程序启动之前,初始化(但不是必须)连接池到您的数据库(在 configure.swift 中)

否则,当您第一次尝试获取数据库连接时,将创建 poll。

app.postgres.register(.psqlEnvironment)

这里 .psqlEnvironment 是您的数据库的标识符。

这是一种基于环境变量的默认自动标识符,它期望以下环境变量

PG_DB
PG_HOST - optional, 127.0.0.1 by default
PG_PORT - optional, 5432 by default
PG_USER - optional, `postgres` by default
PG_PWD - optional, empty string by default (will fix it to nil by default)

因此,PG_DB 是使此自动标识符工作的唯一必需环境变量。

您可以通过编写此类扩展来为您所有的数据库甚至不同的主机创建您自己的标识符

extension DatabaseIdentifier {
    public static var myDb1: DatabaseIdentifier {
        .init(name: "my-db1", host: .myMasterHost, maxConnectionsPerEventLoop: 1)
    }
    public static var myDb1Slave: DatabaseIdentifier {
        .init(name: "my-db1", host: .mySlaveHost, maxConnectionsPerEventLoop: 1)
    }
}
extension DatabaseHost {
    public static var myMasterHost: DatabaseHost {
        return .init(hostname: "127.0.0.1", username: "<username>", password: "<password or nil>", port: 5432, tlsConfiguration: nil)
    }
    public static var mySlaveHost: DatabaseHost {
        return .init(hostname: "192.168.0.200", username: "<username>", password: "<password or nil>", port: 5432, tlsConfiguration: nil)
    }
}

一旦您配置了数据库连接,您就可以开始使用它们了。

表 (Tables)、枚举 (Enums) 和结构体 (Structs)

让我们从 EnumStruct 开始,然后在 Table 中使用它们。

枚举 (Enum)

枚举声明非常简单,只需将其符合 StringInt,以及 BridgesEnum

import Bridges

enum Gender: String, BridgesEnum {
    case male, female, other
}

或者

import Bridges

enum Priority: Int, BridgesEnum {
    case high = 0
    case medium = 1
    case low = 2
}

结构体 (Struct)

结构体声明类似于枚举:只需将其符合 SwifQLCodable

import Bridges

struct AccountOptions: SwifQLCodable {
    var twoFactorAuthEnabled: Bool
    var lastPasswordChangeDate: Date
}

表 (Table)

主要的事情是将您的模型符合 Table 并对所有字段使用 @Column

import Bridges

final class User: Table {
    @Column("id")
    var id: UUID

    @Column("email")
    var email: String

    @Column("name")
    var name: String

    @Column("password")
    var password: String

    @Column("gender")
    var gender: Gender

    @Column("account_options")
    var accountOptions: AccountOptions

    @Column("createdAt")
    public var createdAt: Date

    @Column("updatedAt")
    public var updatedAt: Date

    @Column("deletedAt")
    public var deletedAt: Date?

    /// See `Table`
    init () {}
}

默认情况下,Bridges 创建的表名类似于类名 User。 如果您想给表自定义名称,请使用 tableName 静态变量来设置它

final class User: Table {
    /// set any custom name here
    /// otherwise it will take the name of the table class (`User` in this case)
    static var tableName: String { "users" }

    @Column("id")
    var id: UUID

    // ...

上面的例子将为类 User 创建 users 表。

迁移 (Migrations)

表 (Table)

为了方便起见,您的迁移结构体应符合 TableMigration

struct CreateUser: TableMigration {
    /// set any custom name here
    /// otherwise it will take the name of the migration struct (`CreateUser` in this case)
    static var migrationName: String { "create_user_table" }

    typealias Table = User

    static func prepare(on conn: BridgeConnection) -> EventLoopFuture<Void> {
        createBuilder
            .column("id", .uuid, .primaryKey)
            .column("email", .text, .unique, .notNull)
            .column("name", .text, .notNull)
            .column("password", .text, .notNull)
            .column("gender", .auto(from: Gender.self), .notNull)
            .column("account_options", .jsonb, .notNull)
            .column("createdAt", .timestamptz, .default(Fn.now()), .notNull)
            .column("updatedAt", .timestamptz, .notNull)
            .column("deletedAt", .timestamptz)
            .execute(on: conn)
    }

    static func revert(on conn: BridgeConnection) -> EventLoopFuture<Void> {
        dropBuilder.execute(on: conn)
    }
}

migrationName 变量在运行后设置 migrations 表中迁移的描述,以便 Bridges 知道哪些迁移已部署以及哪些需要在当前批处理中部署。

警告: 尽管可以使用列的键路径 .column(\.$id, .uuid, .primaryKey),但强烈建议您使用 String 类型的列名 .column("id", .uuid, .primaryKey),因为以后当您有很多列重命名的迁移时,您应该能够从头开始运行项目,并且所有迁移将一个接一个地运行,并且它们应该通过。 如果您使用键路径,它们将会失败。

.column() 非常强大,您可以在此处设置名称、类型、默认值和约束

带有检查约束的 rating 列的示例

.column(\.$rating, .int, .notNull, .check(\WorkerReview.$rating >= 0 && \WorkerReview.$rating <= 5))

带有引用(外键)约束的列的示例

.column("workerId", .uuid, .notNull, .references(Worker.self, onDelete: .cascade, onUpdate: .noAction))

另外,我应该说在 TableMigration 中我们有 createBuilderupdateBuilderdropBuilder

在上面的示例中,您可以看到如何使用 createBuilderdropBuilder

createBuilderdropBuilder 都实现了在创建和删除表时的安全检查。在创建表之前,您可以强制迁移检查是否不存在您要添加到数据库的表。当您想删除表时,同样适用,检查是否存在这样的表。

struct CreateUser: TableMigration {
    typealias Table = User

    static func prepare(on conn: BridgeConnection) -> EventLoopFuture<Void> {
        createBuilder
            .checkIfNotExists()
            .column("id", .uuid, .primaryKey)
            // ...
            .execute(on: conn)
    }

    static func revert(on conn: BridgeConnection) -> EventLoopFuture<Void> {
        dropBuilder
            .checkIfExists()
            .execute(on: conn)
    }
}

要更新表,您可以使用 updateBuilder

struct UpdateUser: TableMigration {
    typealias Table = User

    static func prepare(on conn: BridgeConnection) -> EventLoopFuture<Void> {
        updateBuilder
            // adds a check with constraint or expression
            .addCheck(...)
            // you could add new column same way as with `createBuilder`
            .addColumn(...)
            // adds foreign key
            .addForeignKey(...)
            // adds primary key to one or more columns
            .addPrimaryKey(...)
            // adds unique constraint to one or more columns
            .addUnique(...)
            // creates index
            .createIndex(...)
            // drops column
            .dropColumn(...)
            // drops default value at specified column
            .dropDefault(...)
            // drops index by its name
            .dropIndex(...)
            // drops constraint by its name
            .dropConstraint(...)
            // drops `not null` mark at specified column
            .dropNotNull(...)
            // renames column
            .renameColumn(...)
            // renames table
            .renameTable(...)
            // set default value for specified column
            .setDefault(...)
            // mark column as `not null`
            .setNotNull(...)
            // ...
            .execute(on: conn)
    }

    static func revert(on conn: BridgeConnection) -> EventLoopFuture<Void> {
        updateBuilder
            // use update builder to revert updates as well
            .execute(on: conn)
    }
}

枚举 (Enum)

为了方便起见,您的迁移结构体应符合 EnumMigration

struct CreateEnumGender: EnumMigration {
    /// set any custom name here
    /// otherwise it will take the name of the migration struct (`CreateEnumGender` in this case)
    static var migrationName: String { "CreateEnumGender" }

    typealias Enum = Gender

    static func prepare(on conn: BridgeConnection) -> EventLoopFuture<Void> {
        createBuilder
            .add(.male, .female, .other)
            .execute(on: conn)
    }

    static func revert(on conn: BridgeConnection) -> EventLoopFuture<Void> {
        dropBuilder.execute(on: conn)
    }
}

您还可以使用带有 .add() 方法的原始字符串,如下所示

createBuilder
    .add("male", "female", "other")
    .execute(on: conn)

如您所见,我们这里也有 createBuilderdropBuilder,但这里我们也有完全可用的 updateBuilder

// to add one value in the end
updateBuilder.add("bigender").execute(on: conn)
// to add multiple values
updateBuilder.add("bigender").add("mtf", after: "male").add("ftm", before: "female")

迁移执行

我更喜欢在 configure.swift 附近创建 migrations.swift,因为我们在应用程序启动之前执行迁移

// migrations.swift
import Vapor
import PostgresBridge

func migrations(_ app: Application) throws {
    // create `migrations` object on your database connection
    let migrator = app.postgres.migrator(for: .myDb1)

    // Enums

    migrator.add(CreateEnumGender.self) // to create `Gender` enum type in db

    // Models

    migrator.add(CreateUser.self) // to create `User` table

    // migrator.add(SomeCustomMigration.self) // could be some seed migration :)

    try migrator.migrate().wait() // will run all provided migrations one by one inside a transaction
//    try migrator.revertLast().wait() // will revert only last batch
//    try migrator.revertAll().wait() // will revert all migrations one by one in desc order
}

然后在 configure.swift 的末尾某处运行它们

// Called before your application initializes.
public func configure(_ app: Application) throws {
    // some initializations

    try migrations(app)
    try routes(app)
}

查询 (Queries)

使用 SwifQL 的全部功能来构建您的查询。 一旦查询准备就绪,就在连接上执行它。

💡您可以在 ApplicationRequest 对象上获取连接。

Application 对象的示例,例如 configure.swift 文件

// Called before your application initializes.
public func configure(_ app: Application) throws {
    app.postgres.connection(to: .myDb1) { conn in
        SwifQL.select(User.table.*).from(User.table).execute(on: conn).all(decoding: User.self).flatMap { rows in
            print("yaaay it works and returned \(rows.count) rows!")
        }
    }.whenComplete {
        switch $0 {
        case .success: print("query was successful")
        case .failure(let error): print("query failed: \(error)")
        }
    }
}

Request 对象的示例

💡User 表模型应该符合 Content 协议才能作为请求响应返回

func routes(_ app: Application) throws {
    app.get("users") { req -> EventLoopFuture<[User]> in
        req.postgres.connection(to: .myDb1) { conn in
            SwifQL.select(User.table.*).from(User.table).execute(on: conn).all(decoding: User.self)
        }
    }
}

事务 (Transactions)

您可以在事务中执行多个查询

app.postgres.transaction(to: .myDb1) { conn in
    /// `BEGIN` calls automatically

    /// do any amount of queries here

    /// once you finish if everything is ok then `COMMIT` calls automatically

    /// if error has occured then `ROLLBACK` calls automatically
}

我应该释放连接吗?

不。连接会自动释放。

便利方法 (Conveniences)

选择 (Select)

User.select.where(\User.$email == "hello@gmail.com").execute(on: conn).first(decoding: User.self)

插入 (Insert)

let user = User(email: "hello@gmail.com", name: "John", password: "qwerty".sha512, gender: .male)
user.insert(on: conn)

批量插入 (Batch insert)

let user1 = User(email: "hello@gmail.com", name: "John", password: "qwerty".sha512, gender: .male)
let user2 = User(email: "byebye@gmail.com", name: "Amily", password: "asdfgh".sha512, gender: .female)
let user3 = User(email: "trololo@gmail.com", name: "Trololo", password: "zxcvbn".sha512, gender: .other)
[user1, user2, user3].batchInsert(on: conn)

更新 (Update)

User.select.where(\User.$email == "hello@gmail.com").execute(on: conn).first(decoding: User.self).flatMap { user in
    guard let user = user else { return conn.eventLoop.makeFailedFuture(...) }
    user.password = "asdfg"
    return user.update(on: \.$id, on: conn) // executes update just for `password` column and returns EventLoopFuture<User>
}

删除 (Delete)

user.delete(on: \.$id, on: conn) // executes `DELETE FROM User WHERE id=...` returns EventLoopFuture<Void>

贡献 (Contributing)

请随时贡献

联系方式 (Contacts)

提交一个 issue,或者您始终可以在 Vapor 服务器的 Discord 中 #swifql 分支或直接作为 iMike#3049 找到我。