Serverpod : Définition du modèle de données

Dans lʼarticle précédent, je vous ai présenté Serverpod et montré comment implémenter une authentification avec ce package. Continuons cette exploration endéveloppant et en déployant un modèle de données avec cet ORM. Reprenons donc notre projet Serverpod du chapitre précédent et débutons sans plus attendre.

Définition du modèle de données

1. Présentation de notre modèle à implémenter

Afin de correctement appréhender cette section, je vous conseille vivement la lecture de la documentation Serverpod sur lʼimplémentation des modèles et leur relations, fondamentaux de ce chapitre.

Notre application dʼexemple sera donc à destination des chasseurs de primes de lʼunivers de Star Wars. Notre modèle de données sera donc le suivant :

Le BountyHunter représente lʼutilisateur capable de créer, mettre à jour et partager les informations des ForceUser avec dʼautres BountyHunter. Ces ForceUser peuventêtre associés à un LaserSaber et éventuellement à leurs Droid.

En résumé, cette application permet aux chasseurs de primes de lʼunivers Star Wars dʼéchanger des informations sur les utilisateurs de la force quʼils répertorient.Ces utilisateurs peuvent être définis comme Jedi ou Sith, ainsi que comme maîtres ou apprentis. Pour chaque utilisateur de la Force, le chasseur de primes peutsaisir le nom du Jedi/Sith, son maître et ses apprentis en matière de maîtrise de la Force, ainsi que les caractéristiques de son sabre laser et de ses droids. Cesdonnées sont partagées entre tous les chasseurs de primes et peuvent être mises à jour.

Cette application dʼexemple et cette structure de données nous permettront notamment de couvrir une grande partie des relations exposées par Serverpod.

Passons maintenant à la création de ces différents modèles. Pour ce faire nous allons créer des fichiers YAML dans star_wars_bingo_pod/lib/src/models/ qui sont les suivants :

  • bounty_hunter_raw.spy.yaml
  • bounty_hunter.spy.yaml
  • force_user_raw.spy.yaml
  • force_user.spy.yaml
  • laser_saber.spy.yaml
  • droid.spy.yaml

Avant de les compléter, découvrons ensemble les syntaxes pour lʼimplémentation dʼun modèle avec Serverpod.

2. Introduction à la syntaxe des fichiers YAML pour la création de nos modèles et tables

La construction dʼun fichier YAML pour la création dʼune classe ou dʼune table est très intuitive. Voici un exemple expliqué :

class: ClassName table: table_name fields:
# Nommage de la classe à générer
# Nommage de la table à générer
# Description des attributs pour la classe et table
description: String
index: int
date: DateTime
child: OtherClassBuildWithAYamlFile
# Les attributs peuvent prendre comme type l'entièreté du scope de typage Dart # ainsi que toutes classes qui sera définis avec ces fichiers YAML

Tous les fichiers YAML doivent contenir lʼinformation class et au moins définir un attribut dans fields . Au contraire, table nʼest que optionnel. Si non défini, aucune table SQL ne sera générée pour ce modèle lors de lʼexécution de serverpod generate.

Pour appliquer des relations entre nos modèles créés, il suffit dʼajouter le mot-clé relation , comme dans lʼexemple suivant :

# user.spy.yaml class: User table: user fieds:
bio: Bio?, relation # one-to-one
followers: List?, relation # one-to-many (self)
posts: List?, relation(name=posts) # one-to-many paidService: List?, relation # many-to-many
# post.spy.yaml class: Post table: post fields:
text: String
userOwner: User?, relation(name=posts)
# enrolled_service.spy.yaml class: EnrolledService table: enrolled_service
fields:
enrolledUser: User?, relation(name=enrolled) service: Service?, relation(name=service)
indexes: enrolled_service_index_idx:
fields: userId, serviceId unique: true<</pre>

Vous comprendrez avec cet exemple que les relations one-to-one et one-to-many sont définies en déclarant respectivement un objet ou une liste dʼobjets.

De plus, pour une relation many-to-many, comme nous sommes dans un contexte SQL, il faut passer par une table/classe de jonction qui est représentée dans notre exemple avec EnrolledService. Cette dernière contient également des indexes que nous avons marqués comme unique sur les ids de son couple dʼattributs afin decertifier que nous nʼaurons quʼune instance de ce couple. À noter quʼil est également possible dʼapposer un ou des indexes sur les relations one-to-one (pour gagneren performance ou garantir que les deux objets ne sont bien reliés quʼentre eux avec le composant uniquetrue ). Pour plus de détails, dirigez-vous vers la documentation des indexes de Serverpod.

Il est également notable quʼil existe plusieurs façons dʼinstancier les relations one-to-one et one-to-many. Par exemple ici nous avons nommé la relation one-to-many pour lʼattribut posts, ce qui nous à donc contraint à lʼexpliciter dans le modèle de Post. Pour plus dʼinformations à ce sujet, je vous conseille de lire la documentation des relations de Serverpod.

Ceci étant explicité, je peux vous présenter lʼimplémentation de notre projet dʼexemple de lʼunivers Star Wars en vous décrivant simplement les différentes relations.

3. Implémentation et description de notre modèle de données

Commençons maintenant la mise en place de ce projet avec la création des modèles allant des plus distants de notre utilisateur jusquʼaux plus proches. Démarrons avec les modèles de LaserSaber et Droid :

# laser_saber.spy.yaml class: LaserSaber table: laser_saber fields:
name: String color: String
# droid.spy.yaml class: Droid table: droid fields:
name: String
forceUserMaster: ForceUserRaw?, relation(name=force_user_droids)

Ces deux modèles sont assez simples. Il y a seulement à noter la déclaration dʼune relation one-to-many avec le future objet ForceUserRaw qui contiendra une liste deDroid.

Continuons justement avec ce modèle plus complexe, ForceUserRaw :

# laser_saber.spy.yaml class: LaserSaber table: laser_saber fields:
name: String color: String
# droid.spy.yaml class: Droid table: droid fields:
name: String
forceUserMaster: ForceUserRaw?, relation(name=force_user_droids)
 droids: List?, relation(name=force_user_droids) hunterHuntedList: List?, relation(name=hunted)<</pre>

Comme observé avec le modèle Droid, il existe une relation one-to-many, définie ici avec List<Droid>, suivant la même logique de nommage.
LaserSaber, quant à lui, est en relation one-to-one, marquée comme optional. Cela signifie que lʼid de la clé étrangère dans la table ForceUserRaw peut être nul.

Il reste deux relations dans ce modèle, qualifiées de self-relation, cʼest-à-dire des relations dans la même table/classe. Nous avons les deux types de self-relations :

  • master, en one-to-one
  • apprentices, en one-to-many

Les noms des relations pour ces deux attributs sont identiques. Ainsi, lorsque lʼon assigne un master à un ForceUserRaw, la table de ce maître inclut automatiquement lʼélève dans sa liste dʼapprentices, et inversement.

Enfin, dans le modèle ForceUserRaw, une dernière relation many-to-many est implémentée avec le modèle BountyHunterRaw suivant :

# bounty_hunter.spy.yaml class: BountyHunterRaw table: bounty_hunter_raw fields:
userInfo: module:auth:UserInfo?, relation
hunterHuntedList: List?, relation(name=hunter) indexes:
user_info_id_unique_idx: fields: userInfoId unique: true
# hunter_hunted.spy.yaml class: HunterHunted table: hunter_hunted fields:
bountyHunter: BountyHunterRaw?, relation(name=hunter)
forceUser: ForceUserRaw?, relation(name=hunted) indexes:
hunter_hunted_index_idx:
fields: bountyHunterId, forceUserId unique: true<</pre>

Nous retrouvons naturellement la relation many-to-many précédemment mentionnée, avec lʼattribut hunterHuntedList du modèle BountyHunterRaw, représentant lʼutilisateur de lʼapplication. hunterHuntedList prend donc pour type List<HunterHunted>, la table de jonction pour cette relation.

Le modèle HunterHunted déclare un couple de BountyHunterRaw et de ForceUserRaw avec un index unique, assurant quʼil nʼy a quʼune seule instance par couple en base dedonnées.

Nous terminons notre tour des relations avec lʼattribut userInfo, qui est en one-to-one avec BountyHunterRaw et possède un index unique pour garantir quʼil nʼy a quʼun BountyHunterRaw par utilisateur de notre application. Étant donné que le modèle UserInfo est importé dʼun package externe à notre projet initial, nous déclarons son type comme suit : module:auth:UserInfo? .


Néanmoins, vous remarquerez quʼà cet instant, il nous manque lʼimplémentation de lʼenum ForceUserType qui est déployé comme suit :

enum: ForceUserType serialized: byName values:
- sithMaster
- sithApprentice - jediMaster
- jediApprentice

Mais aussi de deux modèles listés précédemment avec les fichiers bounty_hunter.spy.dart et force_user.spy.dart.

4. Implémentation des modèles dʼinterfaçage

Après la génération du code de nos modèles, les classes Dart traduisent directement les attributs des tables générées. Cela peut compliquer les relations entremodèles dans notre projet Flutter. Par exemple, voici les classes générées pour BountyHunterRaw et ForceUserRaw :

Nous pouvons simplifier ces modèles en créant des “classes dʼinterfaçageˮ :

# bounty_hunter.spy.yaml class: BountyHunter fields:
id: int?
userInfo: module:auth:UserInfo forceUsers: List
# force_user.spy.yaml class: ForceUser fields:
id: int?
type: ForceUserType
name: String
laserSaber: LaserSaber master: ForceUser? apprentices: List droids: List<</pre>

Aucun attribut table nʼest attribué à ces modèles car ils ne sont pas stockés en base de données. Cela améliore la lisibilité en supprimant les aspects relationnels.Voici les classes générées :

Il suffit ensuite dʼimplémenter un mapper pour convertir les versions “rawˮ en classes simplifiées après leur récupération en base de données.

5. Génération des tables et classes

Une fois nos modèles créés, exécutez serverpod generate à la racine de votre projet serveur. Cette commande génère :

  • Les tables SQL avec leurs relations
  • Les classes Dart correspondant à ces tables
  • Les opérations CRUD (insert, update, delete, find...) pour chaque classe

Le code généré se trouve dans lib/src/generated.

À lʼissue de cette étape, nous avons à notre disposition les méthodes CRUD, essentielles pour élaborer nos endpoints avec Serverpod. Nous explorerons en détail ce sujet dans la troisième et prochaine partie de cet article.

Quentin Lebreton - Ingénieur Etudes et Développement Junior