コンテンツにスキップ

スキーマ

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("name", .string, .required)

最初のパラメータはフィールドの名前です。これは、関連するモデルプロパティで使用されるキーと一致する必要があります。2番目のパラメータはフィールドのデータ型です。最後に、0個以上の制約を追加できます。

データ型

サポートされているフィールドのデータ型は以下のとおりです。

DataType Swift Type
.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 (推奨)
.date Date (時刻を省略)
.float Float
.double Double
.data Data
.uuid UUID
.dictionary dictionaryを参照
.array arrayを参照
.enum enumを参照

フィールド制約

サポートされているフィールド制約は以下のとおりです。

FieldConstraint 説明
.required nil値を許可しません。
.references このフィールドの値が参照されているスキーマの値と一致することを要求します。外部キーを参照。
.identifier 主キーを示します。識別子を参照。
.sql(SQLColumnConstraintAlgorithm) サポートされていない制約(例:default)を定義します。SQLSQLColumnConstraintAlgorithmを参照。

識別子

モデルが標準の@IDプロパティを使用している場合、id()ヘルパーを使用してそのフィールドを作成できます。これは特別な.idフィールドキーとUUID値型を使用します。

// デフォルト識別子のフィールドを追加
.id()

カスタム識別子型の場合、フィールドを手動で指定する必要があります。

// カスタム識別子のフィールドを追加
.field("id", .int, .identifier(auto: true))

identifier制約は単一のフィールドで使用でき、主キーを示します。autoフラグは、データベースがこの値を自動的に生成するかどうかを決定します。

フィールドの更新

updateFieldを使用してフィールドのデータ型を更新できます。

// フィールドを`double`データ型に更新
.updateField("age", .double)

高度なスキーマ更新の詳細については、advancedを参照してください。

フィールドの削除

deleteFieldを使用してスキーマからフィールドを削除できます。

// "age"フィールドを削除
.deleteField("age")

制約

スキーマの作成または更新時に制約を追加できます。フィールド制約とは異なり、トップレベルの制約は複数のフィールドに影響を与えることができます。

ユニーク

ユニーク制約は、1つ以上のフィールドに重複する値がないことを要求します。

// 重複するメールアドレスを許可しない
.unique(on: "email")

複数のフィールドが制約されている場合、各フィールドの値の特定の組み合わせがユニークである必要があります。

// 同じフルネームのユーザーを許可しない
.unique(on: "first_name", "last_name")

ユニーク制約を削除するには、deleteUniqueを使用します。

// 重複メール制約を削除
.deleteUnique(on: "email")

制約名

Fluentはデフォルトでユニーク制約名を生成します。ただし、カスタム制約名を渡したい場合があります。これはnameパラメータを使用して行うことができます。

// 重複するメールアドレスを許可しない
.unique(on: "email", name: "no_duplicate_emails")

名前付き制約を削除するには、deleteConstraint(name:)を使用する必要があります。

// 重複メール制約を削除
.deleteConstraint(name: "no_duplicate_emails")

外部キー

外部キー制約は、フィールドの値が参照されているフィールドの値のいずれかと一致することを要求します。これは、無効なデータが保存されるのを防ぐのに役立ちます。外部キー制約は、フィールドまたはトップレベルの制約として追加できます。

フィールドに外部キー制約を追加するには、.referencesを使用します。

// フィールド外部キー制約を追加する例
.field("star_id", .uuid, .required, .references("stars", "id"))

上記の制約は、"star_id"フィールドのすべての値がStarの"id"フィールドの値のいずれかと一致する必要があることを要求します。

この同じ制約は、foreignKeyを使用してトップレベルの制約として追加できます。

// トップレベルの外部キー制約を追加する例
.foreignKey("star_id", references: "stars", "id")

フィールド制約とは異なり、トップレベルの制約はスキーマ更新で追加できます。また、名前を付けることもできます。

外部キー制約は、オプションのonDeleteonUpdateアクションをサポートしています。

ForeignKeyAction 説明
.noAction 外部キー違反を防ぎます(デフォルト)。
.restrict .noActionと同じ。
.cascade 外部キーを通じて削除を伝播します。
.setNull 参照が切れた場合、フィールドをnullに設定します。
.setDefault 参照が切れた場合、フィールドをデフォルトに設定します。

以下は外部キーアクションを使用した例です。

// トップレベルの外部キー制約を追加する例
.foreignKey("star_id", references: "stars", "id", onDelete: .cascade)

Warning

外部キーアクションはデータベース内でのみ発生し、Fluentをバイパスします。 これは、モデルミドルウェアや論理削除などが正しく動作しない可能性があることを意味します。

SQL

.sqlパラメータを使用すると、スキーマに任意のSQLを追加できます。これは、特定の制約やデータ型を追加するのに役立ちます。 一般的な使用例は、フィールドのデフォルト値を定義することです:

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

またはタイムスタンプのデフォルト値:

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

Dictionary

dictionary データ型は、ネストされた辞書値を格納できます。これには、Codableに準拠する構造体と、Codable値を持つSwift辞書が含まれます。

Note

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

array データ型は、ネストされた配列を格納できます。これには、Codable値を含むSwift配列と、キーなしコンテナを使用するCodable型が含まれます。

文字列の配列を格納する次の@Fieldを考えてみましょう。

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

このフィールドは.array(of:)データ型を使用して格納できます。

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

配列は同種であるため、ofパラメータを指定します。

Codable SwiftのArrayは常に同種の値型を持ちます。異種の値をキーなしコンテナにシリアライズするカスタムCodable型は例外であり、.arrayデータ型を使用する必要があります。

Enum

enum データ型は、文字列ベースのSwift enumをネイティブに格納できます。ネイティブデータベースenumは、データベースに型安全性の追加レイヤーを提供し、生のenumよりもパフォーマンスが高い場合があります。

ネイティブデータベースenumを定義するには、Databaseenumメソッドを使用します。caseを使用してenumの各ケースを定義します。

// enum作成の例
database.enum("planet_type")
    .case("smallRocky")
    .case("gasGiant")
    .case("dwarf")
    .create()

enumが作成されたら、read()メソッドを使用してスキーマフィールドのデータ型を生成できます。

// enumを読み取り、新しいフィールドを定義するために使用する例
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()

enumを更新するには、update()を呼び出します。既存のenumからケースを削除できます。

// enum更新の例
database.enum("planet_type")
    .deleteCase("gasGiant")
    .update()

enumを削除するには、delete()を呼び出します。

// enum削除の例
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")
            .field("first_name", .string, .required)
            .field("last_name", .string, .required)
            .update()

        // 現在、カスタムSQLを使用せずにこの更新を表現することはできません。
        // また、名前を姓と名に分割する処理は行いません。
        // これにはデータベース固有の構文が必要だからです。
        try await User.query(on: database)
            .set(["first_name": .sql(embed: "name")])
            .run()

        try await database.schema("users")
            .deleteField("name")
            .update()
    }

    func revert(on database: Database) async throws {
        try await database.schema("users")
            .field("name", .string, .required)
            .update()
        try await User.query(on: database)
            .set(["name": .sql(embed: "concat(first_name, ' ', last_name)")])
            .run()
        try await database.schema("users")
            .deleteField("first_name")
            .deleteField("last_name")
            .update()
    }
}

このマイグレーションが機能するためには、削除されたnameフィールドと新しいfirstNameおよびlastNameフィールドの両方を同時に参照できる必要があることに注意してください。さらに、元のUserMigrationは引き続き有効である必要があります。これはキーパスでは不可能でした。

モデルスペースの設定

モデルのスペースを定義するには、テーブルを作成するときにschema(_:space:)にスペースを渡します。例:

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