跳转至

模式

Fluent 的模式 API 允许你以编程方式创建和更新数据库模式。它通常与迁移一起使用,以准备数据库,供模型使用。

// Fluent 模式 API 示例
try await database.schema("planets")
    .id()
    .field("name", .string, .required)
    .field("star_id", .uuid, .required, .references("stars", "id"))
    .create()

要创建 SchemaBuilder,请使用数据库上的 schema 方法。传入要改变的表或集合的名称。如果你正在编辑模型的模式,请确保此名称与模型的 schema 相匹配。

操作

模式 API 支持创建、更新和删除模式。每个操作都支持 API 可用方法的一个子集。

创建

调用 create() 方法在数据库中创建一个新表或集合。支持定义新字段和约束的所有方法。忽略更新或删除的方法。

// 创建模式示例。
try await database.schema("planets")
    .id()
    .field("name", .string, .required)
    .create()

如果具有所选名称的表或集合已存在,则会引发错误。要忽略这一点,请使用 .ignoreExisting() 方法。

更新

调用 update() 方法更新数据库中的现有表或集合。支持创建、更新和删除字段和约束的所有方法。

// 更新模式示例。
try await database.schema("planets")
    .unique(on: "name")
    .deleteField("star_id")
    .update()

删除

调用 delete() 方法从数据库中删除现有的表或集合。不支持其他方法。

// 删除模式示例。
database.schema("planets").delete()

字段(Field)

创建或更新模式时可以添加字段。

// 增加一个新字段。
.field("name", .string, .required)

第一个参数是字段的名称。这应该与关联模型属性上使用的键匹配。第二个参数是字段的数据类型。最后,可以添加零个或多个约束

数据类型(Data Type)

下面列出了支持的字段数据类型。

数据类型 Swift 类型
.string String
.int{8,16,32,64} Int{8,16,32,64}
.uint{8,16,32,64} UInt{8,16,32,64}
.bool Bool
.datetime Date (recommended)
.time Date (omitting day, month, and year)
.date Date (omitting time of day)
.float Float
.double Double
.data Data
.uuid UUID
.dictionary See dictionary
.array See array
.enum See enum

字段约束(Field Constraint)

下面列出了支持的字段约束。

字段约束 描述
.required 不允许 nil 值。
.references 要求此字段的值与引用的模式中的值匹配。参见外键
.identifier 表示主键。参见标识符

标识符(Identifier)

如果你的模型使用标准的 @ID 属性,你可以使用 id() 方法来创建它的字段。使用特殊的 .id 字段键和 UUID 值类型。

// 添加字段默认标识符。
.id()

对于自定义标识符类型,你需要手动指定该字段。

// 添加字段自定义标识符。
.field("id", .int, .identifier(auto: true))

identifier 约束可用于单个字段并表示主键。auto 标志确定数据库是否应自动生成此值。

更新字段

你可以使用 updateField 更新字段的数据类型。

// 更新字段的类型为 `double`。
.updateField("age", .double)

参阅进阶部分了解高级模式的更多信息。

删除字段

您可以使用 deleteField 方法从模式中删除字段。

// 删除 "age" 字段。
.deleteField("age")

约束

可以在创建或更新模式时添加约束。与字段约束不同,顶级约束可以影响多个字段。

唯一(Unique)

唯一约束要求一个或多个字段中没有重复值。

// 不允许有重复的电子邮件地址。
.unique(on: "email")

如果约束了多个字段,则每个字段的值的特定组合必须是唯一的。

// 不允许用户有相同的全名。
.unique(on: "first_name", "last_name")

要删除唯一约束,使用 deleteUnique 方法。

// 删除重复的电子邮件约束。
.deleteUnique(on: "email")

约束名(Constraint Name)

默认情况下,Fluent 将生成唯一的约束名称。但是,你可能希望传递自定义约束名称。你可以使用 name 参数来实现。

// 不允许重复的电子邮件地址。
.unique(on: "email", name: "no_duplicate_emails")

要删除命名约束,必须使用 deleteConstraint(name:) 方法。

// 删除重复的电子邮件约束。
.deleteConstraint(name: "no_duplicate_emails")

外键(Foreign Key)

外键约束要求字段的值与引用字段中的值匹配。这对于防止保存无效数据很有用。外键约束可以作为字段或顶级约束添加。

要将外键约束添加到字段,请使用 .references 方法。

// 字段添加外键约束示例。
.field("star_id", .uuid, .required, .references("stars", "id"))

上述约束要求 ”star_id“ 字段中的所有值必须与 Star 的 “id” 字段中的一个值匹配。

可以使用 foreignKey 将相同的约束添加为顶级约束。

// 添加顶级外键约束示例。
.foreignKey("star_id", references: "stars", "id")

与字段约束不同,可以在模式更新中添加顶级约束。它们也可以被命名

外键约束支持可选 onDeleteonUpdate 操作。

外键操作 描述
.noAction 防止外键违规(默认)。
.restrict .noAction 相同。
.cascade 通过外键传播删除。
.setNull 如果引用被破坏,则将字段设置为空。
.setDefault 如果引用被破坏,则将字段设置为默认值。

下面是使用外键操作的示例。

// 添加顶级外键约束示例。
.foreignKey("star_id", references: "stars", "id", onDelete: .cascade)

警告

外键操作仅发生在数据库中,绕过 Fluent。这意味着模型中间件和软删除之类的东西可能无法正常工作。

SQL

.sql 参数允许你向 schema 中添加任意的 SQL。这对于添加特定的约束或数据类型非常有用。 一个常见的用例是为字段定义默认值:

.field("active", .bool, .required, .sql(.default(true)))

甚至可以为时间戳字段定义默认值:

.field("created_at", .datetime, .required, .sql(.default(SQLFunction("now"))))

字典(Dictionary)

字典数据类型能够存储嵌套的字典值。这包括遵循 Codable 协议的结构和具有 Codable 值的 Swift 字典。

注意

Fluent 的 SQL 数据库驱动程序将嵌套字典存储在 JSON 列中。

采用以下 Codable 结构。

struct Pet: Codable {
    var name: String
    var age: Int
}

由于这个 Pet 遵循 Codable 协议,它可以存储在 @Field 中。

@Field(key: "pet")
var pet: Pet

此字段可以使用 .dictionary(of:) 数据类型存储。

.field("pet", .dictionary, .required)

由于 Codable 类型是异构字典,所以我们不指定 of 参数。

如果字典值是同类的,例如 [String: Int],则 of 参数将指定值类型。

.field("numbers", .dictionary(of: .int), .required)

字典键必须始终是字符串。

数组(Array)

数组数据类型能够存储嵌套数组。这包括包含 Codable 值的 Swift 数组和使用无键容器的 Codable 类型。

以下面的 @Field 为例,它存储字符串数组。

@Field(key: "tags")
var tags: [String]

该字段可以使用 .array(of:) 数据类型存储。

.field("tags", .array(of: .string), .required)

由于数组是同质的,所以我们指定 of 参数。

可编码的 Swift 数组 将始终具有同质的值类型。将异构值序列化为无键容器的自定义 Codable 类型是例外,应使用 .array 数据类型。

枚举(Enum)

枚举数据类型能够以原生地方式存储字符串支持的 Swift 枚举。数据库枚举为数据库提供了额外的类型安全层,并且可能比原始枚举的性能更好。

要定义原生数据库枚举,请使用 Databaseenum 方法。使用 case 定义枚举的每种情况。

// 创建枚举示例。
database.enum("planet_type")
    .case("smallRocky")
    .case("gasGiant")
    .case("dwarf")
    .create()

创建枚举后,你可以使用 read() 方法为模式字段生成数据类型。

// 读取枚举并使用它定义新字段的示例。
database.enum("planet_type").read().flatMap { planetType in
    database.schema("planets")
        .field("type", planetType, .required)
        .update()
}

// 或者

let planetType = try await database.enum("planet_type").read()
try await database.schema("planets")
    .field("type", planetType, .required)
    .update()

要更新枚举,请调用 update() 方法。可以从现有枚举中删除案例。

// 更新枚举示例。
database.enum("planet_type")
    .deleteCase("gasGiant")
    .update()

要删除枚举,请调用 delete() 方法。

// 删除枚举示例。
database.enum("planet_type").delete()

模型耦合

有目的地将模式构建与模型分离。与查询构建不同,模式构建不使用键路径,并且完全是字符串类型。这一点很重要,因为模式定义,尤其是为迁移编写的模式定义,可能需要引用不再存在的模型属性。

为了更好地理解这一点,请查看以下示例迁移。

struct UserMigration: AsyncMigration {
    func prepare(on database: Database) async throws {
        try await database.schema("users")
            .field("id", .uuid, .identifier(auto: false))
            .field("name", .string, .required)
            .create()
    }

    func revert(on database: Database) async throws {
        try await database.schema("users").delete()
    }
}

让我们假设这个迁移已经被推送到生产环境中。现在假设我们需要对 User 模型进行以下更改。

- @Field(key: "name")
- var name: String
+ @Field(key: "first_name")
+ var firstName: String
+
+ @Field(key: "last_name")
+ var lastName: String

我们可以通过以下迁移进行必要的数据库模式调整。

struct UserNameMigration: AsyncMigration {
    func prepare(on database: Database) async throws {
        try await database.schema("users")
            .deleteField("name")
            .field("first_name", .string)
            .field("last_name", .string)
            .update()
    }

    func revert(on database: Database) async throws {
        try await database.schema("users").delete()
    }
}

请注意,要使此迁移起作用,我们需要能够同时引用已删除的 name 字段和新的 firstNamelastName 字段。此外,原来的 UserMigration 应该继续有效。这在键路径上是不可能做到的。

设置模型空间

要定义模型空间,请在创建表时将空间传递给 schema(_:space:)。例如。

try await db.schema("planets", space: "mirror_universe")
    .id()
    // ...
    .create()