数据库迁移 封面
Vapor 大型教程

数据库迁移

随着功能迭代,数据表结构也得跟着变。本文用 Vapor 3 的 Fluent 迁移,给 User 加 Twitter 字段、给 Category 加唯一约束,并按环境创建管理员账号。

一、给用户加一个 Twitter 账号显示

先在 User 模型上增加一个可选的 twitterURL 字段,并同步更新公开结构体与迁移:

...
final class User: Codable {
    ...
    var twitterURL: String?

    init(name: String, username: String, password: String, twitterURL: String? = nil) {
        ...
        self.twitterURL = twitterURL
    }

    final class Public: Codable {
        ...
        var twitterURL: String?
        init(id: UUID?, name: String, username: String, twitterURL: String? = nil) {
            ...
            self.twitterURL = twitterURL
        }
    }
}

...

extension User {
    func convertToPublic() -> User.Public {
        return User.Public(id: self.id, name: self.name, username: self.username, twitterURL: self.twitterURL)
    }
}

...

extension User: Migration {
    static func prepare(on conn: PostgreSQLConnection) -> Future<Void> {
        return Database.create(self, on: conn, closure: { (builder) in
            builder.field(for: \.id, isIdentifier: true)
            builder.field(for: \.name)
            builder.field(for: \.username)
            builder.field(for: \.password)
            builder.unique(on: \.username)
        })
    }
}

但已上线的数据库不会自动加字段,需要新建一个迁移来改表。新建文件 18-12-26-AddTwitterToUser.swift

import FluentPostgreSQL
import Vapor

struct AddTwitterURLToUser: Migration {

    typealias Database = PostgreSQLDatabase

    static func prepare(on conn: PostgreSQLConnection) -> Future<Void> {
        return Database.update(User.self, on: conn) { builder in
            builder.field(for: \.twitterURL)
        }
    }

    static func revert(on conn: PostgreSQLConnection) -> Future<Void> {
        return Database.update(User.self, on: conn) { builder in
            builder.deleteField(for: \.twitterURL)
        }
    }
}

configure.swift 里注册这个迁移:

public func configure(_ config: inout Config, _ env: inout Environment, _ services: inout Services) throws {
    ...
    migrations.add(migration: AddTwitterURLToUser.self, database: .psql)
    services.register(migrations)
    ...
}

然后在 Web 端把 Twitter 字段展示出来。注册表单加上 Twitter 输入框:

...
    <form method="post">
        <div class="form-group">
            <label for="name">Name</label>
            <input type="text" name="name" class="form-control"
            id="name"/>
        </div>
        <div class="form-group">
            <label for="twitterURL">Twitter handle</lable>
            <input type="text" name="twitterURL" class="form-control" id="twitterURL" />
        </div>
...

用户详情页 user.leaf 显示它:

<h1>#(user.name)</h1>
<h2>#(user.username)
    #if(user.twitterURL) {
    - #(user.twitterURL)
    }
</h2>
...

注册处理逻辑读取这个字段(为空则存 nil):

...
struct RegisterData: Content {
    ...
    let twitterURL: String?
}
...
    func registerPostHandler(_ req: Request, data: RegisterData) throws -> Future<Response> {
        ...
        var twitterURL: String?
        if let twitter = data.twitterURL, !twitter.isEmpty {
            twitterURL = twitter
        }
        let user = User(name: data.name, username: data.username, password: password, twitterURL: twitterURL)
        ...
    }
...

二、让类别具有唯一性

类别名应该唯一,避免重复创建同名类别。新建迁移 18-12-26-MakeCategoriesUnique.swift,给 name 加唯一约束,并能在回滚时删除它:

import FluentPostgreSQL
import Vapor

struct MakeCategoriesUnique: Migration {

    typealias Database = PostgreSQLDatabase

    static func prepare(on conn: PostgreSQLConnection) -> Future<Void> {
        return Database.update(Category.self, on: conn) {
            builder in
            builder.unique(on: \.name)
        }
    }

    static func revert(on conn: PostgreSQLConnection) -> Future<Void> {
        return Database.update(Category.self, on: conn) { builder in
            builder.deleteUnique(from: \.name)
        }
    }
}

同样在 configure.swift 注册:

public func configure(_ config: inout Config, _ env: inout Environment, _ services: inout Services) throws {
    ...
    migrations.add(migration: MakeCategoriesUnique.self, database: .psql)
    services.register(migrations)
    ...
}

三、只在开发和测试环境上创建管理员账号

管理员账号不该出现在生产库里。借助 env 判断,只在开发/测试环境注册创建管理员的迁移:

public func configure(_ config: inout Config, _ env: inout Environment, _ services: inout Services) throws {
    ...
    switch env {
    case .development, .testing:
        migrations.add(migration: AdminUser.self, database: .psql)
    default:
        break
    }
    migrations.add(migration: AddTwitterURLToUser.self, database: .psql)
    migrations.add(migration: MakeCategoriesUnique.self, database: .psql)
    services.register(migrations)
    ...
}
迁移的本质:每个迁移都有 preparerevert 两个方法,revert 负责把改动撤销回去。给迁移文件加上日期前缀(如 18-12-26-)是个好习惯,方便排序和追溯。

本系列其他文章