본문으로 건너뛰기

스키마#

Fluent의 스키마 API를 사용하면 프로그래밍 방식으로 데이터베이스 스키마를 생성하고 업데이트할 수 있습니다. 이는 모델을 사용할 수 있도록 데이터베이스를 준비하기 위해 마이그레이션과 함께 사용되는 경우가 많습니다.

// An example of Fluent's schema API
try await database.schema("planets")
    .id()
    .field("name", .string, .required)
    .field("star_id", .uuid, .required, .references("stars", "id"))
    .create()

SchemaBuilder를 생성하려면 database의 schema 메서드를 사용하세요. 영향을 주고자 하는 테이블 또는 컬렉션의 이름을 전달합니다. 모델의 스키마를 편집하는 경우, 이 이름이 모델의 schema와 일치하는지 확인하세요.

액션#

스키마 API는 스키마의 생성, 업데이트, 삭제를 지원합니다. 각 액션은 API에서 사용 가능한 메서드 중 일부를 지원합니다.

생성#

create()를 호출하면 데이터베이스에 새로운 테이블이나 컬렉션이 생성됩니다. 새로운 필드와 제약 조건을 정의하기 위한 모든 메서드가 지원됩니다. 업데이트나 삭제를 위한 메서드는 무시됩니다.

// An example schema creation.
try await database.schema("planets")
    .id()
    .field("name", .string, .required)
    .create()

선택한 이름의 테이블이나 컬렉션이 이미 존재하는 경우, 에러가 발생합니다. 이를 무시하려면 .ignoreExisting()을 사용하세요.

업데이트#

update()를 호출하면 데이터베이스에 있는 기존 테이블이나 컬렉션이 업데이트됩니다. 필드와 제약 조건을 생성, 업데이트, 삭제하기 위한 모든 메서드가 지원됩니다.

// An example schema update.
try await database.schema("planets")
    .unique(on: "name")
    .deleteField("star_id")
    .update()

삭제#

delete()를 호출하면 데이터베이스에서 기존 테이블이나 컬렉션이 삭제됩니다. 추가적인 메서드는 지원되지 않습니다.

// An example schema deletion.
database.schema("planets").delete()

필드#

스키마를 생성하거나 업데이트할 때 필드를 추가할 수 있습니다.

// Adds a new field
.field("name", .string, .required)

첫 번째 매개변수는 필드의 이름입니다. 이는 연관된 모델 프로퍼티에서 사용하는 키와 일치해야 합니다. 두 번째 매개변수는 필드의 데이터 타입입니다. 마지막으로, 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 이 필드의 값이 참조된 스키마에 있는 값과 일치해야 합니다. foreign key를 참고하세요.
.identifier 기본 키를 나타냅니다. identifier를 참고하세요.
.sql(SQLColumnConstraintAlgorithm) 지원되지 않는 제약 조건을 정의합니다 (예: default). SQLSQLColumnConstraintAlgorithm을 참고하세요.

Identifier#

모델이 표준 @ID 프로퍼티를 사용하는 경우, id() 헬퍼를 사용하여 해당 필드를 생성할 수 있습니다. 이는 특별한 .id 필드 키와 UUID 값 타입을 사용합니다.

// Adds field for default identifier.
.id()

커스텀 식별자 타입의 경우, 필드를 직접 지정해야 합니다.

// Adds field for custom identifier.
.field("id", .int, .identifier(auto: true))

identifier 제약 조건은 단일 필드에만 사용할 수 있으며 기본 키를 나타냅니다. auto 플래그는 데이터베이스가 이 값을 자동으로 생성할지 여부를 결정합니다.

필드 업데이트#

updateField를 사용하여 필드의 데이터 타입을 업데이트할 수 있습니다.

// Updates the field to `double` data type.
.updateField("age", .double)

고급 스키마 업데이트에 대한 더 자세한 내용은 advanced를 참고하세요.

필드 삭제#

deleteField를 사용하여 스키마에서 필드를 제거할 수 있습니다.

// Deletes the field "age".
.deleteField("age")

제약 조건#

스키마를 생성하거나 업데이트할 때 제약 조건을 추가할 수 있습니다. 필드 제약 조건과 달리, 최상위 수준의 제약 조건은 여러 필드에 영향을 줄 수 있습니다.

유니크#

유니크 제약 조건은 하나 이상의 필드에 중복된 값이 없어야 함을 요구합니다.

// Disallow duplicate email addresses.
.unique(on: "email")

여러 필드에 제약 조건이 설정된 경우, 각 필드 값의 특정 조합이 유일해야 합니다.

// Disallow users with the same full name.
.unique(on: "first_name", "last_name")

유니크 제약 조건을 삭제하려면 deleteUnique를 사용하세요.

// Removes duplicate email constraint.
.deleteUnique(on: "email")

제약 조건 이름#

Fluent는 기본적으로 유일한 제약 조건 이름을 생성합니다. 하지만 커스텀 제약 조건 이름을 전달하고 싶을 수도 있습니다. name 매개변수를 사용하여 이를 수행할 수 있습니다.

// Disallow duplicate email addresses.
.unique(on: "email", name: "no_duplicate_emails")

이름이 지정된 제약 조건을 삭제하려면, deleteConstraint(name:)을 사용해야 합니다.

// Removes duplicate email constraint.
.deleteConstraint(name: "no_duplicate_emails")

외래 키#

외래 키 제약 조건은 필드의 값이 참조된 필드에 있는 값 중 하나와 일치해야 함을 요구합니다. 이는 유효하지 않은 데이터가 저장되는 것을 방지하는 데 유용합니다. 외래 키 제약 조건은 필드 제약 조건 또는 최상위 수준의 제약 조건으로 추가할 수 있습니다.

필드에 외래 키 제약 조건을 추가하려면, .references를 사용하세요.

// Example of adding a field foreign key constraint.
.field("star_id", .uuid, .required, .references("stars", "id"))

위 제약 조건은 “star_id” 필드에 있는 모든 값이 Star의 “id” 필드에 있는 값 중 하나와 일치해야 함을 요구합니다.

이와 동일한 제약 조건을 foreignKey를 사용하여 최상위 수준의 제약 조건으로 추가할 수도 있습니다.

// Example of adding a top-level foreign key constraint.
.foreignKey("star_id", references: "stars", "id")

필드 제약 조건과 달리, 최상위 수준의 제약 조건은 스키마 업데이트에서 추가할 수 있습니다. 또한 이름을 지정할 수도 있습니다.

외래 키 제약 조건은 선택적으로 onDeleteonUpdate 액션을 지원합니다.

ForeignKeyAction 설명
.noAction 외래 키 위반을 방지합니다 (기본값).
.restrict .noAction과 동일합니다.
.cascade 외래 키를 통해 삭제를 전파합니다.
.setNull 참조가 끊어지면 필드를 null로 설정합니다.
.setDefault 참조가 끊어지면 필드를 기본값으로 설정합니다.

아래는 외래 키 액션을 사용하는 예제입니다.

// Example of adding a top-level foreign key constraint.
.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 타입은 이종(heterogeneous) 딕셔너리이므로, of 매개변수를 지정하지 않습니다.

만약 딕셔너리 값이 [String: Int]처럼 동종(homogeneous)이라면, of 매개변수로 값 타입을 지정합니다.

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

딕셔너리 키는 항상 문자열이어야 합니다.

Array#

array 데이터 타입은 중첩된 배열을 저장할 수 있습니다. 여기에는 Codable 값을 포함하는 Swift 배열과, unkeyed container를 사용하는 Codable 타입이 포함됩니다.

문자열 배열을 저장하는 다음 @Field를 살펴보겠습니다.

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

이 필드는 .array(of:) 데이터 타입을 사용하여 저장할 수 있습니다.

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

배열이 동종(homogeneous)이므로, of 매개변수를 지정합니다.

Codable을 준수하는 Swift Array는 항상 동종의 값 타입을 가집니다. 이종(heterogeneous) 값을 unkeyed container로 직렬화하는 커스텀 Codable 타입은 예외이며, 이 경우 .array 데이터 타입을 사용해야 합니다.

Enum#

enum 데이터 타입은 문자열 기반의 Swift 열거형(enum)을 네이티브로 저장할 수 있습니다. 네이티브 데이터베이스 enum은 데이터베이스에 추가적인 타입 안전성 계층을 제공하며, 원시 enum보다 더 나은 성능을 보일 수 있습니다.

네이티브 데이터베이스 enum을 정의하려면, Databaseenum 메서드를 사용하세요. enum의 각 case를 정의하려면 case를 사용하세요.

// An example of enum creation.
database.enum("planet_type")
    .case("smallRocky")
    .case("gasGiant")
    .case("dwarf")
    .create()

enum이 생성되고 나면, read() 메서드를 사용하여 스키마 필드에 사용할 데이터 타입을 생성할 수 있습니다.

// An example of reading an enum and using it to define a new field.
database.enum("planet_type").read().flatMap { planetType in
    database.schema("planets")
        .field("type", planetType, .required)
        .update()
}

// Or

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

enum을 업데이트하려면 update()를 호출하세요. 기존 enum에서 case를 삭제할 수 있습니다.

// An example of enum update.
database.enum("planet_type")
    .deleteCase("gasGiant")
    .update()

enum을 삭제하려면 delete()를 호출하세요.

// An example of enum deletion.
database.enum("planet_type").delete()

모델과의 결합#

스키마 빌딩은 의도적으로 모델과 분리되어 있습니다. 쿼리 빌딩과 달리, 스키마 빌딩은 key path를 사용하지 않으며 완전히 문자열 타입(stringly typed)으로 이루어집니다. 이는 스키마 정의, 특히 마이그레이션을 위해 작성된 스키마 정의가 더 이상 존재하지 않는 모델 프로퍼티를 참조해야 할 수도 있기 때문에 중요합니다.

이를 더 잘 이해하기 위해, 다음 마이그레이션 예제를 살펴보겠습니다.

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()

        // It is not currently possible to express this update without using custom SQL.
        // This also doesn't try to deal with splitting the name into first and last,
        // as that requires database-specific syntax.
        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도 계속 유효해야 합니다. 이는 key path로는 불가능한 일입니다.

모델 space 설정#

모델의 space를 정의하려면, 테이블을 생성할 때 schema(_:space:)에 space를 전달하세요. 예를 들면 다음과 같습니다.

try await db.schema("planets", space: "mirror_universe")
    .id()
    // ...
    .create()
이 페이지 편집