Codable et object graph complexe
Bonjour à tous,
ça fait un petit bout de temps que je ne suis pas venu par ici. Je ne suis pas développeur pro, mais prof de physique. Je construis une petite appli un peu plus légère qu'une autre appli que j'avais codé en objective-C avec CoreData.
J'ai voulu utiliser le protocole "Codable" à la place de NSCoding. Mon soucis : je pensais naïvement que, comme pour NSCoding, les références circulaires étaient gérées, mais non...
Voici mon modèle
//
// Model.swift
// VallComp
//
// Created by Mickaël VALLIER on 28/10/2020.
// Copyright © 2020 Mickaël VALLIER. All rights reserved.
//
import Foundation
class Content: NSObject, Codable {
var lesEleves = Set<Eleve>()
var lesEvaluations = Set<Evaluation>()
var lesCompetences = Set<Competence>()
var lesPeriodes : Set<Periode>
override init() {
lesPeriodes = Set()
let defaultPeriode = Periode()
defaultPeriode.nom = "Periode 1"
lesPeriodes.insert(defaultPeriode)
}
}
class Evaluation: NSObject, Codable {
var nom = ""
var date = Date.init()
var sur : Int = 20
var coefficient : Float = 1.0
var laPeriode : Periode
var ramenerSur20 = false
var bonus = false
init(unePeriode : Periode) {
self.laPeriode = unePeriode
super.init()
}
}
class Eleve: NSObject, Codable {
var nom = ""
var prenom = ""
var commentaire : String?
var lesNotes = Set<Note>()
var lesEvalsComps = Set<EvalComp>()
}
class Periode : NSObject, Codable {
var nom = "Periode"
var dateDebut : Date = Date.init()
var dateFin : Date?
var lesEvaluations = Set<Evaluation>()
}
class Competence: NSObject, Codable {
var nom = ""
var exemples = ""
}
class Note: NSObject, Codable {
var lEvaluation : Evaluation
var lEleve : Eleve
var valeur = -1
init(unEleve : Eleve, uneEval : Evaluation) {
self.lEleve = unEleve
self.lEvaluation = uneEval
super.init()
}
}
class EvalComp: NSObject, Codable {
var laCompetence : Competence
var lEleve : Eleve
var lEvaluation : Evaluation
var valeur = ""
init(uneEvaluation : Evaluation, unEleve : Eleve, uneCompetence : Competence) {
self.laCompetence = uneCompetence
self.lEvaluation = uneEvaluation
self.lEleve = unEleve
super.init()
}
}
Si je crée un Eleve, sans qu'il n'y ait d'évaluations, pas de soucis. Si je crée un Eleve alors qu'il existe des évaluations, il y aura une référence à ces évaluations dans l'instance de la classe Eleve, et une référence à l'élève en question dans l'instance de la classe Evaluation, et alors impossible d'encoder (référence "circulaire" non gérée).
J'imagine qu'il y a derrière ça la notion de "clef" des bases de données, et j'aimerais des conseils de "bonne pratique" avant de me lancer ! Si la gestion du problème s'avère très chronophage, je retournerai sur le protocole NSCoding...
Réponses
tl;dr: Utilse CoreData.
Faisons simple :
Codable
est utilisé en grande partie pour coder/décoder les requêtes/réponses des APIs web. Souvent du JSON, dans lequel le concept de référence entre objets n'est pas natif. Tu peux le gérer avec des clés mais c'est une utilisation particulière du protocole par une de ses capacités et donc casse-gueule. C'est pour ça qu'une réponse reçue depuis une API web va contenir toutes les informations pertinentes dont la donnée principale est dépendante, pour éviter de multiplier les appels.C'est bien beau tout ça mais qu'est-ce que ça implique ? Prenons un client qui a n adresses. On stocke les clients dans un coins et les adresses dans l'autre dans une DB quelconque. Quand il y a une requête sur la collection client c'est bien d'envoyer la liste de ses adresses avec, c'est cohérent. Pour les requêtes sur les adresses c'est bien de mettre dans la réponse à quel(s) client(s) elle est rattachée. Là je pense que tu commence à voir l'embrouille. Les clients comportent les adresses, qui comportent à leur tour les clients, qui eux comportent les adresses, qui elles... etc... Bref les réponses JSON (XML si t'es encore en 2010) ne reflètent qu'un angle particulier de la base de donnée et ne peuvent donc pas être utilisés pour la modéliser entièrement. Dans le cadre d'une DB relationnelle, si on prend un schéma de données linéaire la chose devient possible. Dans ton cas si tu te borne à avoir une Liste d'élèves qui comportent une liste de notes c'est bon. Par ex :
Tout ça pour dire que
Codable
par ses origines et sa construction ne peut pas gérer les références et doit être utilisé pour coder/encoder des structures simple.Bref utilise CoreData, c'est un object graph très simple que tu as là et tu n'auras pas de gros soucis à gérer, voir pas de soucis du tout 😉
Je n'ai pas énormément travaillé avec Codable, mais ça sert à convertir des dictionnaires et listes en objets/enums/structs. Et vice-versa: Codable = Decodable & Encodable.
Typiquement ça sert à parser du JSON, qui est un arbre de données; donc sans références circulaires.
Au niveau des solutions:
Pour la base de données, je préfère largement GRDB à Core Data, mais si tu veux utiliser des documents, alors NSPersistentDocument est clairement plus indiquée.
Merci pour ces réponses.
Pour l'idée générale, j'aimerais que les données se trouvent sous forme de fichiers pour l'instant, et certains "réglages" récurrent dans un XML "préférences". L'appli précédente que j'avais bricolée (et qui fonctionnait !!) avait le défaut de stocker toutes les données sous la forme d'une base (devenant assez importante du coup) contenant toutes les classes de tous les niveaux etc. L'idée est ici d'avoir des fichiers séparés qu'on peut stocker facilement dans les bons dossiers de l'ordi.
Donc, si je veux stocker les données sous forme de structure (et plus d'objet héritant de NSObject : j'aimerais atteindre cet objectif!) l'idée la plus simple est de virer la réciprocité des relations ? (j'étais dans l'esprit de conception "coreData" avec les relations one to many et réciproquement). Je vais donc essayer de revoir la couche modèle en la rendant "linéaire".
Si toutefois il m'était indispensable de créer une "UUID", est-ce qu'il y a des recommandations pour ce genre d'attribution ? Un simple Int qu'on incrémente à chaque appel ?
En tous cas je remercie beaucoup les contributeurs de ce forum de prendre le temps de répondre à des "novices" !
Sans avoir la compétence de mes collègues en base de données, je me permet d'ajouter mon grain de sel. Je ne comprend pas pourquoi il y a une référence à l'élève dans la classe Evaluation.
Une évaluation est la "propriété" d'un élève, pas le contraire. Est-ce pour des raisons techniques, pour faciliter l'affichage et l'édition quelque part dans l'application ?
Ce que tu décris ici est assez simple:
UserDefaults
C'est intéressant à plus d'un titre ce projet, ça ferait très bon exercice. Je suis sûr qu'entre
CoreData
,NSBinding
,NSUserDefaultsController
et Interface Builder on peut le produire sans écrire la moindre ligne de code.Bonjour Draken,
En fait, chaque élève maintient une relation vers un set de notes, et chaque note maintient une relation vers l'élève et aussi vers l'évaluation (ce qui permet de retrouver des infos sur le coefficient de l'éval, si c'est un bonus etc..), l'évaluation elle-même maintenant un lien vers les notes correspondantes (sur le code, il manque d'ailleurs pour l'instant ce lien). L'avantage de cette structure est qu'on a un accès direct aux notes d'un élèves d'une part, et on peut aussi d'autre part retrouver les notes liée à une évaluation. Si par exemple je supprime un élève, je peux retrouver toutes ses notes et les supprimer, de même si je supprime une évaluation. Avec Core Data, j'avais construit ainsi le modèle, et ça marchait pas mal.
Je vais essayer de penser autrement le modèle pour le "linéariser" et peut-être le rendre plus simple au final. La difficulté sera de retrouver les liens entre les objets : s'il faut refaire tout un travail pour relier les bons objets entres eux au décodage du fichier, c'est effectivement peu judicieux d'utiliser ce protocole Codable.
@Pyroh,
Oui je pense que je vais retourner à Core Data. Je voulais écrire "en vrai" ma couche modèle pour éviter la "boite noire" core Data, mais finalement c'est peut-être le plus efficace.
Tu pourrais créer une base de données SQLite pour chaque document, mais en pratique c'est chiant à cause de la gestion des version des documents introduite dans macOS 10.7. NSPersistentDocument le fait, mais au prix d'un tas de bidouilles que seule Apple peut se permettre de faire. Moi, j'avais fini par désactiver la gestion des versions, mais tout de même il y avait encore plein de gestion à faire.
On peut même imaginer enregistrer en JSON et utiliser une base de données SQLite 'in-memory'. Ce ne sont pas les solutions qui manquent, plutôt le temps de les implémenter. (je précise que je ne te conseille pas particulièrement cette approche).
Oui et non. Dans le schéma Core Data, on crée des relations, mais au final, c'est bien une base de données SQLite dessous. Aussi, chaque Entité aura sa propre table, et il y aura aussi des tables pour faire des jointures entre les tables d'entités. Tu peux ouvrir ton fichier Core Data avec un éditeur SQLite pour t'en convaincre.
Il se trouve que Core Data est une ORM (Object-Relational database Mapping), et fait le boulot pour toi afin que tu puisses travailler avec des objets. Mais au final, chaque instance de l'entité a un id (clef primaire) qui l'identifie de façon unique.
Mais évidemment, avec une base de données on peut faire des relations one-to-many ou même many-to-many. C'est juste qu'il faudra lancer des requêtes sur la base, et de fait, ça apparait moins magique — dans le bon et le mauvais sens du terme.
Un UUID (Universal Unique Id) est typiquement créé par la classe (NS)UUID. C'est une chaîne de caractère suffisamment longue pour que les risques de collision soient infimes.
Mais pour mieux répondre à ta question, pour identifier les 'lignes' de la base de données il nous faut des clefs primaires, qui sont habituellement des entiers incrémentés automatiquement à l'insertion. On utilise plutôt les UUID quand on doit échanger des données avec une autre machine, parce qu'elle a sa propre base de données. Par exemple, un élève peut être l'enregistrement 18 sur ta machine mais l'enregistrement 804 sur le serveur, en utilisant un UUID, on est sûr que c'est le même élève.
Je n'appellerais pas ça des questions ne novice. Nous sommes en plein dans les choix architecturaux. C'est toujours une affaire de compromis.
J'ai beau détester Core Data, je dirais que c'est l'outil le plus adapté ici, parce que NSPersistentDocument va faire une grosse partie du travail pour toi. Et c'est important pour un projet sur lequel tu vas travailler en dilettante.
Bon, finalement, je me suis rabattu sur le protocole NSCoding : j'ai rendu les classes modèles conformes en ajoutant les fonctions init (coder aDecoder:NSCoder) et encode(with aCoder:NSCoder) à chaque classe. Par contre, c'est super lourd d'écrire ces bouts de code quasi-identiques pour chaque classe. (D'où peut-être l'utilité de CoreData...) Dans la sous-classe NSDocument, j'utilise la classe NSKeyedArchiver/NSKeyedUnarchiver pour encoder le "rootObject".
Ca fonctionne très bien ainsi, et je crois que je vais rester là-dessus. Je voulais repartir sur des structures swifts "natives" et pas sur des NSObjects, mais ce n'est pas vraiment possible.
J'ai quand même évité CoreData, même si j'aurais effectivement pu l'utiliser, pour éviter de reprendre tout le projet.
Merci pour ces réponses en tous cas !
Voici la partie Model
En fait, quand on rend une classe conforme à Codable, le compilateur génère secrètement du code pour accéder aux champs.
Il y a des gens qui utilisent un générateur de code (comme Sourcery) pour implémenter NSCoding de la même façon. Évidemment, pour un petit projet ça n'en vaut pas la peine.