🧽

Swift Fluent 4 ORMのPostgreSQLでJSONB型を使用する方法

2025/02/07に公開

概要

RDBでJSON型を使用したいときは時々あると思います。(例えばEvent SourcingをRDBでサクッとしたいときなど)

そんなとき、Server-Side Swiftフレームワーク Vaporの公式ORMであるFluentでJSONの使い方に少し詰まったのでこの記事を記述しました。

結論

  • JSONとして格納したい型はCodableに準拠する
  • モデルのプロパティにその型をそのまま使う
  • Migration時に.jsonをカラム型として指定する
EventDAO.swift
import Fluent
import Foundation

final class EventDAO: Fluent.Model, @unchecked Sendable {
    static let schema = "events"

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

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

    @Field(key: "entity_name")
    var entityName: String

    @Field(key: "entity_id")
    var entityId: UUID

    @Field(key: "event")
    var event: Event // ← JSONB型に格納される。Event型はCodableに準拠

    @Field(key: "created_at")
    var createdAt: Date

    init() { }

    init(id: UUID? = nil, topic: String, entityName: String, entityId: UUID, event: Event, createdAt: Date) {
        self.id = id
        self.topic = topic
        self.entityName = entityName
        self.entityId = entityId
        self.event = event
        self.createdAt = createdAt
    }
}
EventDAOMigration.swift
import Fluent

struct EventDAOMigration: AsyncMigration {
    func prepare(on database: any Database) async throws {
        try await database.schema("events")
            .id()
            .field("topic", .string, .required)
            .field("entity_name", .string, .required)
            .field("entity_id", .uuid, .required)
            .field("event", .json, .required)
            .field("created_at", .datetime, .required)
            .create()
    }

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

解説

当初、@Fieldプロパティラッパーで定義するプロパティをData型やString型にして試してみましたが、ORM側でJSON型に変換されず、以下のPSQLErrorが発生しました。

Data型、あるいはString型をJSONカラムに入れようとした場合のエラー
PSQLError(code: server, serverInfo: [sqlState: 42804, file: parse_target.c, hint: You will need to rewrite or cast the expression., line: 586, message: column "event" is of type jsonb but expression is of type bytea, position: 113, routine: transformAssignedExpr, localizedSeverity: ERROR, severity: ERROR], triggeredFromRequestInFile: PostgresKit/PostgresDatabase+SQL.swift, line: 57, query: PostgresQuery(sql: INSERT INTO "events" ("id", "topic", "entity_name", "entity_id", "event", "created_at") VALUES ($1, $2, $3, $4, $5, $6) RETURNING "id", binds: [(****; UUID; format: binary), (****; TEXT; format: binary), (****; TEXT; format: binary), (****; UUID; format: binary), (****; BYTEA; format: binary), (****; TIMESTAMPTZ; format: binary)]))

Codableに準拠した型をそのままプロパティに用いたところ、JSONとして挿入することができました。FluentではJSONカラムにはCodableに準拠したオブジェクト型のプロパティにする必要があるようです。

また、PostgreSQLにはJSON型とJSONB型がありますが、FluentKitの定義でマイグレーション時の.jsonの型は.dictionaryと同じになるように設定されています

https://github.com/vapor/fluent-kit/blob/main/Sources/FluentKit/Schema/DatabaseSchema.swift#L51-L53

FluentPostgresDriverで.dictionaryjsonb型に指定されているので.json指定するとカラムの型としてはjsonbになります。

https://github.com/vapor/fluent-postgres-driver/blob/main/Sources/FluentPostgresDriver/PostgresConverterDelegate.swift#L20-L21

あとがき

あまりにも使い勝手が良いので、ついすべてをJSONで格納したくなる衝動に駆られます。特に、集約単位でのデータ管理やキャッシュ的な用途に、RDBでJSON型を使うのは非常に有効だと感じました。

nextbeat Tech Blog

Discussion