Fluent Relations¶
Fluent supports two methods for relating models: one-to-many (parent-child) and many-to-many (siblings). These relations help make working with a normalized data structure easy.
Parent-Child¶
The most common model relation is the one-to-many or parent-child relation. In this relation, each child model stores at most one identifier of a parent model. In most cases, multiple child models can store the same parent identifier at the same time. This means that any given parent can have zero or more related child models. Hence the name, one (parent) to many (children).
Note
If each child must store a unique parent ID, this relation is called a one-to-one relation.
Take a look at the following diagram in which an example parent-child relation between two models (Galaxy
and Planet
) is shown.
In the example above, Galaxy
is the parent and Planet
is the child. Planets store an identifier referencing exactly one galaxy (the galaxy they are in). In turn, each galaxy has zero or more planets that belong to it.
Let's take a look at what these models would look like in Fluent.
struct Galaxy: Model { // ... var id: Int? var name: String }
struct Planet: Model { // ... var id: Int? var name: String var galaxyID: Int }
For more information on defining models see Fluent → Models.
Fluent provides two helpers for working with parent-child relations: Parent
and Children
. These helpers can be created using extensions on the related models for convenient access.
extension Galaxy { // this galaxy's related planets var planets: Children<Galaxy, Planet> { return children(\.galaxyID) } }
Here the children(_:)
method is used on Galaxy
to create the relation. The resulting type has two generic arguments in the signature that can be thought of as <From, To
>. Since this relation goes from galaxy to planet, they are ordered as such in the generic arguments.
Note that this method is not static. That is because it must access the galaxy's identifier to perform the relation lookup.
extension Planet { // this planet's related galaxy var galaxy: Parent<Planet, Galaxy> { return parent(\.galaxyID) } }
Here the parent(_:)
method is used on Planet
to create the inverse relation. The resulting type also has two generic arguments. In this case, they are reversed since this relation now goes from planet to galaxy.
Note that this method is also not static. That is because it must access the referenced identifier to perform the relation lookup.
Now that the models and relation properties are created, they can be used to create, read, update, and delete related data.
let galaxy: Galaxy = ... let planets = galaxy.planets.query(on: ...).all()
The query(on:)
method on a relation creates an instance of QueryBuilder
filtered to the related models. See Fluent → Querying for more information on working with the query builder.
let planet: Planet = ... let galaxy = planet.galaxy.get(on: ...)
Since the child can have at most one parent, the most useful method is [get(on:)
] which simply returns the parent model.
Siblings¶
A more powerful (and complex) relation is the many-to-many or siblings relation. In this relation, two models are related by a third model called a pivot. The pivot is a simple model that carries one identifier for each of the two related models. Because a third model (the pivot) stores identifiers, each model can be related to zero or more models on the other side of the relation.
Take a look at the following diagram in which an example siblings relation between two models (Planet
and Tag
) and a pivot (PlanetTag
) is shown.
A siblings relation is required for the above example because:
- Both Earth and Venus have the Earth Sized tag.
- Earth has both the Earth Sized and Liquid Water tag.
In other words, two planets can share one tag and two tags can share one planet. This is a many-to-many relation.
Let's take a look at what these models would look like in Fluent.
struct Planet: Model { // ... var id: Int? var name: String var galaxyID: Int }
struct Tag: Model { // ... var id: Int? var name: String }
For more information on defining models see Fluent → Models.
Now let's take a look at the pivot. It may seem a bit intimidating at first, but it's really quite simple.
struct PlanetTag: Pivot { // ... typealias Left = Planet typealias Right = Tag static var leftIDKey: LeftIDKey = \.planetID static var rightIDKey: RightIDKey = \.tagID var id: Int? var planetID: Int var tagID: Int }
A pivot must have Left
and Right
model types. In this case, those model types are Planet
and Tag
. Although it is arbitrary which model is left vs. right, a good rule of thumb is to order things alphabetically for consistency.
Once the left and right models are defined, we must supply Fluent with key paths to the stored properties for each ID. We can use the LeftIDKey
and RightIDKey
type-aliases to do this.
A Pivot
is also a Model
itself. You are free to store any additional properties here if you like. Don't forget to create a migration for it if you are using a database that supports schemas.
Once the pivot and your models are created, you can add convenience extensions for interacting with the relation just like the parent-child relation.
extension Planet { // this planet's related tags var tags: Siblings<Planet, Tag, PlanetTag> { return siblings() } }
Because the siblings relation requires three models, it has three generic arguments. You can think of the arguments as <From, To, Through>
. This relation goes from a planet to tags through the planet tag pivot.
The other side of the relation (on tag) is similar. Only the first two generic arguments are flipped.
extension Tag { // all planets that have this tag var planets: Siblings<Tag, Planet, PlanetTag> { return siblings() } }
Now that the relations are setup, we can query a planet's tags. This works just like the Children
type in the parent-child relationship.
let planet: Planet = ... planet.tags.query(on: ...).all()
Modifiable Pivot¶
If the pivot conforms to ModifiablePivot
, then Fluent can help to create and delete pivots (called attaching and detaching).
Conforming a pivot is fairly simple. Fluent just needs to be able to initialize the pivot from two related models.
extension PlanetTag: ModifiablePivot { init(_ planet: Planet, _ tag: Tag) throws { planetID = try planet.requireID() tagID = try tag.requireID() } }
Once the pivot type conforms, there will be extra methods available on the siblings relation.
let planet: Planet = ... let tag: Tag = ... planet.tags.attach(tag, on: ...)