IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)
Navigation

Inscrivez-vous gratuitement
pour pouvoir participer, suivre les réponses en temps réel, voter pour les messages, poser vos propres questions et recevoir la newsletter

ORM PHP Discussion :

Relation n-n réflexive & updates sous symfony


Sujet :

ORM PHP

Vue hybride

Message précédent Message précédent   Message suivant Message suivant
  1. #1
    Membre émérite Avatar de Herode
    Homme Profil pro
    Développeur Web
    Inscrit en
    Mars 2005
    Messages
    825
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Savoie (Rhône Alpes)

    Informations professionnelles :
    Activité : Développeur Web
    Secteur : High Tech - Multimédia et Internet

    Informations forums :
    Inscription : Mars 2005
    Messages : 825
    Par défaut Relation n-n réflexive & updates sous symfony
    Bonjour à tous,

    je suis toujours sur mes problèmes de relations n-n réflexives. Il s'agit ici de produits croisés : un produit peut pointer 0-n autres produits (qui seront proposés comme produits similaires au visiteur du site).
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
     
    Product:
      columns:
        name:
          type: string(45)
          notnull: true
      relations:
        Links:
          refClass: ProductLink
          class: Product
          local: product_id
          foreign: linked_product_id
     
    ProductLink:
      columns:
        product_id:
          type: integer
          primary: true
          notnull: true
        linked_product_id:
          type: integer
          primary: true
          notnull: true
      relations:
        LinkingProduct:
          class: Product
          local: product_id
          foreignAlias: AsLinker
          onDelete : cascade
          onUpdate : cascade
        LinkedProduct:
          class: Product
          local: linked_product_id
          foreignAlias: AsLinked
          onDelete : cascade
          onUpdate : cascade
    Avec ce schéma, Symfony me génère des modèles et des formulaires qui me vont bien, je peux faire mes associations. Mais j'ai besoin de rendre la relation symétrique : si un produit P1 est lié à un produit P2, alors P2 doit être aussi lié à P1. Pour y arriver, mon idée était de surcharger le doSave() du formulaire pour y ajouter les ProductLinks ad hoc et sauvegarder tout ça. La tentative plante lamentablement, je désactive mon code, je mets des traces dans le doSave() :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
     
        protected function doSave($conn = null) {
    	sfContext::getInstance()->getLogger()->debug(__METHOD__ . " avant parent::doSave ligne " . __LINE__);
    	parent::doSave($conn);
    	sfContext::getInstance()->getLogger()->debug(__METHOD__ . " après parent::doSave ligne " . __LINE__);
    	$product = $this->getObject();
     
    	// La relation 'produits croisés' est commutative : on s'assure donc que les entrées symétriques
    	// à celles du formulaire existent : si #6 est lié à #7, #7 sera lié à #6.
    	$linkedProducts = $product->getLinks();
    	foreach ($linkedProducts as $linked) {
    	    $lp = $product->createLinker($linked);
    	    sfContext::getInstance()->getLogger()->debug(__METHOD__ . " avant save ligne " . __LINE__);
    	    $lp->save();
    	    sfContext::getInstance()->getLogger()->debug(__METHOD__ . " après save ligne " . __LINE__);
    	}
        }
    et voici le résultat : le produit qui fait les liens est celui d'id #3, les 3 produits liés sont d'id 1, 2, 4 :
    Dec 02 11:02:20 symfony [debug] ProductForm::doSave avant parent::doSave ligne 84
    Dec 02 11:02:20 symfony [info] {Doctrine_Connection_Statement} execute : DELETE FROM product_link WHERE (linked_product_id = ? AND product_id IN (?, ?, ?)) - (3, 1, 2, 4)
    Dec 02 11:02:20 symfony [info] {Doctrine_Connection_Statement} execute : UPDATE product_link SET linked_product_id = ? WHERE product_id = ? AND linked_product_id = ? - (1, 1, 3)
    Dec 02 11:02:20 symfony [info] {Doctrine_Connection_Statement} execute : UPDATE product_link SET linked_product_id = ? WHERE product_id = ? AND linked_product_id = ? - (2, 2, 3)
    Dec 02 11:02:20 symfony [info] {Doctrine_Connection_Statement} execute : UPDATE product_link SET linked_product_id = ? WHERE product_id = ? AND linked_product_id = ? - (4, 4, 3)
    Dec 02 11:02:20 symfony [info] {Doctrine_Connection_Statement} execute : DELETE FROM product_link WHERE product_id = ? AND linked_product_id = ? - (1, 3)
    Dec 02 11:02:20 symfony [info] {Doctrine_Connection_Statement} execute : DELETE FROM product_link WHERE product_id = ? AND linked_product_id = ? - (2, 3)
    Dec 02 11:02:20 symfony [info] {Doctrine_Connection_Statement} execute : DELETE FROM product_link WHERE product_id = ? AND linked_product_id = ? - (4, 3)
    Dec 02 11:02:20 symfony [debug] ProductForm::doSave après parent::doSave ligne 86
    Dec 02 11:02:20 symfony [debug] ProductForm::doSave avant save ligne 94
    Dec 02 11:02:20 symfony [info] {Doctrine_Connection_Statement} execute : INSERT INTO product_link (linked_product_id, product_id, created_at) VALUES (?, ?, ?) - (3, 1, 2010-12-02 11:02:20)
    Dec 02 11:02:20 symfony [debug] ProductForm::doSave après save ligne 96
    Dec 02 11:02:20 symfony [debug] ProductForm::doSave avant save ligne 94
    Dec 02 11:02:20 symfony [info] {Doctrine_Connection_Statement} execute : INSERT INTO product_link (linked_product_id, product_id, created_at) VALUES (?, ?, ?) - (3, 2, 2010-12-02 11:02:20)
    Dec 02 11:02:20 symfony [debug] ProductForm::doSave après save ligne 96
    Dec 02 11:02:20 symfony [debug] ProductForm::doSave avant save ligne 94
    Dec 02 11:02:20 symfony [info] {Doctrine_Connection_Statement} execute : INSERT INTO product_link (linked_product_id, product_id, created_at) VALUES (?, ?, ?) - (3, 4, 2010-12-02 11:02:20)
    Dec 02 11:02:20 symfony [debug] ProductForm::doSave après save ligne 96


    Le premier DELETE me laisse déjà perplexe car Symfony me supprime d'office tous les liens pointant vers mon produit, ce qui n'est pas demandé ici.

    Les trois UPDATE qui suivent, je ne les comprends tout simplement pas : ils vont créer dans la table ProductLink des entrées avec des produits se pointant eux-mêmes, ce qui est évidemment faux.

    Et ces entrées calamiteuses sont de toutes façons supprimées par les 3 DELETE qui suivent...

    Toutes ces requêtes abracadabrantes disparaissent si je retire ma surcharge, bien sûr. Mais je ne comprends pas
    - pourquoi Symfony génère ces requêtes aussi aberrantes ?
    - comment faire pour automatiser la création de liens symétriques ?

  2. #2
    Expert confirmé
    Avatar de Michel Rotta
    Homme Profil pro
    DPO
    Inscrit en
    Septembre 2005
    Messages
    4 954
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 62
    Localisation : France, Bouches du Rhône (Provence Alpes Côte d'Azur)

    Informations professionnelles :
    Activité : DPO
    Secteur : Distribution

    Informations forums :
    Inscription : Septembre 2005
    Messages : 4 954
    Par défaut
    Ne pourrait-on considérer que ton problème s'apparente à une relation hiérarchique multi-route, qui peut être implantée directement dans doctrine ?

  3. #3
    Membre émérite Avatar de Herode
    Homme Profil pro
    Développeur Web
    Inscrit en
    Mars 2005
    Messages
    825
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Savoie (Rhône Alpes)

    Informations professionnelles :
    Activité : Développeur Web
    Secteur : High Tech - Multimédia et Internet

    Informations forums :
    Inscription : Mars 2005
    Messages : 825
    Par défaut
    Je ne pense pas :
    - ce n'est pas une structure hiérarchique
    - les approches en forêt (multi root) sont correctes des n*1-n, pas pour des n-n.

  4. #4
    Membre émérite Avatar de Herode
    Homme Profil pro
    Développeur Web
    Inscrit en
    Mars 2005
    Messages
    825
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Savoie (Rhône Alpes)

    Informations professionnelles :
    Activité : Développeur Web
    Secteur : High Tech - Multimédia et Internet

    Informations forums :
    Inscription : Mars 2005
    Messages : 825
    Par défaut
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    Product:
      columns:
        name:
          type: string(45)
          notnull: true
      relations:
        Links:
          refClass: ProductLink
          class: Product
          local: product_id
          foreign: linked_product_id
    J'ai vérifié le code généré par Symfony dans BaseProduct::setUp(). Je vois que pour la relation Links, Symfony crée deux appels à hasMany : l'un qui correspond à la déclaration explicite ci-dessus, l'autre qui correspond à la déclaration implicite de la relation à l'autre bout de la chaîne n-n (donc toujours dans Product puisque la relation ici est réflexive :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
            $this->hasMany('Product as LinksTo', array(
                 'refClass' => 'ProductLink',
                 'local' => 'product_id',
                 'foreign' => 'linked_product_id'));
     
            $this->hasMany('Product', array(
                 'refClass' => 'ProductLink',
                 'local' => 'linked_product_id',
                 'foreign' => 'product_id'));
    Lors de l'appel à la méthode BaseProduct::save(), Symfony parcourt toutes les relations connues et fait les mises à jour correspondantes, ce qui explique l'apparition de requêtes supplémentaires.

    J'ai donc supposé que le problème venait du fait que le formulaire contenant une zone de saisie pour l'association dans un sens (Links) mais pas dans l'autre. Sur cette hypothèse, j'ai ajouté une seconde zone de saisie en espérant que les choses allaient revenir dans l'ordre. Le fait est qu'il y a du mieux (moins de requêtes parasites) mais ce n'est toujours pas ça.

    Exemple :

    1 - j'associe un produit #3 aux produits #1 et #2 : (3,1) + (3,2). Je clique sur Save. Dans le doSave(), j'ajoute les liens symétriques aux premiers : (1,3) + (2,3). Le formulaire se réaffiche, les liens apparaissent bien pré-sélectionnés dans mes deux doubles listes.

    2 - je clique sur Save sans rien changer. On ne devrait avoir aucune requête, mais il y en a et elles sont fautives :
    UPDATE product_link SET linked_product_id = ? WHERE product_id = ? AND linked_product_id = ? - (1, 1, 3)
    UPDATE product_link SET linked_product_id = ? WHERE product_id = ? AND linked_product_id = ? - (2, 2, 3)
    + quelques inserts
    Au Save suivant, on obtient bien sûr une belle exception mais de toutes façons, ces updates sont tout simplement faux. J'en viens à penser que soit il y a une grosse erreur dans mon schema, soit Symfony ne sait pas gérer les relations réflexives (ou les gère mais il y a un bug).

    Avec le retard que j'ai pris là-dessus, il va falloir que je trouve une solution très vite en tout cas...

  5. #5
    Expert confirmé
    Avatar de Michel Rotta
    Homme Profil pro
    DPO
    Inscrit en
    Septembre 2005
    Messages
    4 954
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 62
    Localisation : France, Bouches du Rhône (Provence Alpes Côte d'Azur)

    Informations professionnelles :
    Activité : DPO
    Secteur : Distribution

    Informations forums :
    Inscription : Septembre 2005
    Messages : 4 954
    Par défaut
    Ton shema, peut-être, amélioré...
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
     
    product:   //minuscules
      columns:
        name:
          type: string(45)
          notnull: true
      relations:
        links:
          refClass: product_link
          class: Product
          foreignAlias: link_backs
          // dans une refClass, il ne peut y avoir de local ou foreign, c'est la
          // table refClass qui utilise ces propres liaisons.
    product_link:
      columns:
        product_id:
          type: integer
          primary: true
        linked_product_id:
          type: integer
          primary: true
      relations:
        linking_product:
          class: product
          local: product_id
          foreign: id   // si je défini local je défini foreign, mais je ne sais plus 
                           // pourquoi j'en suis arrivé là. Une erreur un jour.
          foreignAlias: as_linker
          onDelete : cascade // ATTENTION ! au boucles infernales
        linked_product:
          class: product
          local: linked_product_id
          foreign: id
          foreignAlias: as_linked
          onDelete : cascade
          // onUpdate sur une clef primaire, je doute...
    Il est possible que l'erreur provienne du schéma.

    Il est aussi possible que cela provienne du CRUD que tu utilises pour modifier.

    Le onDelette me fait un peu peur, une cascade infernal est toujours possible. Si c'est pour supprimer le lien, il n'est pas nécessaire. Si c'est pour supprimer les articles enfants, je le gèrerais à la mains, et même là, il faut faire attention... !!!

    De plus, il me semble que tu n'as pas de vérification de boucle infernal dans tes articles (ex : A -> B -> C -> A)

    Ce type de liaisons est toujours difficile à exploiter, quelque soit la base et les outils utilisés. Les bases de données relationnels ne sont pas à l'aise sur des relations réflectives de type n-n.

  6. #6
    Membre émérite Avatar de Herode
    Homme Profil pro
    Développeur Web
    Inscrit en
    Mars 2005
    Messages
    825
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Savoie (Rhône Alpes)

    Informations professionnelles :
    Activité : Développeur Web
    Secteur : High Tech - Multimédia et Internet

    Informations forums :
    Inscription : Mars 2005
    Messages : 825
    Par défaut
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    product:   //minuscules
      columns:
        name:
          type: string(45)
          notnull: true
      relations:
        links:
          refClass: product_link
          class: Product
          foreignAlias: link_backs
    J'avais essayé ce type de déclaration au début mais cela me donnait (et me donne à nouveau) une erreur SQLSTATE[23000]: Integrity constraint violation: 1452 Cannot add or update a child row: a foreign key constraint fails. Logique puisque la requête est :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    INSERT INTO product_link (product_id, created_at) VALUES (?, ?)', array('3', '2010-12-03 14:24:29')
    et il manque bien une partie de la clé. C'est pourquoi j'avais ajouté les champs local/foreign dans la déclaration de la relation.

    Ou alors je fais une faute de frappe quelque part et mon schema est encore faux mais j'ai beau m'user les yeux, je ne vois rien de louche

    Le onDelette me fait un peu peur, une cascade infernal est toujours possible
    Heu... que veux-tu dire ? "onDelete : cascade" supprime l'enregistrement dans la table ProductLink si la clé dans Product est supprimée. La cascade n'est à craindre que si ProductLink est référencée à coups de clés étrangères dans d'autres tables, non ? Et même dans ce cas, utiliser les cascades est bien utile et évite d'ajouter du code supplémentaire pour gérer cela (règle d'or du développeur d'application : pour faire moins de bugs, faites moins de code ). Et je parle de vraies applications d'entreprise, of course, pas d'une misérable gestion de catalogue avec 10 tables qui se battent en duel.

    Pareil pour le "onUpdate: cascade" : si la clé est modifiée sur la table distante (la clé, pas l'objet lui-même), il garantit que la clé étrangère de ma table sera mise à jour pour
    - ne pas pointer sur une clé invalide
    - ne pas pointer sur la mauvaise clé

    Cela dit, je t'accorde que modifier une clé reste quelque chose de très rare (et ne concerne généralement pas les clés numériques autoincrémentales).

  7. #7
    Expert confirmé
    Avatar de Michel Rotta
    Homme Profil pro
    DPO
    Inscrit en
    Septembre 2005
    Messages
    4 954
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 62
    Localisation : France, Bouches du Rhône (Provence Alpes Côte d'Azur)

    Informations professionnelles :
    Activité : DPO
    Secteur : Distribution

    Informations forums :
    Inscription : Septembre 2005
    Messages : 4 954
    Par défaut
    Le problème, pour te répondre, est que je n'ai pas idée du code que tu utilises pour insérer ton enregistrement.

    As-tu essayé avec un fixature pour voir ? C'est un code très pure qui va faire l'ajout.

    Ou encore, de créer une action fictive qui va te créer, avec des requêtes de base sur les objets, des insertions. En dehors de tout contexte applicatif. Une fois ces ajouts "pures" réussi, on aura validé le modèle et son fonctionnement. Il sera toujours temps de valider tes form...

    Idéalement, les tests proposés pourraient être fais dans le cadre de test de symfony, histoire de pouvoir être régulièrement relancés. Et de valider la non régressivités de ton application.

  8. #8
    Membre émérite Avatar de Herode
    Homme Profil pro
    Développeur Web
    Inscrit en
    Mars 2005
    Messages
    825
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Savoie (Rhône Alpes)

    Informations professionnelles :
    Activité : Développeur Web
    Secteur : High Tech - Multimédia et Internet

    Informations forums :
    Inscription : Mars 2005
    Messages : 825
    Par défaut
    Tu as raison. Quelques essais plus loin, voici ce que je peux dire :

    - J'ai un jeu de fixtures qui marche bien

    - Pour m'assurer que le code que j'ajoute sur le doSave() n'est pas à l'origine du problème, je l'ai supprimé. Je reviens donc au fonctionnement classique de Symfony et je le laisse gérer ses affaires.

    Dans ce cadre, je fais un test simple :

    1 - dans mon formulaire d'édition du produit #3, je crée un lien vers le #1.
    Requête générée par Symfony :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    INSERT INTO product_link (linked_product_id, product_id) VALUES (?, ?, ?) - (1, 3)
    OK

    2 - j'ouvre #1 en édition, je lui ajoute un lien vers #3.
    Et là, re-patatras ! Requêtes générées par Symfony :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    DELETE FROM product_link WHERE (linked_product_id = ? AND product_id IN (?)) - (1, 3)
    UPDATE product_link SET linked_product_id = ? WHERE product_id = ? AND linked_product_id = ? - (3, 3, 1)
    INSERT INTO product_link (linked_product_id, product_id, created_at) VALUES (?, ?, ?) - (3, 1, 2010-12-03 17:02:52)
    DELETE FROM product_link WHERE product_id = ? AND linked_product_id = ? - (3, 1)
    Toujours le même n'importe quoi... Donc Symfony n'a pas besoin de moi pour partir en vrille avec le schéma :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
     
    Product:
      columns:
        name:
          type: string(45)
          notnull: true
      relations:
        LinksTo:
          refClass: ProductLink
          class: Product
          local: product_id
          foreign: linked_product_id
        LinksFrom:
          refClass: ProductLink
          class: Product
          local: linked_product_id
          foreign: product_id
     
    ProductLink:
      columns:
        product_id:
          type: integer
          primary: true
          notnull: true
        linked_product_id:
          type: integer
          primary: true
          notnull: true
      relations:
        LinkingProduct:
          class: Product
          local: product_id
          foreignAlias: AsLinker
        LinkedProduct:
          class: Product
          local: linked_product_id
          foreignAlias: AsLinked

  9. #9
    Expert confirmé
    Avatar de Michel Rotta
    Homme Profil pro
    DPO
    Inscrit en
    Septembre 2005
    Messages
    4 954
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 62
    Localisation : France, Bouches du Rhône (Provence Alpes Côte d'Azur)

    Informations professionnelles :
    Activité : DPO
    Secteur : Distribution

    Informations forums :
    Inscription : Septembre 2005
    Messages : 4 954
    Par défaut
    Et si tu essayes avec les commandes de base de doctrine pour ajouter ?

    A priori, pour ton test, tu passes par un form.

Discussions similaires

  1. Update sous Access
    Par Sk8cravis dans le forum Langage SQL
    Réponses: 7
    Dernier message: 16/04/2009, 14h29
  2. Réponses: 1
    Dernier message: 15/05/2006, 13h48
  3. Relation php et iis 5.1 sous xp pro
    Par Rousselin dans le forum IIS
    Réponses: 1
    Dernier message: 15/04/2006, 14h09
  4. [UPDATE]Sous-requetes avec plusieurs nuplets
    Par Tchinkatchuk dans le forum Langage SQL
    Réponses: 2
    Dernier message: 11/07/2005, 18h28
  5. syntaxe d'un update sous SQL SERVER
    Par wello00 dans le forum MS SQL Server
    Réponses: 2
    Dernier message: 08/12/2004, 14h13

Partager

Partager
  • Envoyer la discussion sur Viadeo
  • Envoyer la discussion sur Twitter
  • Envoyer la discussion sur Google
  • Envoyer la discussion sur Facebook
  • Envoyer la discussion sur Digg
  • Envoyer la discussion sur Delicious
  • Envoyer la discussion sur MySpace
  • Envoyer la discussion sur Yahoo