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


 


«1

Réponses

  • CéroceCéroce Membre, Modérateur
    Il faut effectivement utiliser un prédicat.

    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.

    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 ?

    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.

  • CéroceCéroce Membre, Modérateur

    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é).

    Attention, je ne dis rien de tel. Pour nourrir une NSTableView, il y a deux possibilités: soit utiliser une datasource, soit utiliser les bindings. Comme tu as utilisé les bindings, je te conseille de binder un NSPredicate au NSArrayController.

    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


  • Om NomOm Nom Membre
    août 2014 modifié #6

    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) ?


  • CéroceCéroce Membre, Modérateur
    août 2014 modifié #7
    La NSTableView et le NSArrayController possèdent tous les deux une notion de sélection. En l'occurence, c'est celle du NSArrayController qui fait foi. Il te faut binder la sélection de la tableview sur celle du array controller. Je ne suis pas sûr que ça règle totalement le problème, mais tu y verras plus clair, puisque le problème sera alors à  régler du côté du array controller.
  • 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.


     

    Des idées ?

     

    Merci d'avance

     

    Om Nom

  • 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)


     


    en tous cas, je Core Data continue à  me botter, même si je trouve qu'il y a de drôles de barrières à  l'entrée... 

  • Joanna CarterJoanna Carter Membre, Modérateur
    C'était pas à  cause de l'accent au-dessus le e en Vêtement ?
  • 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).


  • CéroceCéroce Membre, Modérateur


     



    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 ?





    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.

  • Joanna CarterJoanna Carter Membre, Modérateur

    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).




    Est-ce que tu peux nous montrer une capture d'écran du modèle ?


  • 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.




     


    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.



  • Est-ce que tu peux nous montrer une capture d'écran du modèle ?




     


    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 :


     


    348326modele.png


     


    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

  • Om NomOm Nom Membre
    août 2014 modifié #16


    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.




     


    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...  :*


  • Je note que tu as mis une majuscule à  "Mois" dans ton code, mais pas dans ton modèle...

    Une piste ?


  • Je note que tu as mis une majuscule à  "Mois" dans ton code, mais pas dans ton modèle...

    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 ?

  • Non, autant pour moi j'ai regardé la relation au lieu de l'entité !
  • 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à ...


  • Joanna CarterJoanna Carter Membre, Modérateur


    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 !




     


    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.


     




    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à ...




     


    Bonne question  :-*



  • 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


  • Swift ? Connais pas !! Et j'ai pas l'intention d'apprendre avant quelques mois...


    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 !!!
  • Joanna CarterJoanna Carter Membre, Modérateur
    août 2014 modifié #25

    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.


  • Joanna CarterJoanna Carter Membre, Modérateur
    août 2014 modifié #26

    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


  • Joanna CarterJoanna Carter Membre, Modérateur


    Qu'as tu mis comme init de ton MOC ? C'est en général dans le AppDelegate...




     


    C'est pas seulement le code dans le AppDelegate - il y en a plus dans les ViewControllers qu'il faut démêler  :*



  • 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.




     


    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).


     




    @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




     


    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 {


                                


    // mon code commence ici 

        @IBAction func fillMonths(sender: AnyObject) {



                var mois1:NSManagedObject = NSEntityDescription.insertNewObjectForEntityForName("Mois", inManagedObjectContext: self.managedObjectContext) as NSManagedObject   


        }



    // mon code finit ici 

     

    }


     


    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 ?




  • 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)


Connectez-vous ou Inscrivez-vous pour répondre.