Comprendre les différences entre votre modèle de données de base et les objets gérés

Publié par donnywals en octobre 5, 2020

Vous avez peut-être remarqué que lorsque Xcode génère vos classes NSManagedObject en fonction de votre fichier de modèle de données de base, la plupart des propriétés de votre objet géré sont facultatives. Même si vous les avez obligatoires dans l’éditeur de modèle, Xcode générera un objet géré où la plupart des propriétés sont facultatives.

Dans cet article, nous allons explorer ce phénomène et pourquoi il se produit.

Exploration des sous-classes NSManagedObject générées

Lorsque vous construisez un projet qui utilise la génération automatique de code de Xcode pour les modèles de données de base, vos sous-classes NSManagedObject sont générées lorsque vous construisez votre projet. Ces classes sont écrites dans le dossier de données dérivées de votre projet et vous ne devez pas les modifier directement. Les modèles générés par Xcode auront des propriétés facultatives pour certaines des propriétés que vous avez ajoutées à votre entité, que vous ayez ou non rendu la propriété facultative dans l’éditeur de modèle.

Bien que cela puisse paraître étrange au début, ce n’est en fait pas si étrange. Jetez un œil à la sous-classe NSManagedObject suivante:

extension ToDoItem { @nonobjc public class func fetchRequest() -> NSFetchRequest<ToDoItem> { return NSFetchRequest<ToDoItem>(entityName: "ToDoItem") } @NSManaged public var completed: Bool @NSManaged public var label: String?}

L’une des deux propriétés de mon ToDoItem est facultative même si elles sont toutes deux requises dans l’éditeur de modèle. Lorsque je crée une instance de this ToDoItem, j’utiliserais le code suivant:

let item = ToDoItem(context: managedObjectContext)

L’initialiseur d’un objet géré prend un contexte d’objet géré. Cela signifie que je n’attribue pas de valeur aux propriétés gérées lors de l’initialisation du ToDoItem. L’impression de la valeur des propriétés label et completed donne un résultat intéressant:

print(item.label) // nilprint(item.completed) // false

Alors que label vaut nil comme prévu, Core Data a attribué une valeur par défaut de false à la propriété completed, ce qui est logique car Xcode a généré une propriété non facultative pour completed. Allons plus loin et jetons un coup d’œil au code suivant:

let item = ToDoItem(context: managedObjectContext)item.label = "Hello, item"print(item.label) // "Hello, item"print(item.completed) // falsedo { try managedObjectContext.save()} catch { print(error)}

Lorsque vous exécutez ce code, vous constaterez qu’il produit la sortie suivante:

Optional("Hello, item")falseError Domain=NSCocoaErrorDomain Code=1570 "completed is a required value." UserInfo={NSValidationErrorObject=<CoreDataExample.ToDoItem: 0x60000131c910> (entity: ToDoItem; id: 0x600003079900 <x-coredata:///ToDoItem/t1FABF4F1-0EF4-4CE8-863C-A815AA5C42FF2>; data: { completed = nil; label = "Hello, item";}), NSValidationErrorKey=completed, NSLocalizedDescription=completed is a required value.}

Cette erreur indique clairement completed is a required value., ce qui implique que completed n’est pas défini, et l’objet géré imprimé affiché à côté du message d’erreur indique également que completed vaut nil. Ainsi, bien qu’il existe une sorte de valeur par défaut pour completed, elle n’est pas considérée comme non-nil tant qu’elle n’est pas explicitement assignée.

Pour comprendre ce qui se passe, nous pouvons attribuer une valeur à completed et jeter un œil à la description imprimée de item:

let item = ToDoItem(context: managedObjectContext)item.label = "Hello, item"print(item.completed)print(item)

Ce code produit la sortie suivante:

false<CoreDataExample.ToDoItem: 0x6000038749b0> (entity: ToDoItem; id: 0x600001b576e0 <x-coredata:///ToDoItem/tD27C9C9D-A676-4280-9D7C-A1E154B2AD752>; data: { completed = 0; label = "Hello, item";})

C’est assez intéressant, non ?

La propriété completed est définie comme un Bool, mais elle est imprimée sous forme de nombre. Nous pouvons trouver la raison de cela dans le magasin SQLite sous-jacent. Le moyen le plus simple d’explorer le fichier SQLite de votre magasin de données principal consiste à passer -com.apple.CoreData.SQLDebug 1 comme argument de lancement à votre application et à ouvrir le fichier SQLite auquel Core Data se connecte dans un explorateur SQLite tel que le navigateur de base de données SQLite.

Astuce:
En savoir plus sur les arguments de lancement de Core Data dans cet article.

Lorsque vous regardez la définition du schéma pour ZTODOITEM, vous constaterez qu’il utilise INTEGER comme type pour ZCOMPLETED. Cela signifie que la propriété completed est stockée sous forme d’entier dans le magasin SQLite sous-jacent. La raison pour laquelle completed est stocké sous la forme d’un INTEGER est simple. SQLite n’a pas de type BOOLEAN et utilise une valeur INTEGER de 0 pour représenter false, et 1 pour représenter true à la place.

Le data que vous voyez imprimé lorsque vous imprimez votre instance d’objet géré n’est pas la valeur de votre propriété completed, c’est la valeur de completed qui sera écrite dans le magasin SQLite.

Il y a deux choses à apprendre de cette section.

Tout d’abord, vous savez maintenant qu’il existe un décalage entre l’optionnalité de votre modèle de données de base défini et les objets gérés générés. Un String non optionnel est représenté par un String optionnel dans votre modèle généré tandis qu’un Bool non optionnel est représenté par un Bool non optionnel dans votre modèle généré.

Deuxièmement, vous avez appris qu’il existe une différence entre la façon dont une valeur est représentée dans votre modèle d’objet géré et la façon dont elle est représentée dans le magasin SQLite sous-jacent. Pour voir quelles valeurs sont utilisées pour écrire votre instance d’objet géré dans le stockage sous-jacent, vous pouvez imprimer l’objet géré et lire le champ data dans la sortie imprimée.

La principale leçon ici est que votre modèle de données de base dans l’éditeur de modèle et vos sous-classes d’objets gérés ne représentent pas les données de la même manière. Facultatif dans votre modèle de données de base ne signifie pas toujours facultatif dans votre sous-classe d’objets gérés et vice versa. Une valeur non facultative dans votre modèle de données de base peut être représentée comme une valeur facultative dans votre sous-classe d’objets gérés. Core Data validera votre objet géré par rapport à son modèle d’objet géré lorsque vous tentez de l’écrire dans le magasin persistant et génère des erreurs s’il rencontre des erreurs de validation.

Alors pourquoi cette inadéquation existe-t-elle? Ne serait-il pas beaucoup plus facile si le modèle d’objet géré et les sous-classes d’objets gérés avaient un mappage direct?

Comprendre le décalage entre les objets gérés et le modèle de données de base

Une grande partie de la raison pour laquelle il existe un décalage entre vos objets gérés et le modèle que vous avez défini dans l’éditeur de modèle provient des racines Objective-C de Core Data.

Étant donné que Objective-C ne traite pas du tout de Optional, il n’y a pas toujours un bon mappage de la définition du modèle au code Swift. Souvent, la façon dont la cartographie fonctionne semble quelque peu arbitraire. Par exemple, Optional<String> et Optional<Bool> ne peuvent pas être représentés en tant que type dans Objective-C pour la simple raison que Optional n’existe pas dans Objective-C. Cependant, Swift et Objective-C peuvent interagir et Optional<String> peut être automatiquement relié à un NSString. Malheureusement, Optional<Bool> ne peut être mappé à rien dans Objective-C automatiquement car Xcode vous le dira lorsque vous tenterez de définir une propriété @NSManaged comme Bool?.

Si vous n’avez jamais travaillé avec Objective-C, il peut vous sembler très étrange qu’il n’y ait pas de concept de Optional. Comment les gens utilisaient-ils les propriétés optionnelles dans Core Data avant Swift ? Et que se passe-t-il quand quelque chose est censé être nil dans Objective-C?

Dans Objective-C, il est parfaitement correct que n’importe quelle valeur soit nil, même lorsque vous ne vous y attendez pas. Et puisque les données de base ont leurs racines dans Objective-C, une partie de cet héritage est transmise à vos classes Swift générées d’une manière parfois moins qu’idéale.

Le point à retenir le plus important ici n’est pas comment fonctionne Objective-C, ni comment Xcode génère exactement le code. Au lieu de cela, je veux que vous vous souveniez que les types et la configuration de votre définition de modèle de données de base ne (doivent) pas correspondre aux types de votre sous-classe d’objets gérés (générés).

En résumé

Dans l’article de cette semaine, vous avez beaucoup appris sur la façon dont vos sous-classes d’objets gérés et la définition du modèle de données de base ne s’alignent pas toujours comme vous les attendez. Vous avez vu qu’une propriété non facultative dans l’éditeur de modèle peut parfois devenir facultative dans la sous-classe d’objets gérés générés, et d’autres fois, elle se retrouve comme une propriété non facultative avec une valeur par défaut même si vous n’avez pas attribué de valeur par défaut vous-même.

Vous avez également vu que si une valeur par défaut est présente sur une instance d’objet géré, cela ne signifie pas que la valeur est réellement présente au moment où vous enregistrez votre objet géré, sauf si vous avez explicitement défini une valeur par défaut dans l’éditeur de modèle de données de base.

Bien que cela soit certainement déroutant et malheureux, Core Data est assez bon pour vous dire ce qui ne va pas dans les erreurs qu’il génère lors de l’enregistrement d’un objet géré. Il est également possible d’inspecter les valeurs que Core Data tentera de stocker en imprimant votre instance d’objet géré et en inspectant son attribut data.

Sur une note personnelle, j’espère que le comportement que j’ai décrit dans l’article de cette semaine sera abordé dans une future mise à jour des données de base qui le rendra plus convivial lorsque les sous-classes d’objets gérés ont un mappage plus proche, éventuellement direct au modèle de données de base défini dans un éditeur de modèle. Mais jusque-là, il est important de comprendre que l’éditeur de modèle et vos sous-classes d’objets gérés ne représentent pas votre modèle de la même manière, et que cela est au moins partiellement lié aux racines Objective-C de Core Data.

Si vous avez des questions, des corrections ou des commentaires à propos de cet article, veuillez me le faire savoir sur Twitter. Cet article fait partie des recherches, de l’exploration et de la préparation que je fais pour un livre sur les données de base sur lequel je travaille. Pour les mises à jour sur ce livre, assurez-vous de me suivre sur Twitter. Je prévois actuellement de sortir le livre vers la fin de 2020.

Restez à jour avec ma newsletter hebdomadaire

Moissonneuse-batteuse pratique

Apprenez tout ce que vous devez savoir sur la moissonneuse-batteuse et comment l’utiliser dans vos projets avec mon nouveau livre Moissonneuse-batteuse pratique. Vous aurez treize chapitres, un terrain de jeu et une poignée d’exemples de projets pour vous aider à démarrer avec Combine dès que possible.

Le livre est disponible en téléchargement numérique pour seulement 29,99 $!

Obtenez une combinaison pratique

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée.