Question de débutant en core data
Bonjour à tous,
(don't judge me, I'm just a beginner)...
Je me frotte pour la première fois à Core Data en essayant de faire une petite appli perso en guise d'exercice. Je bloque sur un point assez simple. J'ai parcouru une tonne d'exemples et de docs sur core data, et je n'ai toujours pas trouvé d'exemple analogue à mon problème pourtant assez basique.
Je voudrais donc vos conseils...
Je vous décris mon modèle (en simplifié) :
1) j'ai une entité "Fabriquant", une entité "Vêtement" (avec une relation 1-to-many qui va bien). Je les affiche dans des arrayView, etc. Jusqu'ici tout baigne. Je gère les ajouts/modifs/suppressions/sélection.
2) J'ai aussi une entité "infosDuStock" qui contient un prix d'achat, un prix de vente, la quantité dispo et la date de dispo éventuelle.
C'est ce que je compte afficher, sauf que ça ne dépend pas QUE de mes deux entités : je dois croiser ça avec une autre donnée : la taille (XS,S,M,L,XL) qui est affichée dans un popup menu (et qui marche bien aussi). J'ai donc créé une quatrième entité "Taille" avec une relation (à sens unique) de "infosDuStock" vers "Taille".
Ce que je veux (c'est assez évident, en fait) :
Je veux pouvoir cliquer sur un vêtement (disons un pull rouge), choisir sa taille (XL) dans le popup menu, et voir dans une troisième vue dédiée les infos du stock sur ce modèle (donc l'instance de mon entité infosDuStock qui rassemble ces deux critères).
Et donc, voilà ma question de débutant (je vous avais prévenus !):
Est-ce que je dois faire un controller custom juste pour pouvoir fetcher l'entité qui répond aux deux critères (vêtement+taille) ou bien est-ce que je peux gérer ce genre de requête croisée mais très simple (c'est juste une intersection d'ensembles) via un prédicat ou autre que je pourrais définir dans l'interface ?
Merci d'avance.
Om Nom
Réponses
En gros, NSPredicate peut s'utiliser à deux occasions:
1) quand on va récupérer les données dans le Managed Object Context (MOC) en lançant une NSFetchRequest.
2) quand on dispose d'un ensemble de données (NSArray, NSSet, etc.). Par exemple, il y a une méthode -[NSSet filteredSetUsingPredicate:].
La première technique est plus rapide et consomme moins de mémoire, puisque sous le capot, le prédicat ajuste la requête SQL.
Je ne sais pas de quel contrôleur du parle. Si c'est un NSArrayController, je te conseille de NE JAMAIS le sous-classer. On peut binder une "Filter Predicate" dessus, ça devrait convenir.
Si tu parles du NSViewController ou NSWindowController, il te faudra effectivement un système pour changer le prédicat selon la sélection du pop-up. Le plus facile est sans doute d'échanger l'instance de NSPredicate quand sa méthode d'action est appelée.
Effectivement je pensais à un NSArrayController. L'idée d'utiliser un view/window controller ne m'avait même pas effleuré l'esprit (mais au vu de ta réponse, ça me conforte dans l'idée que ce ne serait pas le plus adapté).
En tous cas, merci de ta réponse. Maintenant que je sais qu'un prédicat bindé sur un NSArrayController peut résoudre mon souci, je vais pouvoir me lancer sur cette voie sans avoir peur de me perdre dans une impasse.
Ce que je te dis de faire, c'est de tirer une action depuis le NSPopUpButton vers un contrôleur. Ce contrôleur peut être une sous-classe de NSDocument, NSWindowController, NSViewController, ou même NSObject, peu importe. Il faut juste que l'action change le binding "Filter Predicate".
Dans une vraie appli, on ne met pas tout le code dans la sous-classe de NSDocument ou dans l'AppDelegate, autrement, elle devient énorme et ingérable. On utilise donc NSViewController et NSWindowController pour séparer.
Ok, j'avais mal compris.
Dans ta première réponse, j'avais cru que je pouvais me contenter d'un seul prédicat qui récupère "dynamiquement" la valeur associée au popupbutton à chaque évaluation (l'équivalent d'un join SQL, ce qui signifiait qu'une fois défini, il n'était pas utile de le modifier).
Avec tes précisions, je comprends mieux le principe : définir un prédicat par taille (S,M,L...), et à chaque changement du popup, je change mon filtre pour celui qui correspond à la taille sélectionnée.
ça me parait effectivement assez simple à réaliser. Pour ce qui est du contrôleur, je pense que ce sera un simple héritage de NSObject car de prime abord je ne vois pas ce que m'apporteraient les autres classes dans le cas présent.
Merci de tes réponses, en tous cas.
Om Nom
Alors, je reviens avec le résultat ! ^_^
J'ai bien suivi tes conseils : j'ai créé un NSPredicate que j'ai bindé au NSArrayController qui gère mes entités infosDuStock (via la partie "Filter Predicate" de l'interface) et lorsque mon menu popup change, ça déclenche une action qui adapte mon prédicat pour obtenir la taille choisie.
ça fonctionne bien : le filtre est bien mis à jour en fonction de la taille choisie (S, M, L, etc) et je me retrouve donc avec une sélection réduite à un élément.
En revanche, il me reste encore une étape: même si l'élément est unique, il n'est pas sélectionné automatiquement.
En clair: si mon popup est sur une valeur (disons la taille M) et que j'ai sélectionné un t-shirt dans la tableView, je vois bien tous les champs de l'instance infosDuStock en question. Si je demande alors à regarder les infos pour la taille L, le prédicat s'adapte, mais ces champs que j'observais en taille M deviennent vides (Not selected). Il faut systématiquement que je relique sur mon t-shirt (qui est pourtant toujours sélectionné) pour que les différents champs se remplissent avec les nouvelles valeurs correspondant au prédicat.
Une idée pour "forcer" le rafraichissement (ou plus précisément sélectionner tout de suite l'unique élément retourné par mon prédicat) ?
Bon, merci encore pour tes réponses. Je me suis débattu un temps avec mes controllers, en appliquant ton conseil, puis à force de ne pas arriver à forcer ma sélection, j'ai fini par changer légèrement mon design pour avoir un truc plus propre, et qui ne me pose pas ces problèmes de sélection, c'est déjà ça.
Et là ... j'ai une nouvelle question de débutant (encore plus neuneu encore que la précédente, mais tant pis : j'assume)
Disons que je veux remplir mon modèle à partir de données parsées dans un fichier d'import.
J'arrive sans problème à remplir mon premier niveau (Fabriquant) avec une ligne de ce type :
var fabriquant:NSManagedObject = NSEntityDescription.insertNewObjectForEntityForName("Fabriquant", inManagedObjectContext: self.managedObjectContext) as NSManagedObject
Mais dès que je veux entrer un niveau en dessous (j'ai une relation 1-to-many vers l'entité "Vêtement"), je n'arrive pas à trouver le bon contexte pour faire la même chose...
ça compile sans souci mais à l'exécution, il jette une exception pour dire qu'il ne trouve pas l'entité "Vêtement" dans le contexte !
Tout semble pourtant bien renseigné...
J'ai essayé plusieurs managedObjectContext, de self.managedObjectContext jusqu'à ceux des controllers, et c'est toujours la même rengaine. D'où ma question, comment créer une instance de mon entité sans recevoir systématiquement nil ?
Pour mémoire, j'ai deux table view qui affichent respectivement le nom du fabriquant et le nom du vêtement. Chacune de ces vues est bindée à un NSArrayController (Disons FabriquantCtrl et VetementCtrl), lequel est bindé sur l'entité qui lui correspond.
J'aurais bien appelé l'action "add" de mes NSArrayController, mais ça m'oblige à retrouver d'abord l'objet fraichement ajouté pour lui attribuer ses valeurs... L'avantage de ma méthode au-dessus est d'avoir tout de suite un objet de ma classe pour travailler dessus.
Bon, même si je ne sais toujours pas pourquoi ça ne fonctionnait pas avec la syntaxe ci-dessus, ça semble mieux marcher si je passe par cette syntaxe :
let moc = self.managedObjectContext!
var fabriquantEntity:NSEntityDescription =
moc.persistentStoreCoordinator.managedObjectModel.entitiesByName["Fabriquant"] as NSEntityDescription
var fabriquant:NSManagedObject = NSManagedObject(entity: fabriquantEntity, insertIntoManagedObjectContext: moc)
var vetementEntity:NSEntityDescription =
moc.persistentStoreCoordinator.managedObjectModel.entitiesByName["Vetement"] as NSEntityDescription
var vetement:NSManagedObject = NSManagedObject(entity: vetementEntity, insertIntoManagedObjectContext: moc)
Euh non, ça c'est juste moi qui ai accentué sans faire gaffe le nom de l'entité en rédigeant mon post.
Les noms ne sont pas accentués dans mes codes (malgré swift, il me faudra probablement quelques années avant de perdre cette vieille habitude).
Je ne saisis pas. Comment peux-tu avoir plusieurs MOC ?
Si tu utilises un NSPersistentDocument, il y a effectivement un MOC par document, mais pas plus.
Est-ce que tu peux nous montrer une capture d'écran du modèle ?
Il peut effectivement y avoir plusieurs MOC pour un document, ou plus généralement pour un store. Le schéma classique est d'avoir un MOC principal, dans la main queue, qui se synchronise avec l'affichage, et un MOC "enfant" du MOC principal dans une autre queue.
Je l'ai modifié depuis, mais j'ai encore mieux : hier soir, j'ai voulu en avoir le coe“ur net en créant un projet vide en en faisant un modèle simplissime, pour savoir si le problème se reproduisait.
Bon, pour ledit modèle, je ne me suis pas cramé les neurones: il y a un entité Mois et une entité Jour, avec toutes deux un nom (en String) et pour le mois un numéro (Int16). J'ai donc créé un modèle avec deux entités (ayant chacune un ou deux attributs) et une relation 1-to-many (réversible) entre les deux entités. Voici la capture :
J'ai ensuite posé un simple bouton, bindé à une IBAction dans mon App Delegate qui devait créer des entités avec la première méthode évoquée ci-dessus, à savoir :
var mois1:NSManagedObject = NSEntityDescription.insertNewObjectForEntityForName("Mois", inManagedObjectContext: self.managedObjectContext) as NSManagedObject
Verdict: même problème. Je clique et je retrouve le même message d'erreur :
2014-08-23 23:31:18.528 testContext[6072:303] +entityForName: could not locate an entity named 'Mois' in this model.
Par contre, c'est grâce à ce projet de test que j'ai fini par arriver à l'autre méthode qui, elle, fonctionne bien. Donc, de deux choses l'une: soit je n'ai pas compris comment marche la méthode insertNewObjectForEntityForName (ce qui n'est pas exclu, vu que je cumule bravement une semaine d'expérience pratique en Core Data), soit elle a un souci sous Swift (ou avec Xcode 6).
Om Nom
Oui j'ai effectivement vu des infos similaires en cherchant des réponses à mes questions.
Dans mon cas, c'était un peu flou pour moi (particulièrement à l'heure où j'ai rédigé mon message de cette nuit) et je voulais juste dire que j'ai tenté de changer le moc passé en paramètre inManagedObjectContext en essayant successivement des attributs de l'app delegate, de mes controllers, etc.
Mais bon c'est aussi parce que moi, je lis aussi un peu de travers quand on approche d'une heure avancée de la nuit. C'est seulement maintenant que je réalise que le message parlait du model et non du context...
Une piste ?
La relation mois dans l'entité Ville est en minuscule, mais l'entité a bien la majuscule. A moins que tu parles d'un autre endroit ?
Sinon, je ne sais pas s'il y a des conventions de nommage pour les entité et les relations... mais ce n'est peut-être pas très judicieux de leur avoir mis des noms aussi proches... La lisibilité risque d'être difficile d'ici quelques mois !
Concernant ton problème, le message dit qu'il ne trouve pas ton entité. As tu bien initialisé ton MOC ? Montre ton code d'initialisation. çà doit se trouver de ce côté là ...
Il n'y a pas de conventions obligatoire mais c'est "normal" d'utiliser le même nom pour les propriétés et les relations que pour pour les classes.
Bonne question :-*
Hmmmm... c'est peut-être bien là que se trouve le souci, alors.
Je me suis contenté de l'initialisation faite par défaut quand on coche "Core Data" à la création du projet.
En fait, j'ai suivi des étapes analogues à ce que j'ai appris avec Lynda.com (le cours "Core Data for iOS ans OS X de Simon Allardice), et je ne me souviens pas d'avoir vu des changement dans l'initialisation du MOC.
Sachant que le cours date un peu (2012) et que je transpose en Swift par choix personnel, il y a des trucs supplémentaires à faire ?
Om Nom
Ceci étant, la seconde méthode consistant à récupérer d'abord la NSEntityDescription (via moc.persistentStoreCoordinator.managedObjectModel.entitiesByName) avant de créer le NSManagedObject fonctionne.
Est-ce que ça ne signifie pas que le moc est quand même correctement initialisé ?
Om Nom
Par contre, tu veux dire quoi par "supplémentaire" ? Par rapport à quoi ? Qu'as tu mis comme init de ton MOC ? C'est en général dans le AppDelegate...
Si tu ne montres pas ton code, ce sera difficile de t'aider !!!
Je crois que j'ai trouvé quelque chose de soucis dans le code défaut des projets Core Data en Swift.
Est-ce que tu as utilisé le template Master/Detail et, est-ce que le code, que tu as montré ici, se trouve dans un des viewControllers ? Si oui, il y a des grosses erreurs dans le code des viewControllers.
Je vais redigier une version plus correcte et le poster ici.
Après que j'ai relis le code défaut dans MasterViewController.swift, je pige plus ce qu'ils ont fait là . Les grosses erreurs ne s'y trouves pas mais le code est assez confus et difficile à suivre.
@Om Nom - S'il te plaà®t répondre à mes questions en montrant ton code, surtout des modifications que tu as fait et dans quelle classe tu les as fait, et je pourrais avoir une solution
C'est pas seulement le code dans le AppDelegate - il y en a plus dans les ViewControllers qu'il faut démêler
Mea culpa, j'aurais dû être plus précis: c'est pour une appli OSX, pas iOS. Donc, pas de Master/Detail, juste le template Cocoa Application, dans lequel j'ai coché Core Data et rien d'autre (ni Storyboard ni Document-based).
Pour mon appli "bouton" minimaliste (celle où j'ai juste les entités "Mois" et "Jour" mais où je reproduis le message d'erreur), j'ai simplement créé mon modèle (celui dont j'ai fait la capture d'écran un peu plus haut) puis j'ai ajouté un simple push button sur ma fenêtre principale.
A partir de ce bouton dans IB, j'ai tiré une action vers l'Application Delegate (AppDelegate.swift) que j'ai définie ainsi :
class AppDelegate: NSObject, NSApplicationDelegate {
@IBAction func fillMonths(sender: AnyObject) {
var mois1:NSManagedObject = NSEntityDescription.insertNewObjectForEntityForName("Mois", inManagedObjectContext: self.managedObjectContext) as NSManagedObject
}
}
Je n'ai rien fait d'autre que ça. Et dès que je clique j'obtiens ça dans la console :
+entityForName: could not locate an entity named 'Mois' in this model.
Possible que j'ai loupé un truc essentiel, mais pour moi, ça ne devrait pas produire d'erreur normalement. En tous cas, dans le cours de Lynda.com, je n'ai pas vu d'autre étape essentielle pour démarrer...
Om Nom
Il est initialisé comment le self.managedObjectContext ?
Dans le code fourni par défaut par le template d'Apple :
lazy var managedObjectContext: NSManagedObjectContext? = {
// Returns the managed object context for the application (which is already bound to the persistent store coordinator for the application.) This property is optional since there are legitimate error conditions that could cause the creation of the context to fail.
let coordinator = self.persistentStoreCoordinator
if !coordinator {
return nil
}
var managedObjectContext = NSManagedObjectContext()
managedObjectContext.persistentStoreCoordinator = coordinator
return managedObjectContext
}()
Et le Store Coordinator est bien initialisé avec le bon modèle ?
Et juste par acquis de conscience, as tu essayé :
- de remettre à zéro le build de ta cible sous Xcode (Clean Target : Command-Majuscule-K)
- de mettre ton application à la corbeille avant de la reconstruire (pour être sûr de repartir d'un store tout neuf)