Chapter 03

Fluent ORM

用 Swift 类型系统操作数据库——Model 定义、Migration 版本管理、QueryBuilder 组合查询,让数据库交互既安全又优雅。

ORM 与原生 SQL 的取舍

ORM(Object-Relational Mapping,对象关系映射)是一种让开发者用面向对象的方式操作关系型数据库的技术。你定义 Swift 结构体/类来表示数据库表,ORM 框架负责将方法调用转换为 SQL 语句并执行。

Fluent 是 Vapor 的官方 ORM,支持 PostgreSQL、MySQL、SQLite 多种数据库。它的核心价值在于:类型安全(字段名拼写错误在编译期报错)、跨数据库抽象(切换数据库无需改业务代码)、Migration 版本管理(数据库结构变更有历史记录)。

维度Fluent ORM原生 SQL
类型安全编译期检查字段运行时才报错
可读性Swift 风格,直观字符串拼接,易出错
性能轻微抽象损耗最优,直接执行
复杂查询某些场景较繁琐灵活,无限制
数据库迁移Migration 体系管理手动维护 SQL 文件
跨数据库切换驱动即可需改 SQL 方言

Fluent 也支持通过 db.raw() 执行原生 SQL,两种方式可以混用——让 ORM 处理常规 CRUD,让原生 SQL 处理复杂报表查询。

核心概念词典

Model
Fluent 的核心协议,实现它的类型代表数据库中的一张表。每个字段用属性包装器(@ID@Field@Parent等)标注,Fluent 据此生成 SQL 列映射。Model 必须是引用类型(class)。
Migration
数据库迁移脚本,描述数据库结构的创建或变更。实现 prepare(on:) 方法(创建/修改表)和 revert(on:) 方法(回滚)。Fluent 追踪已执行的 Migration,保证每条只执行一次,是数据库版本管理的基础。
QueryBuilder
链式查询构建器,通过 Model.query(on: db) 创建,支持 .filter().sort().range().join().with()(预加载关联)等方法组合,最终调用 .all().first().count() 等执行查询。
@Field / @ID
属性包装器,@ID 标注主键(自动生成 UUID 或自增 Int),@Field(key:) 标注普通列并指定数据库列名。列名用 FieldKey 枚举而非字符串字面量,避免拼写错误。
@Parent / @Children / @Siblings
关系属性包装器:@Parent 表示"属于"(外键),@Children 表示"拥有多个"(一对多的"多"端),@Siblings 表示多对多关系(通过中间表)。这三者是 Fluent 关联查询的基础。

定义 Model 与 Migration

以博客系统为例,定义 UserPost 两个模型,演示一对多关系。规范做法是把字段的 FieldKey 定义为枚举扩展,避免字符串散落在代码各处。

// Sources/App/Models/User.swift
import Fluent
import Vapor

final class User: Model, Content {
    static let schema = "users"  // 数据库表名

    @ID(format: .uuid)
    var id: UUID?

    @Field(key: FieldKeys.name)
    var name: String

    @Field(key: FieldKeys.email)
    var email: String

    @Field(key: FieldKeys.passwordHash)
    var passwordHash: String

    @Timestamp(key: FieldKeys.createdAt, on: .create)
    var createdAt: Date?

    // 一对多:一个 User 拥有多个 Post
    @Children(for: \.author)
    var posts: [Post]

    init() {}  // Fluent 要求无参数初始化器

    init(id: UUID? = nil, name: String, email: String, passwordHash: String) {
        self.id           = id
        self.name         = name
        self.email        = email
        self.passwordHash = passwordHash
    }
}

// 字段名枚举(避免字符串散落各处)
extension User {
    enum FieldKeys {
        static let name:         FieldKey = "name"
        static let email:        FieldKey = "email"
        static let passwordHash: FieldKey = "password_hash"
        static let createdAt:    FieldKey = "created_at"
    }
}
// Sources/App/Models/Post.swift
import Fluent
import Vapor

final class Post: Model, Content {
    static let schema = "posts"

    @ID(format: .uuid)
    var id: UUID?

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

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

    @Field(key: "published")
    var published: Bool

    // 多对一:每个 Post 属于一个 User(外键)
    @Parent(key: "author_id")
    var author: User

    @Timestamp(key: "created_at", on: .create)
    var createdAt: Date?

    init() {}

    init(title: String, body: String, published: Bool = false, authorID: UUID) {
        self.title     = title
        self.body      = body
        self.published = published
        self.$author.id = authorID
    }
}

Migration:创建与管理表结构

Migration 是数据库结构变更的版本控制。每次表结构改动(新增列、删除索引等)都写成一个新的 Migration 类,而不是修改已有的 Migration,这样可以追踪所有变更历史,也可以回滚到任意版本。

// Sources/App/Migrations/CreateUser.swift
import Fluent

struct CreateUser: AsyncMigration {
    // 正向迁移:创建 users 表
    func prepare(on database: Database) async throws {
        try await database.schema("users")
            .id()                                  // UUID 主键
            .field("name",          .string, .required)
            .field("email",         .string, .required)
            .field("password_hash", .string, .required)
            .field("created_at",    .datetime)
            .unique(on: "email")                  // email 唯一索引
            .create()
    }

    // 回滚:删除表
    func revert(on database: Database) async throws {
        try await database.schema("users").delete()
    }
}

// Sources/App/Migrations/CreatePost.swift
struct CreatePost: AsyncMigration {
    func prepare(on database: Database) async throws {
        try await database.schema("posts")
            .id()
            .field("title",      .string,   .required)
            .field("body",       .string,   .required)
            .field("published", .bool,     .required)
            .field("author_id", .uuid,     .required,
                   .references("users", "id", onDelete: .cascade))
            .field("created_at",.datetime)
            .create()
    }
    func revert(on database: Database) async throws {
        try await database.schema("posts").delete()
    }
}
// configure.swift — 注册 Migration
public func configure(_ app: Application) async throws {
    // 配置数据库(PostgreSQL)
    app.databases.use(.postgres(configuration: .init(
        hostname: Environment.get("DB_HOST") ?? "localhost",
        port:     Int(Environment.get("DB_PORT") ?? "5432") ?? 5432,
        username: Environment.get("DB_USER") ?? "vapor",
        password: Environment.get("DB_PASS") ?? "vapor",
        database: Environment.get("DB_NAME") ?? "vapor_db"
    )), as: .psql)

    // 按顺序注册 Migration(顺序很重要!外键依赖)
    app.migrations.add(CreateUser())
    app.migrations.add(CreatePost())

    // 开发时自动运行 Migration(生产环境慎用)
    try await app.autoMigrate()
}
# 手动执行迁移
swift run App migrate

# 回滚最后一批迁移
swift run App migrate --revert

QueryBuilder:查询组合

Fluent 的 QueryBuilder 采用链式调用风格,每个方法返回新的 QueryBuilder,最终调用终结方法(.all().first()等)触发实际 SQL 执行。

Swift QueryBuilder 链: User.query(on: db) .filter(\.$email == "alice@example.com") → WHERE email = 'alice@example.com' .filter(\.$published == true) → AND published = true .sort(\.$createdAt, .descending) → ORDER BY created_at DESC .range(0..<10) → LIMIT 10 OFFSET 0 .with(\.$posts) → LEFT JOIN posts (预加载关联) .all() → 执行 SQL,返回 [User] 生成 SQL(近似): SELECT users.*, posts.* FROM users LEFT JOIN posts ON posts.author_id = users.id WHERE users.email = 'alice@example.com' AND posts.published = true ORDER BY users.created_at DESC LIMIT 10
// 常用查询示例

// 1. 按条件过滤
let activeUsers = try await User.query(on: db)
    .filter(\.$email, .hasSuffix, "@example.com")
    .all()

// 2. 预加载关联(避免 N+1 查询问题)
let users = try await User.query(on: db)
    .with(\.$posts) { posts in
        posts.filter(\.$published == true) // 只加载已发布的文章
    }
    .all()

// 3. 聚合查询
let count = try await Post.query(on: db)
    .filter(\.$published == true)
    .count()

// 4. 数据库事务(原子性)
try await db.transaction { txDB in
    let user = User(name: "Alice", email: "alice@e.com", passwordHash: "hash")
    try await user.save(on: txDB)

    let post = Post(title: "Hello", body: "World", authorID: user.id!)
    try await post.save(on: txDB)
    // 任意 throw 都会自动回滚两条操作
}
生产环境慎用 autoMigrate app.autoMigrate() 在每次启动时自动执行未运行的 Migration,方便开发,但在生产环境中可能造成意外。生产部署建议在 CI/CD 中显式执行 migrate 命令,并做好数据备份后再运行。
本章小结 Fluent 通过属性包装器(@ID、@Field、@Parent、@Children)建立类型安全的数据库映射;Migration 提供可回滚的数据库版本管理;QueryBuilder 的链式 API 组合出清晰的查询逻辑。下一章将学习如何处理 HTTP 请求体与响应,深入 Content 协议和错误处理。