Relacje#
API modelu Fluenta pomaga tworzyć i utrzymywać odwołania między Twoimi modelami za pomocą relacji. Wspierane są trzy typy relacji:
Parent#
Relacja @Parent przechowuje odwołanie do właściwości @ID innego modelu.
final class Planet: Model {
// Przykład relacji rodzica.
@Parent(key: "star_id")
var star: Star
}
@Parent zawiera @Field o nazwie id, który jest używany do ustawiania i aktualizowania relacji.
// Ustaw identyfikator relacji rodzica
earth.$star.id = sun.id
Na przykład inicjalizator Planet mógłby wyglądać tak:
init(name: String, starID: Star.IDValue) {
self.name = name
// ...
self.$star.id = starID
}
Parametr key określa klucz pola używany do przechowywania identyfikatora rodzica. Zakładając, że Star ma identyfikator typu UUID, ta relacja @Parent jest kompatybilna z następującą definicją pola.
.field("star_id", .uuid, .required, .references("star", "id"))
Zwróć uwagę, że ograniczenie .references jest opcjonalne. Zobacz schemat, aby uzyskać więcej informacji.
Optional Parent#
Relacja @OptionalParent przechowuje opcjonalne odwołanie do właściwości @ID innego modelu. Działa podobnie do @Parent, ale pozwala, aby relacja miała wartość nil.
final class Planet: Model {
// Przykład opcjonalnej relacji rodzica.
@OptionalParent(key: "star_id")
var star: Star?
}
Definicja pola jest podobna do tej dla @Parent, z tą różnicą, że ograniczenie .required powinno zostać pominięte.
.field("star_id", .uuid, .references("star", "id"))
Kodowanie i dekodowanie rodziców#
Jedną rzeczą, na którą należy uważać podczas pracy z relacjami @Parent, jest sposób, w jaki są one wysyłane i odbierane. Na przykład w formacie JSON @Parent dla modelu Planet może wyglądać tak:
{
"id": "A616B398-A963-4EC7-9D1D-B1AA8A6F1107",
"star": {
"id": "A1B2C3D4-1234-5678-90AB-CDEF12345678"
}
}
Zwróć uwagę, że właściwość star jest obiektem, a nie identyfikatorem, którego można by się spodziewać. Podczas wysyłania modelu jako ciała żądania HTTP, dane muszą mieć taką strukturę, aby dekodowanie zadziałało. Z tego powodu zdecydowanie zalecamy używanie DTO do reprezentowania modelu podczas wysyłania go przez sieć. Na przykład:
struct PlanetDTO: Content {
var id: UUID?
var name: String
var star: Star.IDValue
}
Następnie możesz zdekodować DTO i przekonwertować je na model:
let planetData = try req.content.decode(PlanetDTO.self)
let planet = Planet(id: planetData.id, name: planetData.name, starID: planetData.star)
try await planet.create(on: req.db)
To samo dotyczy zwracania modelu klientom. Twoi klienci muszą być w stanie obsłużyć zagnieżdżoną strukturę, albo musisz przekonwertować model na DTO przed jego zwróceniem. Więcej informacji o DTO znajdziesz w dokumentacji Modelu
Optional Child#
Właściwość @OptionalChild tworzy relację jeden-do-jednego między dwoma modelami. Nie przechowuje ona żadnych wartości w modelu głównym.
final class Planet: Model {
// Przykład opcjonalnej relacji dziecka.
@OptionalChild(for: \.$planet)
var governor: Governor?
}
Parametr for przyjmuje key-path do relacji @Parent lub @OptionalParent, odwołującej się do modelu głównego.
Nowy model może zostać dodany do tej relacji za pomocą metody create.
// Przykład dodawania nowego modelu do relacji.
let jane = Governor(name: "Jane Doe")
try await mars.$governor.create(jane, on: database)
Spowoduje to automatyczne ustawienie identyfikatora rodzica w modelu dziecka.
Ponieważ ta relacja nie przechowuje żadnych wartości, dla modelu głównego nie jest wymagany żaden wpis w schemacie bazy danych.
Charakter jeden-do-jednego tej relacji powinien być wymuszony w schemacie modelu dziecka za pomocą ograniczenia .unique na kolumnie odwołującej się do modelu rodzica.
try await database.schema(Governor.schema)
.id()
.field("name", .string, .required)
.field("planet_id", .uuid, .required, .references("planets", "id"))
// Przykład ograniczenia unikalności
.unique(on: "planet_id")
.create()
Warning
Pominięcie ograniczenia unikalności na polu identyfikatora rodzica w schemacie klienta może prowadzić do nieprzewidywalnych rezultatów.
Jeśli nie ma ograniczenia unikalności, tabela dziecka może zawierać więcej niż jeden wiersz dziecka dla danego rodzica; w takim przypadku właściwość @OptionalChild nadal będzie mogła uzyskać dostęp tylko do jednego dziecka naraz, bez możliwości kontrolowania, które dziecko zostanie załadowane. Jeśli chcesz przechowywać wiele wierszy dziecka dla danego rodzica, użyj zamiast tego @Children.
Children#
Właściwość @Children tworzy relację jeden-do-wielu między dwoma modelami. Nie przechowuje ona żadnych wartości w modelu głównym.
final class Star: Model {
// Przykład relacji dzieci.
@Children(for: \.$star)
var planets: [Planet]
}
Parametr for przyjmuje key-path do relacji @Parent lub @OptionalParent, odwołującej się do modelu głównego. W tym przypadku odwołujemy się do relacji @Parent z poprzedniego przykładu.
Nowe modele mogą zostać dodane do tej relacji za pomocą metody create.
// Przykład dodawania nowego modelu do relacji.
let earth = Planet(name: "Earth")
try await sun.$planets.create(earth, on: database)
Spowoduje to automatyczne ustawienie identyfikatora rodzica w modelu dziecka.
Ponieważ ta relacja nie przechowuje żadnych wartości, żaden wpis w schemacie bazy danych nie jest wymagany.
Siblings#
Właściwość @Siblings tworzy relację wiele-do-wielu między dwoma modelami. Osiąga to za pomocą trzeciego modelu zwanego pivotem.
Przyjrzyjmy się przykładowi relacji wiele-do-wielu między Planet a Tag.
enum PlanetTagStatus: String, Codable { case accepted, pending }
// Przykład modelu pivota.
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
@OptionalField(key: "comments")
var comments: String?
@OptionalEnum(key: "status")
var status: PlanetTagStatus?
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()
self.comments = comments
self.status = status
}
}
Każdy model, który zawiera co najmniej dwie relacje @Parent, po jednej dla każdego modelu, który ma zostać powiązany, może zostać użyty jako pivot. Model może zawierać dodatkowe właściwości, takie jak jego ID, a nawet inne relacje @Parent.
Dodanie ograniczenia unique do modelu pivota może pomóc zapobiec powielaniu wpisów. Zobacz schemat, aby uzyskać więcej informacji.
// Zapobiega duplikowaniu relacji.
.unique(on: "planet_id", "tag_id")
Gdy pivot zostanie utworzony, użyj właściwości @Siblings, aby utworzyć relację.
final class Planet: Model {
// Przykład relacji rodzeństwa.
@Siblings(through: PlanetTag.self, from: \.$planet, to: \.$tag)
public var tags: [Tag]
}
Właściwość @Siblings wymaga trzech parametrów:
through: Typ modelu pivota.from: Key-path od pivota do relacji rodzica odwołującej się do modelu głównego.to: Key-path od pivota do relacji rodzica odwołującej się do powiązanego modelu.
Odwrotna właściwość @Siblings w powiązanym modelu dopełnia relację.
final class Tag: Model {
// Przykład relacji rodzeństwa.
@Siblings(through: PlanetTag.self, from: \.$tag, to: \.$planet)
public var planets: [Planet]
}
Siblings Attach#
Właściwość @Siblings posiada metody do dodawania i usuwania modeli z relacji.
Użyj metody attach(), aby dodać pojedynczy model lub tablicę modeli do relacji. Modele pivota są tworzone i zapisywane automatycznie w razie potrzeby. Można podać domknięcie zwrotne, aby wypełnić dodatkowe właściwości każdego utworzonego pivota:
let earth: Planet = ...
let inhabited: Tag = ...
// Dodaje model do relacji.
try await earth.$tags.attach(inhabited, on: database)
// Wypełnij atrybuty pivota podczas nawiązywania relacji.
try await earth.$tags.attach(inhabited, on: database) { pivot in
pivot.comments = "This is a life-bearing planet."
pivot.status = .accepted
}
// Dodaj wiele modeli z atrybutami do relacji.
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
}
Podczas dołączania pojedynczego modelu możesz użyć parametru method, aby wybrać, czy relacja powinna zostać sprawdzona przed zapisem.
// Dołącza tylko wtedy, gdy relacja jeszcze nie istnieje.
try await earth.$tags.attach(inhabited, method: .ifNotExists, on: database)
Użyj metody detach, aby usunąć model z relacji. Powoduje to usunięcie odpowiadającego modelu pivota.
// Usuwa model z relacji.
try await earth.$tags.detach(inhabited, on: database)
Możesz sprawdzić, czy model jest powiązany, czy nie, za pomocą metody isAttached.
// Sprawdza, czy modele są powiązane.
earth.$tags.isAttached(to: inhabited)
Get#
Użyj metody get(on:), aby pobrać wartość relacji.
// Pobiera wszystkie planety słońca.
sun.$planets.get(on: database).map { planets in
print(planets)
}
// Lub
let planets = try await sun.$planets.get(on: database)
print(planets)
Użyj parametru reload, aby wybrać, czy relacja powinna zostać ponownie pobrana z bazy danych, jeśli została już wcześniej załadowana.
try await sun.$planets.get(reload: true, on: database)
Query#
Użyj metody query(on:) na relacji, aby utworzyć query builder dla powiązanych modeli.
// Pobierz wszystkie planety słońca, których nazwa zaczyna się na M.
try await sun.$planets.query(on: database).filter(\.$name =~ "M").all()
Zobacz zapytania, aby uzyskać więcej informacji.
Eager Loading#
Query builder Fluenta pozwala na wstępne ładowanie relacji modelu podczas jego pobierania z bazy danych. Nazywa się to eager loading i pozwala na synchroniczny dostęp do relacji bez konieczności wcześniejszego wywoływania get.
Aby wczytać relację z wyprzedzeniem, przekaż key-path do relacji do metody with na query builderze.
// Przykład eager loadingu.
Planet.query(on: database).with(\.$star).all().map { planets in
for planet in planets {
// `star` jest tutaj dostępne synchronicznie
// ponieważ zostało wcześniej załadowane (eager loaded).
print(planet.star.name)
}
}
// Lub
let planets = try await Planet.query(on: database).with(\.$star).all()
for planet in planets {
// `star` jest tutaj dostępne synchronicznie
// ponieważ zostało wcześniej załadowane (eager loaded).
print(planet.star.name)
}
W powyższym przykładzie key-path do relacji @Parent o nazwie star jest przekazywany do with. Powoduje to, że query builder wykonuje dodatkowe zapytanie po załadowaniu wszystkich planet, aby pobrać wszystkie powiązane z nimi gwiazdy. Gwiazdy są następnie dostępne synchronicznie za pomocą właściwości @Parent.
Każda relacja załadowana w ten sposób wymaga tylko jednego dodatkowego zapytania, niezależnie od liczby zwróconych modeli. Eager loading jest możliwe tylko z metodami all i first query buildera.
Nested Eager Load#
Metoda with query buildera pozwala na wczytanie z wyprzedzeniem relacji modelu, który jest przedmiotem zapytania. Możesz jednak również wczytać z wyprzedzeniem relacje modeli powiązanych.
let planets = try await Planet.query(on: database).with(\.$star) { star in
star.with(\.$galaxy)
}.all()
for planet in planets {
// `star.galaxy` jest tutaj dostępne synchronicznie
// ponieważ zostało wcześniej załadowane (eager loaded).
print(planet.star.galaxy.name)
}
Metoda with przyjmuje jako drugi parametr opcjonalne domknięcie. To domknięcie przyjmuje eager load builder dla wybranej relacji. Nie ma ograniczenia co do głębokości zagnieżdżenia eager loadingu.
Lazy Eager Loading#
W przypadku, gdy masz już pobrany model rodzica i chcesz załadować jedną z jego relacji, możesz użyć w tym celu metody get(reload:on:). Spowoduje to pobranie powiązanego modelu z bazy danych (lub z cache, jeśli jest dostępny) i umożliwi dostęp do niego jako do lokalnej właściwości.
planet.$star.get(on: database).map {
print(planet.star.name)
}
// Lub
try await planet.$star.get(on: database)
print(planet.star.name)
Jeśli chcesz mieć pewność, że otrzymywane dane nie pochodzą z cache, użyj parametru reload:.
try await planet.$star.get(reload: true, on: database)
print(planet.star.name)
Aby sprawdzić, czy relacja została załadowana, użyj właściwości value.
if planet.$star.value != nil {
// Relacja została załadowana.
print(planet.star.name)
} else {
// Relacja nie została załadowana.
// Próba dostępu do planet.star zakończy się niepowodzeniem.
}
Jeśli masz już powiązany model w zmiennej, możesz ręcznie ustawić relację za pomocą wspomnianej wyżej właściwości value.
planet.$star.value = star
Spowoduje to dołączenie powiązanego modelu do rodzica tak, jakby został on załadowany metodą eager loading lub lazy loading, bez dodatkowego zapytania do bazy danych.