跳转至

关系

Fluent 的模型 API 可帮助你通过关系创建和维护模型之间的引用。支持三种类型的关系:

Parent

@Parent 关系存储对另一个模型 @ID 属性的引用。

final class Planet: Model {
    // parent 关系示例。
    @Parent(key: "star_id")
    var star: Star
}

@Parent 包含一个名为 id@Field 字段,用于设置和更新关系。

// 设置 parent 关系的 id 字段
earth.$star.id = sun.id

举个例子,Planet 模型初始化如下所示:

init(name: String, starID: Star.IDValue) {
    self.name = name
    // ...
    self.$star.id = starID
}

key 参数定义了用于存储父标识符的字段键。假设 Star 有一个 UUID 标识符,这个 @Parent 关系与下面的字段定义兼容。

.field("star_id", .uuid, .required, .references("star", "id"))

请注意,.references约束是可选的。了解更多信息,请参见模式章节。

Optional Parent

@OptionalParent 关系存储对另一个模型 @ID 属性的可选引用。它的工作方式类似于 @Parent 但允许关系为 nil

final class Planet: Model {
    // 可选 parent 关系示例。
    @OptionalParent(key: "star_id")
    var star: Star?
}

字段定义与 @Parent 类似,但 .required 约束应省略。

.field("star_id", .uuid, .references("star", "id"))

Optional Child

@OptionalChild 属性在两个模型之间创建了一对一的关系。它不在根模型上存储任何值。

final class Planet: Model {
    // 可选 child 关系示例。
    @OptionalChild(for: \.$planet)
    var governor: Governor?
}

for 参数接受一个指向引用根模型的 @Parent@OptionalParent 关系的键路径。

可以使用 create 方法将一个新模型添加到这个关系中。

// 添加新模型到关系中。
let jane = Governor(name: "Jane Doe")
try await mars.$governor.create(jane, on: database)

这将自动为子模型上设置父 id。

由于此关系不存储任何值,因此根模型不需要数据库模式条目。

关系的一对一性质应该在子模型的模式中使用 .unique 对引用父模型的列的约束来强制执行。

try await database.schema(Governor.schema)
    .id()
    .field("name", .string, .required)
    .field("planet_id", .uuid, .required, .references("planets", "id"))
    // 唯一性约束示例。
    .unique(on: "planet_id")
    .create()

警告

从客户端模式中省略父 ID 字段的唯一性约束可能会导致不可预知的结果。如果没有唯一性约束,则子表可能最终包含任何给定父表的多个子行;在这种情况下,@OptionalChild 属性一次只能访问一个子级,无法控制加载哪个子级。如果你可能需要为任何给定的父级存储多个子行,请使用 @Children

Children

@Children 属性在两个模型之间创建一对多关系。它不在根模型上存储任何值。

final class Star: Model {
    // children 关系示例。
    @Children(for: \.$star)
    var planets: [Planet]
}

for 参数接受引用根模型的 @Parent@OptionalParent 关系的键路径。在本例中,我们引用了前面示例中的 @Parent关系。

可以使用 create 方法将新模型添加到此关系中

// 添加新模型到关系中。
let earth = Planet(name: "Earth")
try await sun.$planets.create(earth, on: database)

这将自动为子模型上设置父 id。

由于此关系不存储任何值,因此不需要数据库模式条目。

Siblings

@Siblings 属性在两个模型之间创建多对多关系。它通过称为 pivot 的三级模型来实现这一点。

让我们看一个 PlanetTag 之间的多对多关系的例子。

enum PlanetTagStatus: String, Codable { case accepted, pending }

// pivot 模型示例。
final class PlanetTag: Model {
    static let schema = "planet+tag"

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

    @Parent(key: "planet_id")
    var planet: Planet

    @Parent(key: "tag_id")
    var tag: Tag

    init() { }

    init(id: UUID? = nil, planet: Planet, tag: Tag, comments: String?, status: PlanetTagStatus?) throws {
        self.id = id
        self.$planet.id = try planet.requireID()
        self.$tag.id = try tag.requireID()
    }
}

任何包含至少两个 @Parent 关系的模型,每个关系对应两个要关联的模型,可以作为一个枢纽(pivot)。该模型可以包含其他属性,例如其 ID,甚至可以包含其他 @Parent 关系。

向 pivot 模型添加 unique 约束有助于防止冗余条目。请参阅模式了解更多信息。

// 不允许重复的关系。
.unique(on: "planet_id", "tag_id")

创建 pivot 后,使用该 @Siblings 属性创建关系。

final class Planet: Model {
    // siblings 关系示例。
    @Siblings(through: PlanetTag.self, from: \.$planet, to: \.$tag)
    public var tags: [Tag]
}

@Siblings 属性需要三个参数:

  • through:pivot 模型的类型。
  • from:从 pivot 到引用根模型的父关系的键路径。
  • to:从 pivot 到引用相关模型的父关系的键路径。

相关模型上的反向 @Siblings 属性完成了这种关系。

final class Tag: Model {
    // siblings 关系示例。
    @Siblings(through: PlanetTag.self, from: \.$tag, to: \.$planet)
    public var planets: [Planet]
}

Siblings Attach

@Siblings 属性具有从关系中添加和删除模型的方法。

使用 attach() 方法将单个模型或模型数组添加到关系中。需要时,会自动创建并保存枢纽模型。可以指定回调闭包以填充每个创建的枢纽模型的其他属性。

let earth: Planet = ...
let inhabited: Tag = ...
// 添加模型到关系中。
try await earth.$tags.attach(inhabited, on: database)
// 在建立关联时填充枢纽模型的属性。
try await earth.$tags.attach(inhabited, on: database) { pivot in
    pivot.comments = "This is a life-bearing planet."
    pivot.status = .accepted
}
// 将带有属性的多个模型添加到关系中。
let volcanic: Tag = ..., oceanic: Tag = ...
try await earth.$tags.attach([volcanic, oceanic], on: database) { pivot in
    pivot.comments = "This planet has a tag named \(pivot.$tag.name)."
    pivot.status = .pending
}

附加单个模型时,你可以使用 method 参数选择是否在保存之前检查关系。

// 只有当关系不存在时才附加。
try await earth.$tags.attach(inhabited, method: .ifNotExists, on: database)

使用 detach 方法从关系中删除一个模型。这将删除相应的 pivot 模型。

// 从关系中删除模型。
try await earth.$tags.detach(inhabited, on: database)

你可以使用 isAttached 方法检查模型是否相关。

// 检查模型是否有关。
earth.$tags.isAttached(to: inhabited)

Get

使用 get(on:) 方法获取关系的值。

// 获取太阳系的行星。
sun.$planets.get(on: database).map { planets in
    print(planets)
}

// 或者

let planets = try await sun.$planets.get(on: database)
print(planets)

使用 reload 参数来选择是否应该重新从数据库中获取已经加载的关系。

try await sun.$planets.get(reload: true, on: database)

查询

在关系上使用 query(on:) 方法为相关模型创建查询构建器。

// 获取太阳系中的行星,且命名以 M 开头。
try await sun.$planets.query(on: database).filter(\.$name =~ "M").all()

请参阅查询了解更多信息。

Eager Loading

当从数据库中获取模型关系时,可以使用 Fluent 的查询构建器预加载模型关系。这被称为预加载,它允许你同步访问关系,而不需要首先调用get方法。

要预加载关系,请将关系的键路径传递给查询构建器上 with 方法。

// 预加载示例。
Planet.query(on: database).with(\.$star).all().map { planets in
    for planet in planets {
        // `star` 在这里是同步访问的
        // 因为它已经预加载了。
        print(planet.star.name)
    }
}

// 或者

let planets = try await Planet.query(on: database).with(\.$star).all()
for planet in planets {
    // `star` 在这里是同步访问的
    // 因为它已经预加载了。
    print(planet.star.name)
}

在上面的例子中,名为 star@Parent关系的键路径被传递给了 with 方法。这将导致查询构建器在所有行星加载后执行额外查询,以获取它们相关的所有恒星。然后通过 @Parent 属性同步访问恒星。

无论返回多少个模型,每个预加载的关系只需要一个额外的查询。只有使用查询构建器的 allfirst 方法才能立即加载。

Nested Eager Load

查询构建器的 with 方法允许你在被查询的模型上预先加载关系。但是,你也可以在相关模型上预先加载关系。

let planets = try await Planet.query(on: database).with(\.$star) { star in
    star.with(\.$galaxy)
}.all()
for planet in planets {
    // `star.galaxy` 在这里是同步访问的
    // 因为它已经被预加载
    print(planet.star.galaxy.name)
}

with 方法接受一个可选闭包作为第二个参数。这个闭包接受所选关系的预加载构建器。预加载嵌套深度没有限制。

Lazy Eager Loading

如果你已经检索了父模型,并且你想加载它的一个关系,你可以使用 get(reload:on:) 方法来实现这个目的。这将从数据库(或可用缓存)中获取相关的模型,并允许它作为本地属性访问。

planet.$star.get(on: database).map {
    print(planet.star.name)
}

// Or

try await planet.$star.get(on: database)
print(planet.star.name)

使用realod:参数可以确保取回的数据并非来自缓存。

try await planet.$star.get(reload: true, on: database)
print(planet.star.name)

要检查是否已加载关系,请使用 value 属性。

if planet.$star.value != nil {
    // 关系已被加载。
    print(planet.star.name)
} else {
    // 关系还未加载。
    // 试图访问 planet.star 将会失败。
}

如果你已经在变量中拥有相关模型,则可以使用上述的 value 属性手动设置关系。

planet.$star.value = star

这会将相关模型附加到父模型,就好像它是预先加载或延迟加载的,而无需额外的数据库查询。