Vapor 中,给我一个 recipe ,我怎么知道 tags 有被关联获取?

132 阅读2分钟

假设有 Recipe 模型定义如下:

final class Recipe: Model, Content, @unchecked Sendable {
    static let schema = "recipes"  // 表名

    enum FieldKeys {
        static let title: FieldKey = "title"  // 菜谱名称
        static let coverImageId: FieldKey = "coverImageId"  // 菜谱封面图ID
        static let description: FieldKey = "description"  // 菜谱简介
        static let difficulty: FieldKey = "difficulty"  // 难度级别 (easy / medium / hard)
        static let durationMinutes: FieldKey = "duration_minutes"  // 准备和烹饪所需的时间 (分钟)
        static let servings: FieldKey = "servings"  // 适合人数
        static let content: FieldKey = "content"  // 菜谱步骤,富文本或 JSON 格式
        static let createdAt: FieldKey = "created_at"  // 创建时间
        static let updatedAt: FieldKey = "updated_at"  // 更新时间
    }

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

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

    @OptionalParent(key: FieldKeys.coverImageId)
    var coverImage: UploadedFile?

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

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

    @Field(key: FieldKeys.durationMinutes)
    var durationMinutes: Int

    @Field(key: FieldKeys.servings)
    var servings: Int

    @Field(key: FieldKeys.content)
    var content: String?

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

    @Timestamp(key: FieldKeys.updatedAt, on: .update)
    var updatedAt: Date?

    @Children(for: \.$recipe)
    var ingredients: [RecipeIngredient]  // 食材

    @Siblings(through: RecipeTag.self, from: \.$recipe, to: \.$tag)
    var tags: [Tag]  // 标签

    init() {}

    init(id: UUID? = nil, title: String, coverImageId: UUID? = nil, description: String) {
        self.id = id
        self.title = title
        self.$coverImage.id = coverImageId
        self.description = description
        self.difficulty = "easy"  // 默认设置为 "easy"
        self.durationMinutes = 0  // 默认时长为 0 分钟
        self.servings = 1  // 默认适合人数为 1
    }
} 

要知道一个Recipe 实例的 tags 有没有已经被取值(加载),你可以检查 Fluent 提供的 $tags 属性的状态,但 Fluent 本身并没有提供一个明显的 isLoaded API。

可以用以下可靠的方式来判断:

✅ 方式 1:尝试读取 $tags.value

if let tags = recipe.$tags.value {
    print("✅ 已加载 tags: \(tags)")
} else {
    print("❌ tags 还未加载,需要 .get(on:) 或 .with()")
}
  • recipe.$tags.valuenil 表示还没有加载(lazy 状态)
  • 有值说明已经通过 .with().load() 加载了

🔍 示例代码

let recipe = try await Recipe.query(on: req.db)
    .filter(\.$id == recipeID)
    .first()

if let recipe = recipe {
    if let tags = recipe.$tags.value {
        print("✅ 已经加载了 tags: \(tags.map { $0.name })")
    } else {
        print("❌ 尚未加载 tags,手动加载中...")
        let tags = try await recipe.$tags.get(on: req.db)
        print("加载成功: \(tags.map { $0.name })")
    }
}

🔍 为什么默认不会加载关联数据?

Fluent 遵循 延迟加载(Lazy Loading) 的原则:

  • @Siblings@Children@Parent 这些关系字段本身只是一个“查询通道”;

  • 你必须显式调用 .load(on:).load(on:) 来发起查询;

  • 默认只加载模型本身的数据,避免自动 JOIN 导致性能问题。

✅ 延伸:如何确保总是加载好?

.with(\.$tags) 提前加载是最佳做法,之后你就能直接访问 recipe.tags 而不担心是空:

let recipe = try await Recipe.query(on: req.db)
    .with(\.$tags)
    .filter(\.$id == recipeID)
    .first()

let tags = recipe?.tags

为什么这么简单的特性,我需要在本文中拎出来单独强调,因为在模型间的关联越复杂的时候,某些方法不总是需要加载关联字段的详情,而有些地方需要,甚至本身就是忘却了加载,这个时候,我们的输出模型就需要兼容这两种情况,不然就会造成代码错误。

 let mealRecipes = try await meal.$recipes.query(on: req.db)
       .sort(\.$sortOrder, .ascending)
       .with(\.$recipe, { recipe in
             recipe.with(\.$ingredients)
             recipe.with(\.$tags)
             recipe.with(\.$coverImage)
       })
       .all()

更多文章,请关注微信公众号:OldBird