# 模式 Fluent 的模式 API 允许你以编程方式创建和更新数据库模式。它通常与[迁移](migration.zh.md)一起使用,以准备数据库,供[模型](model.zh.md)使用。 ```swift // Fluent 模式 API 示例 try await database.schema("planets") .id() .field("name", .string, .required) .field("star_id", .uuid, .required, .references("stars", "id")) .create() ``` 要创建 `SchemaBuilder`,请使用数据库上的 `schema` 方法。传入要改变的表或集合的名称。如果你正在编辑模型的模式,请确保此名称与模型的 [`schema`](model.zh.md#schema) 相匹配。 ## 操作 模式 API 支持创建、更新和删除模式。每个操作都支持 API 可用方法的一个子集。 ### 创建 调用 `create()` 方法在数据库中创建一个新表或集合。支持定义新字段和约束的所有方法。忽略更新或删除的方法。 ```swift // 创建模式示例。 try await database.schema("planets") .id() .field("name", .string, .required) .create() ``` 如果具有所选名称的表或集合已存在,则会引发错误。要忽略这一点,请使用 `.ignoreExisting()` 方法。 ### 更新 调用 `update()` 方法更新数据库中的现有表或集合。支持创建、更新和删除字段和约束的所有方法。 ```swift // 更新模式示例。 try await database.schema("planets") .unique(on: "name") .deleteField("star_id") .update() ``` ### 删除 调用 `delete()` 方法从数据库中删除现有的表或集合。不支持其他方法。 ```swift // 删除模式示例。 database.schema("planets").delete() ``` ## 字段(Field) 创建或更新模式时可以添加字段。 ```swift // 增加一个新字段。 .field("name", .string, .required) ``` 第一个参数是字段的名称。这应该与关联模型属性上使用的键匹配。第二个参数是字段的[数据类型](#data-type)。最后,可以添加零个或多个[约束](#field-constraint)。 ### 数据类型(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)| |`.date`|`Date` (omitting time of day)| |`.float`|`Float`| |`.double`|`Double`| |`.data`|`Data`| |`.uuid`|`UUID`| |`.dictionary`|See [dictionary](#dictionary)| |`.array`|See [array](#array)| |`.enum`|See [enum](#enum)| ### 字段约束(Field Constraint) 下面列出了支持的字段约束。 |字段约束|描述| |-|-| |`.required`|不允许 `nil` 值。| |`.references`|要求此字段的值与引用的模式中的值匹配。参见[外键](#foreign-key)。| |`.identifier`|表示主键。参见[标识符](#identifier)| ### 标识符(Identifier) 如果你的模型使用标准的 `@ID` 属性,你可以使用 `id()` 方法来创建它的字段。使用特殊的 `.id` 字段键和 `UUID` 值类型。 ```swift // 添加字段默认标识符。 .id() ``` 对于自定义标识符类型,你需要手动指定该字段。 ```swift // 添加字段自定义标识符。 .field("id", .int, .identifier(auto: true)) ``` `identifier` 约束可用于单个字段并表示主键。`auto` 标志确定数据库是否应自动生成此值。 ### 更新字段 你可以使用 `updateField` 更新字段的数据类型。 ```swift // 更新字段的类型为 `double`。 .updateField("age", .double) ``` 参阅[进阶](advanced.zh.md#sql)部分了解高级模式的更多信息。 ### 删除字段 您可以使用 `deleteField` 方法从模式中删除字段。 ```swift // 删除 "age" 字段。 .deleteField("age") ``` ## 约束 可以在创建或更新模式时添加约束。与[字段约束](#field-constraint)不同,顶级约束可以影响多个字段。 ### 唯一(Unique) 唯一约束要求一个或多个字段中没有重复值。 ```swift // 不允许有重复的电子邮件地址。 .unique(on: "email") ``` 如果约束了多个字段,则每个字段的值的特定组合必须是唯一的。 ```swift // 不允许用户有相同的全名。 .unique(on: "first_name", "last_name") ``` 要删除唯一约束,使用 `deleteUnique` 方法。 ```swift // 删除重复的电子邮件约束。 .deleteUnique(on: "email") ``` ### 约束名(Constraint Name) 默认情况下,Fluent 将生成唯一的约束名称。但是,你可能希望传递自定义约束名称。你可以使用 `name` 参数来实现。 ```swift // 不允许重复的电子邮件地址。 .unique(on: "email", name: "no_duplicate_emails") ``` 要删除命名约束,必须使用 `deleteConstraint(name:)` 方法。 ```swift // 删除重复的电子邮件约束。 .deleteConstraint(name: "no_duplicate_emails") ``` ## 外键(Foreign Key) 外键约束要求字段的值与引用字段中的值匹配。这对于防止保存无效数据很有用。外键约束可以作为字段或顶级约束添加。 要将外键约束添加到字段,请使用 `.references` 方法。 ```swift // 字段添加外键约束示例。 .field("star_id", .uuid, .required, .references("stars", "id")) ``` 上述约束要求 ”star_id“ 字段中的所有值必须与 Star 的 “id” 字段中的一个值匹配。 可以使用 `foreignKey` 将相同的约束添加为顶级约束。 ```swift // 添加顶级外键约束示例。 .foreignKey("star_id", references: "stars", "id") ``` 与字段约束不同,可以在模式更新中添加顶级约束。它们也可以被[命名](#constraint-name)。 外键约束支持可选 `onDelete` 和 `onUpdate` 操作。 |外键操作|描述| |-|-| |`.noAction`|防止外键违规(默认)。| |`.restrict`|与 `.noAction` 相同。| |`.cascade`|通过外键传播删除。| |`.setNull`|如果引用被破坏,则将字段设置为空。| |`.setDefault`|如果引用被破坏,则将字段设置为默认值。| 下面是使用外键操作的示例。 ```swift // 添加顶级外键约束示例。 .foreignKey("star_id", references: "stars", "id", onDelete: .cascade) ``` !!! warning "警告" 外键操作仅发生在数据库中,绕过 Fluent。这意味着模型中间件和软删除之类的东西可能无法正常工作。 ## SQL `.sql` 参数允许你向 schema 中添加任意的 SQL。这对于添加特定的约束或数据类型非常有用。 一个常见的用例是为字段定义默认值: ```swift .field("active", .bool, .required, .sql(.default(true))) ``` 甚至可以为时间戳字段定义默认值: ```swift .field("created_at", .datetime, .required, .sql(.default(SQLFunction("now")))) ``` ## 字典(Dictionary) 字典数据类型能够存储嵌套的字典值。这包括遵循 `Codable` 协议的结构和具有 `Codable` 值的 Swift 字典。 !!! note "注意" Fluent 的 SQL 数据库驱动程序将嵌套字典存储在 JSON 列中。 采用以下 `Codable` 结构。 ```swift struct Pet: Codable { var name: String var age: Int } ``` 由于这个 `Pet` 遵循 `Codable` 协议,它可以存储在 `@Field` 中。 ```swift @Field(key: "pet") var pet: Pet ``` 此字段可以使用 `.dictionary(of:)` 数据类型存储。 ```swift .field("pet", .dictionary, .required) ``` 由于 `Codable` 类型是异构字典,所以我们不指定 `of` 参数。 如果字典值是同类的,例如 `[String: Int]`,则 `of` 参数将指定值类型。 ```swift .field("numbers", .dictionary(of: .int), .required) ``` 字典键必须始终是字符串。 ## 数组(Array) 数组数据类型能够存储嵌套数组。这包括包含 `Codable` 值的 Swift 数组和使用无键容器的 `Codable` 类型。 以下面的 `@Field` 为例,它存储字符串数组。 ```swift @Field(key: "tags") var tags: [String] ``` 该字段可以使用 `.array(of:)` 数据类型存储。 ```swift .field("tags", .array(of: .string), .required) ``` 由于数组是同质的,所以我们指定 `of` 参数。 可编码的 Swift `数组` 将始终具有同质的值类型。将异构值序列化为无键容器的自定义 `Codable` 类型是例外,应使用 `.array` 数据类型。 ## 枚举(Enum) 枚举数据类型能够以原生地方式存储字符串支持的 Swift 枚举。数据库枚举为数据库提供了额外的类型安全层,并且可能比原始枚举的性能更好。 要定义原生数据库枚举,请使用 `Database` 的 `enum` 方法。使用 `case` 定义枚举的每种情况。 ```swift // 创建枚举示例。 database.enum("planet_type") .case("smallRocky") .case("gasGiant") .case("dwarf") .create() ``` 创建枚举后,你可以使用 `read()` 方法为模式字段生成数据类型。 ```swift // 读取枚举并使用它定义新字段的示例。 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()` 方法。可以从现有枚举中删除案例。 ```swift // 更新枚举示例。 database.enum("planet_type") .deleteCase("gasGiant") .update() ``` 要删除枚举,请调用 `delete()` 方法。 ```swift // 删除枚举示例。 database.enum("planet_type").delete() ``` ## 模型耦合 有目的地将模式构建与模型分离。与查询构建不同,模式构建不使用键路径,并且完全是字符串类型。这一点很重要,因为模式定义,尤其是为迁移编写的模式定义,可能需要引用不再存在的模型属性。 为了更好地理解这一点,请查看以下示例迁移。 ```swift 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 模型进行以下更改。 ```diff - @Field(key: "name") - var name: String + @Field(key: "first_name") + var firstName: String + + @Field(key: "last_name") + var lastName: String ``` 我们可以通过以下迁移进行必要的数据库模式调整。 ```swift 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` 字段和新的 `firstName` 和 `lastName` 字段。此外,原来的 `UserMigration` 应该继续有效。这在键路径上是不可能做到的。 ## 设置模型空间 要定义[模型空间](model.zh.md#database-space),请在创建表时将空间传递给 `schema(_:space:)`。例如。 ```swift try await db.schema("planets", space: "mirror_universe") .id() // ... .create() ```