티스토리 뷰

Fluent

Fluent를 사용해 PostgreSQL 데이터베이스에 실제 데이터를 저장해보자

 

지난 글 보기 - PostgreSQL 개념, 설치, 사용

 

 

[Swift] Swift로 서버 만들기 Vapor 03 - PostgreSQL 개념, 설치, 사용

이번 글에서는 Vapor - Fluent에서 사용하는 PostgreSQL에 대해 알아보자 이전 글 보기 [Swift] Swift로 서버 만들기 Vapor 02 - Vapor 기본 프로젝트 뜯어보기 이번 포스트에서는 Vapor new 명령어로 만들어진 기

malchafrappuccino.tistory.com

 

 

Fluent 개념

Fluent는 Swift 객체-관계 매핑(Object–relational mapping, ORM) 프레임워크이다.

 

객체 - 관계 매핑은 Swift 객체와 관계형 데이터베이스를 연결(매핑)해 준 것이다 즉, 데이터베이스를 사용하기 쉽게 Swift로 추상화해준 것이다.

 

 

SQL 문법없이 Swift 문법을 사용해 데이터베이스에 CRUD를 할 수 있다. 강타입 언어인 Swift의 이점을 취한다.

 

 

Vapor 4.x 버전 기준 Fluent는 4가지 데이터베이스를 지원한다. -> PostgreSQL, SQLite, MySQL, MongoDB

 

공식 문서에서는 PostgreSQL를 추천한다. 추천하는 이유는 딱히 안 나와있어 상황에 맞게 선택해서 사용하면 될 것 같다. 

 

 

 

설치

설치는 2단계로 나뉜다.

 

Vapor 프로젝트 생성 시 같이 설치

기존 vapor 프로젝트 설치시 같이 설치할 수 있다.

 

Vapor 설치 & Fluent 설치하기

 

 

[Swift] Swift로 서버 만들기 Vapor 01 - 개념 및 설치

Swift는 iOS 앱을 만들기 위한 언어이다. 보통 혼자서 토이 프로젝트를 하면서 앱을 만들면 3가지 선택지가 있다. 1. 서버가 필요없는 앱을 만든다. 2. 파이어베이스와 같은 애플리케이션 서비스를

malchafrappuccino.tistory.com

 

 

기존 Vapor 프로젝트에 설치

기존 Vapor 프로젝트가 있다면 Package.Swift 파일에 dependency와 target을 추가해준다.

 

    dependencies: [
        .package(url: "https://github.com/vapor/vapor.git", from: "4.76.0"),
        .package(url: "https://github.com/vapor/fluent.git", from: "4.8.0"),
        .package(url: "https://github.com/vapor/fluent-postgres-driver.git", from: "2.7.2"),
    ],
    targets: [
        .executableTarget(
            name: "App",
            dependencies: [
                .product(name: "Fluent", package: "fluent"),
                .product(name: "FluentPostgresDriver", package: "fluent-postgres-driver"),
                .product(name: "Vapor", package: "vapor")
            ],
    // ...

PostgreSQL도 따로 추가해주어야 한다.

 

만약 다른 SQLite를 사용한다면

 

// depedencies
.package(url: "https://github.com/vapor/fluent-sqlite-driver.git", from: "4.0.0")

// targets - depedencies
.product(name: "FluentSQLiteDriver", package: "fluent-sqlite-driver")

 

이렇게 된다.

 

형식이 바뀔 수 있으니 공식문서를 참고하자 - Fluent 공식 문서

 

 

Models

모델은 테이블, 컬렉션과 같이 데이터베이스에서 고정된 데이터 구조를 나타낸다.

 

  • 모델에는 Codable을 준수하는 Field가 하나 이상 있다.
  • 모델에는 고유한 ID가 있다.
  • 모델은 Model 프로토콜을 준수한다.

 

Vapor 기본 프로젝트 생성시 생성되는 Todo 모델을 확인해보자

 

final class Todo: Model, Content {
    static let schema = "todos"
    
    @ID(key: .id)
    var id: UUID?

    @Field(key: "title")
    var title: String

    init() { }

    init(id: UUID? = nil, title: String) {
        self.id = id
        self.title = title
    }
}

 

모델은 Model 프로토콜을 채택해야 한다.

 

Model 프로토콜을 준수하려면

  1. static 변수인 schema를 선언하고
  2. id 프로퍼티를 선언해야한다.

 

Content 프로토콜

기본 생성된 Todo 모델을 보면 Content 프로토콜을 채택하고 있다.

 

Content 프로토콜은 모델이 HTTP 메시지에서 쉽게 인코딩/디코딩할 수 있도록 한다. HTTP용 Codable이라 생각하면 될 것 같다.

 

기본적으로 JSON 인코딩이 사용되며 URL 인코딩 형식과 멀티파트 형식도 지원한다. API는 또한 구성 가능하여 특정 HTTP 콘텐츠 유형에 대한 인코딩 전략을 추가, 수정 또는 교체할 수 있다.

 

 

Scehma

schema는 모델이 매핑되는 테이블을 의미한다. Todo모델은 데이터베이스 - todos 테이블에 매핑된다.

 

schema는 데이터베이스에 기존에 있는 테이블 이거나 migration을 통해 생성해주어야 한다. 

 

스키마 이름은 보통 모델의 소문자, 복수형으로 설정한다. -> Todo의 스키마는 todos가 된다

 

 

Identifier

id는 반드시 @ID 프로퍼티 래퍼를 사용해야 한다.

@ID(key: .id)
var id: UUID?

 

Fluent는 UUID 타입을 사용해 id를 저장한다. 만약 custom 타입을 사용하고 싶다면 @ID(custom:) 을 작성해 오버라이딩한다.

 

Fluent는 모델이 생성될 때 UUID 식별자를 자동으로 생성한다.

 

exists 프로퍼티를 통해 해당 모델이 데이터베이스에 존재하는지 확인할 수 있다.

초기화 할 때는 false 값을 가진다.

모델을 저장하거나 데이터베이스에서 가져올 때 true 값을 가진다.

if planet.$id.exists {
    // 데이터베이스에 모델이 있을 경우 실행됨
}

 

 

Fields

필드는 테이블에 저장할 속성 값을 나타낸다. @Field 프로퍼티 래퍼를 사용한다.

 

key 파라미터는 실제 데이터 베이스에 저장되는 필드의 이름이다.

 

필드 컨벤션

데이터베이스에는 snake_case, 프로퍼티 이름에는 camelCase를 사용한다.

 

Field 값은 Codable을 채택하는 모든 타입이 가능하다. 중첩 구조체, 배열도 지원

 

하지만 필터링 명령어는 제한된다. (filter)

 

옵셔널 값을 가지는 필드는 @OptionalField를 사용한다.

@OptionalField(key: "tag")
var tag: String?

 

 

Initializer

모든 모델은 빈 이니셜라이져를 가진다. Fluent가 새로운 모델을 생성하기 위해 필요하다

init() { }

 

편의 이니셜라이져를 추가할 수 있다.

    init(id: UUID? = nil, title: String) {
        self.id = id
        self.title = title
    }

편의 이니셜라이져는 필수는 아니다. 하지만 모델을 더 쉽게 생성할 수 있다.

 

 

 

Timestamp

@Timestamp는 @Field의 특별한 타입 중 하나로 Foundation.Date 타입을 저장한다.

 

Fluent에 의해 자동으로 설정된다.

final class Planet: Model {
    // 모델 생성시
    @Timestamp(key: "created_at", on: .create)
    var createdAt: Date?

    // 모델 업데이트 시
    @Timestamp(key: "updated_at", on: .update)
    var updatedAt: Date?
}

.create, .update, .delete 3가지 트리거가 있다.

 

-> .delete하면 삭제 되는거 아닌가??

기본적으로 삭제를 할 경우 soft delete가 된다. 데이터베이스에는 남아있지만 쿼리를 날리면 응답에는 오지 않는다.

 

만약 데이터베이스에서 아예 삭제하고 싶다면 force 파라미터를 사용해 삭제해주어야 한다.

model.delete(force: true, on: database)

 

soft delete의 경우 resotre 메소드를 사용해 복구가 가능하다.

model.restore(on: database)

 

Timestamp Format에는 .default, .iso8601, .unix 3가지가 있다.

포맷 설명 타입
.default 사용하는 데이터베이스에 최적화되는 타입 Date
.iso8601 ISO 8601 문자열 String
.unix 유닉스 시간 (분수 포함) Double

 

실제 코드에서는 아래와 같이 사용한다.

@Timestamp(key: "updated_at", on: .update, format: .iso8601)
var updatedAt: Date?

 

 

Todo 모델 수정하기

기존 Todo 모델에 Field를 추가해보자

final class Todo: Model, Content {
    static let schema = "todos"
    
    @ID(key: .id)
    var id: UUID?

    @Field(key: "title")
    var title: String
    
    @Field(key: "description")
    var description: String
    
    @Timestamp(key: "created_at", on: .create)
    var createdAt: Date?
    
    @Timestamp(key: "updated_at", on: .update)
    var updatedAt: Date?

    init() { }

    init(id: UUID? = nil, title: String, description: String, createdAt: Date?, updatedAt: Date?) {
        self.id = id
        self.title = title
        self.description = description
        self.createdAt = createdAt
        self.updatedAt = updatedAt
    }
}

오늘 할 일에 내용(=description)과 생성일, 수정일을 추가해주었다.

 

Field 프로퍼티 래퍼의 키는 snake_case를 준수하고, Swift의 프로퍼티는 camelCase를 준수하도록 작성해주었다.

 

 

Migration (마이그레이션)

Fluent Model을 작성한다고 해서 자동으로 Fluent -> Database로 동기화가 되지 않는다.

 

마이그레이션을 해줘서 데이터베이스를 업데이트해줘야 한다.

 

Vapor 기본 프로젝트 생성시 생성되는 CreatTodo.swift 파일을 살펴보자

struct CreateTodo: AsyncMigration {
    func prepare(on database: Database) async throws {
        try await database.schema("todos")
            .id()
            .field("title", .string, .required)
            .create()
    }

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

파일에는 AsyncMigration을 준수하는 CreateTodo 구조체가 있다. async라는 키워드 답게 비동기로 작동한다. 

 

prepare : Fluent -> 데이터베이스로 업데이트

revert: prepare로 업데이트된 내용 제거

 

마이그레이션을 하는 방법은 2가지가 있다.

 

 

Shell

shell에서 프로젝트 파일을 치고 명령어를 입력한다.

swift run App migrate

추가 키워드 없이 작성하면 prepare가 실행된다.

 

revert를 하고 싶다면 --revert 키워드를 작성해준다.

swift run App migrate --revert

 

마이그레이션을 하고 프로젝트를 바로 실행하고 싶다면 --auto-migrate 키워드를 사용한다.

swift run App serve --auto-migrate

 

코드

엔트리 포인트에 자동 마이그레이션 코드를 작성해준다. 

@main
enum Entrypoint {
    static func main() async throws {
        //... 
        try await app.autoMigrate() // 자동 마이그레이션
        try await configure(app)
        try await app.runFromAsyncMainEntrypoint()
    }
}

 

자동 마이그레이션을 하면 편하지만 마이그레이션이 필요 없는 경우에도 실행되기 때문에 초기 시작 속도가 느려진다. 무조건 좋다고 할 수 없으니까 상황에 맞게 선택하자.

 

 

 

정리

ORM 프레임워크 Fluent의 개념에 대해 알아봤다.

 

데이터베이스 쿼리를 몰라도 Swift를 사용해 손쉽게 모델을 생성하고 관리할 수 있다.

 

다음 포스트에서 Fluent와 PostgreSQL과 연결해서 데이터 CRUD를 해보자

 

 

끄읕

 

 

출처

 

Vapor: Fluent → Overview

Fluent Fluent is an ORM framework for Swift. It takes advantage of Swift's strong type system to provide an easy-to-use interface for your database. Using Fluent centers around the creation of model types which represent data structures in your database. T

docs.vapor.codes

 

 

Vapor: Fluent → Model

Models Models represent data stored in tables or collections in your database. Models have one or more fields that store codable values. All models have a unique identifier. Property wrappers are used to denote identifiers, fields, and relations. Below is

docs.vapor.codes

 

 

Vapor: Fluent → Migrations

Migrations Migrations are like a version control system for your database. Each migration defines a change to the database and how to undo it. By modifying your database through migrations, you create a consistent, testable, and shareable way to evolve you

docs.vapor.codes

 

 

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2025/06   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30
글 보관함