Aller au contenu principal

Gestion des relations avec Prisma et MongoDB

Dans ce domcument, nous allons voir comment modéliser et requêter les relations entre nos schema prisma

Préambule: Réflection autour de la propriété connect

Dans Prisma, la propriété connect est un outil vraiment pratique créer et manager les connexions entre collection. Celà étant dit, il faut également garder à l'esprit les contraintes lié à l'outil, qui malheuresement ne nous permettent pas d'utiliser connect tout le temps.

Malheuresement, les choses se compliquent ici. En effet il va falloir différencier deux cas. Est-ce que l'ordre d'ajout des données peux être déterminé ? Est-ce qu'on maitrise toutes données ?

Si la réponse à ces deux questions est Oui !, alors on peut opter pour la solution avec connect. Autrement, il faut utiliser une approche plus manuelle.

Les relations One-to-One et One-to-Many, présentés dans ce document, peuvent être utiliser à la fois avec connect ou manuellement. Seul la relation Many-to-Many doit être designé différement en fonction des cas de figure

Context

Dans notre architecture, la quasi totalité des ajout de données en base se fait via les lambdas et le traitement des différents flux. Afin d'utiliser connect, il faut s'assurer que l'objet que l'on veux connecter existe. Malheuresement, vérifier l'existence des données à lier impliquerait plus de travail pour la DB, et étant donnée le caractère asynchrone du traitement des flux, la donnée pourrait très bien ne pas exister encore.

Une "régle générale", valable dans 90% des cas, dans les traitements du flux PIM, on peut utiliser connect. Dans les autres cas il faut opter pour l'approche manuelle.

Relation One to One

Voici la modélisation d'une relation one to one. L'établissement peut avoir une feature, la feature est forcement relier à un établissement

Modèle

model Establishment {
id String @id @default(auto()) @map("_id") @db.ObjectId
iResaId Int @unique
name String
feature Feature?
}

model Feature {
id String @id @default(auto()) @map("_id") @db.ObjectId
iResaId Int @unique
name String
establishmentId Int @unique // iResaId de l'établissement
establishment Establishment @relation(fields: [establishmentId], references: [iResaId])
}

Ajouter en base

Peut importe l'ordre d'ajout de l'etablissement ou de la feature, le lien entre les deux entité se fera automatiquement quand on renseignera le champs establishmentId dans le modèle Feature

await prisma.establishment.create({
data: {
iResaId: 111,
name: 'Résidence 111',
},
});

await prisma.feature.create({
data: {
iResaId: 222,
name: 'Feature 222',
establishmentId: 111, // le lien se fait manuellement en reseignant l'id
},
});

//Avec Connect
await prisma.feature.create({
data: {
iResaId: 222,
name: 'Feature 222',
establishment: {
connect: { iResaId: 111 },
},
},
});

Find

Dans une relation one-to-one, on peut simplement utiliser include pour récupérer l'objet lié

const ests = prisma.establishment.findMany({
include: { feature: true },
});

const features = prisma.feature.findMany({
include: { establishment: true },
});

Relation One to Many

Voici la modélisation d'une relation one to many. Prenons l'exemple de la relation entre un establishment et des rooms

Modèle

model Establishment {
id String @id @default(auto()) @map("_id") @db.ObjectId
iResaId Int @unique
name String
rooms Room[]
}

model Room {
id String @id @default(auto()) @map("_id") @db.ObjectId
iResaId Int @unique
name String
establishmentId Int @unique // iResaId de l'établissement
establishment Establishment @relation(fields: [establishmentId], references: [iResaId])
}

Ajouter en base

Peut importe l'ordre d'ajout de l'etablissement ou des rooms, le lien entre les deux entité se fera automatiquement quand on renseignera le champs establishmentId dans le modèle Room

await prisma.establishment.create({
data: {
iResaId: 111,
name: 'Résidence 111',
},
});

await prisma.room.create({
data: {
iResaId: 1000,
name: 'Room 1000',
establishmentId: 111, // le lien se fait manuellement en reseignant l'id
},
});

await prisma.room.create({
data: {
iResaId: 1000,
name: 'Room 1000',
establishment: {
connect: { iResaId: 111 },
},
},
});

Find

Du point de vue de l'établissement, il suffit d'utilise include simplement.

const ests = prisma.establishment.findMany({
include: { feature: true },
});

Au moment de créer la room, le champ establishmentId est obligatoire, mais il est possible d'ajouter un établissement qui n'existe pas encore (ex: room 9999). Dans ce cas, si on utilise juste include, on va récupérer une erreur Pour cela, on va rajouter une clause where pour filtrer les rooms lié a un établissement inconnu.

// Cette opération est valide
await prisma.room.create({
data: {
iResaId: 1000,
name: 'Room 1000',
establishmentId: 111,
},
});

// Prisma va nous retourner un erreur car l'établissement 9999 n'existe pas au moment d'include
const roomsWithEst = await prisma.room.findMany({
include: { establishment: true },
});

// Ajoute d'un where pour récupérer que les rooms "complètes"
const roomsWithEst = await prisma.room.findMany({
where: { establishment: { id: { not: undefined } } },
include: { establishment: true },
});

/// résultats
[
{
id: '66fd4312686a4d6ec06a9c12',
iResaId: 1000,
name: 'Room 1000',
establishmentId: 111,
establishment: {
id: '66fd4312686a4d6ec06a9c11',
iResaId: 111,
name: 'Résidence 111',
},
},
];

Cas particulier

Une autre approche serait possible, en fonction de la logique métier. Imaginons un modèle Type, qui peut être lié à un établissement

model Establishment {
id String @id @default(auto()) @map("_id") @db.ObjectId
iResaId Int @unique
name String
types Type[]
}

model Model {
id String @id @default(auto()) @map("_id") @db.ObjectId
iResaId Int @unique
name String
establishmentId Int? @unique // iResaId de l'établissement
establishment Establishment? @relation(fields: [establishmentId], references: [iResaId])
}

const allModels = await prisma.model.findMany({
include: { establishment: true },
});
// Résultats:
[
{
id: '66fd44c3cdfb42e45aa29629',
iResaId: 1,
name: 'Model 1',
establishmentId: 111,
establishment: {
id: '66fd44c3cdfb42e45aa29628',
iResaId: 111,
name: 'Résidence 111',
},
},
{
id: '66fd44c3cdfb42e45aa2962a',
iResaId: 2,
name: 'Model 2',
establishmentId: 999,
establishment: null,
},
];

L'ajout de Int? et Establishment? rend optionnel l'include. On récupère logiquement establishment: null. Ce cas devra être gérer dans le code ensuite.

Relation Many to Many

Many to Many avec Connect

C'est le cas, par exemple, avec les données du PIM. Quand on process un flux PIM, on a tous les fichiers disponibles avant de commencer, on est capable d'ordonner nos imports (Countries puis Regions puis Stations puis Establishment ...) D'autre part, on ne mélange pas l'origine de nos données, tout viens du PIM, à chaque modification, ils nous fournissent l'ensemble des données. Dans ce cas, on peut facilement modéliser cette relation.

Modèle

model Establishment {
id String @id @default(auto()) @map("_id") @db.ObjectId
iResaId Int @unique
name String
holidayTypes HolidayType[] @relation(fields: [holidayTypeIds], references: [id])
holidayTypeIds String[] @db.ObjectId
}

model HolidayType {
id String @id @default(auto()) @map("_id") @db.ObjectId
iResaId Int @unique
name String
establishmentIds String[] @db.ObjectId
establishments Establishment[] @relation(fields: [establishmentIds], references: [id])
}

Il faut noté les références dans cette relation, il s'agit des id. Malheuresement, Mongo nous impose une double contrainte :

  1. Pour les relations many-to-many, le champs references DOIT être la clé primaire de la collection, modéliser par le @id
  2. La clé primaire d'une collection mongo doit être le champs interne _id, d'où le @map("_id")

Ajouter en base

await prisma.holidayType.create({
data: {
iResaId: 1,
name: 'Type 1',
},
});

await prisma.establishment.create({
data: {
iResaId: 111,
name: 'Résidence 111',
holidayTypes: {
connect: [{ iResaId: 1 }],
},
},
});

Dans ce cas, on peut utiliser connect car dans notre workflow, on upsert les holidayTypes avant les establishments, et on sais que si les holidayTypes d'un établissement changent (un nouveau type), il sera dans le flux également

Find

On peut simplement utiliser connect

"Many to Many" manuellement.

C'est le cas des relations entre données de flux différents. Par exemple, les thématiques, les packages ou les offres sont gérer à la fois dans Iresa et dans le PIM. Malhuresement on ne maitrise pas l'ordre d'ajout des flux, une partie de la data peut être dans un flux mais pas encore dans l'autre. Dans ce cas, on est obligé de traiter les deux flux séparement et recréer la logique manuellement quand on souhaite traiter la donnée.

model Offer {
id String @id @default(auto()) @map("_id") @db.ObjectId
iResaId Int @unique
itemKey String @unique
roomIds Int[]
}

dans le cas ci dessus, la collection Offer est alimenter via des données cache-data, qui nous fourni une liste de type roomIds (liste de iResaID). Or les rooms sont gérer dans le PIM. Au moment où l'on importe l'offer, on a aucune idée de si toutes les rooms soient déjà créer.

//Dans apollo par exemple
const offer = await prisa.offer.findUnique({
where: { iResaId: 123 },
});
const rooms = await prisma.room.findMany({
where: {
iResaId: { in: offer.roomIds },
},
});

// TODO: traiter le cas "pas de room" ?