Ankündigung

Einklappen
Keine Ankündigung bisher.

[GELÖST] Doctrine2: Insert mit to-many Relation: Referenzierte Zeile wird nicht gescgrieben

Einklappen

Neue Werbung 2019

Einklappen
X
  • Filter
  • Zeit
  • Anzeigen
Alles löschen
neue Beiträge

  • [GELÖST] Doctrine2: Insert mit to-many Relation: Referenzierte Zeile wird nicht gescgrieben

    Crosspost von Stackoverflow

    Ich habe zwei Entities A und B. A hat eine unidirektionale Many-To-Many relation zu B. Wenn ich nun ein neues A erzeuge und ihm Bs zuweise knallt mein Constraint auf der Join-Tabelle. Doctrine versucht ein A mit der ID #0 zu referenzieren.

    PHP-Code:
    /**
     * @Entity
     * @Table(name="a")
     */
    class {
        
    /**
         * @var int
         * @Id @Column(type="integer") @GeneratedValue
         */
        
    protected $id;
        
    /**
         * @var B[]
         * @ManyToMany(targetEntity="B", fetch="LAZY")
         * @JoinTable(name="jointable",
         *     joinColumns={@JoinColumn(name="a_id", referencedColumnName="id")},
         *     inverseJoinColumns={@JoinColumn(name="b_id", referencedColumnName="id")}
         * )
         */
        
    protected $bs;

        public function 
    getBs() { return $this->bs; }
    }

    // Ich lasse B hier mal weg, das ist unwichtig

    // folgender code im Controller:

    $a = new A();
    $a->getBs()->add($em->find(B::class, 8));

    $em->persist($a);
    $em->flush(); 
    Das wirft folgenden Fehler:

    Code:
    An exception occurred while executing 'INSERT INTO jointable (a_id, b_id) VALUES (?, ?)' with params [0, 8]:
    
    SQLSTATE[23000]: Integrity constraint violation...
    Ein Blick in die Datenbank (und in den Query-Log) verrät mir, dass das A nie geINSERTed wird. Deshalb kann Doctrine natürlich auch keine Verbindung zum B #8 herstellen.

    Wie kann ich denn Doctrine jetzt beibringen, dass es das A zuerst INSERTen muss? Und sollte das ein O/RM nicht eigentlich können?! o.0

  • #2
    Geht bei mir einwandfrei:

    A.php
    PHP-Code:
    <?php

    /**
     * @Entity
     * @Table(name="a")
     */
    class {
        
    /**
         * @var int
         * @Id @Column(type="integer") @GeneratedValue
         */
        
    protected $id;
        
    /**
         * @var B[]
         * @ManyToMany(targetEntity="B", fetch="LAZY")
         * @JoinTable(name="jointable",
         *     joinColumns={@JoinColumn(name="a_id", referencedColumnName="id")},
         *     inverseJoinColumns={@JoinColumn(name="b_id", referencedColumnName="id")}
         * )
         */
        
    protected $bs;

        public function 
    __construct() {
            
    $this->bs = new \Doctrine\Common\Collections\ArrayCollection();
        }

        public function 
    getBs() { return $this->bs; }
    }
    B.php
    PHP-Code:
    <?php

    /**
     * @Entity
     * @Table(name="b")
     */
    class {
        
    /**
         * @var int
         * @Id @Column(type="integer") @GeneratedValue
         */
        
    protected $id;
    }
    test.php
    PHP-Code:
    <?php

    require_once __DIR__ '/bootstrap.php';

    $a = new A();
    $a->getBs()->add($entityManager->find(B::class, 8));

    $entityManager->persist($a);
    $entityManager->flush();
    Code:
    mysql> SELECT * FROM jointable;
    +------+------+
    | a_id | b_id |
    +------+------+
    |    1 |    8 |
    +------+------+
    1 row in set (0.00 sec)

    Kommentar


    • #3
      Ich habe jetzt, nach mehreren Stunden Doctrine Quellcode lesen und debuggen, folgendes herausgefunden:

      Die Tabelle für A hatte eine Spalte TINYINT(4) NN DEFAULT 0. Das war im Entity ein @Column(type="boolean"). Folglich hat Doctrine eine 0 als Boolean an PDO übergeben: ->bindValue(1, 0, PDO::PARAM_BOOL).
      Das funktioniert aber nur, wenn das Attribut PDO::ATTR_EMULATE_PREPARES auf true gesetzt ist. Bei mir war es auf false. Ich vermute mal, dass das 0 und das BOOLEAN dann an die DB gingen, welche damit nichts anfangen konnte.

      Weil die DB mit den Infos nichts anfangen konnte, schlug der Query fehl (PDOStatement::execute() === false); allerdings ohne Exception und ohne Daten in PDOStatement::errorInfo() zu hinterlassen. Deshalb nahm Doctrine an, dass der Insert erfolgreich war. Bei der Abfrage auf die INSERT-ID des nicht-existenten A kam logischerweise 0 zurück, weshalb Doctrine dem A die ID 0 zuwies. Und so kam es, dass Doctrine auch versuchte, die ID 0 beim assoziieren mit dem B zu verwenden.

      Ich habe das als Bug bei PHP eingereicht: https://bugs.php.net/bug.php?id=71059

      Lösung:

      In unserer Datenbank sind alle BOOLEAN felder TINYINT(4) oder TINYINT(1). In diesem fall kann man den boolean Datentyp von Doctrine einfach überschreiben:
      Beim aufsetzten von der EntityManager Configuration (bootstrap.php):

      PHP-Code:
      Type::overrideType(Type::BOOLEAN,    \Doctrine\DBAL\Types\TinyintBooleanType::class); 
      Wenn nicht alle BOOLEAN Spalten konsistent TINYINTs sind, müsst ihr einen neuen Typ einführen:

      PHP-Code:
      Type::addType(\Doctrine\DBAL\Types\TinyintBooleanType::NAME, \Doctrine\DBAL\Types\TinyintBooleanType::class)
      $entityManager->getConnection()->getDatabasePlatform()->registerDoctrineTypeMapping(Type::SMALLINT, \Doctrine\DBAL\Types\TinyintBooleanType::NAME); 
      Diesen Typ könnt ihr dann als "tinyintasbool" in eurer Entity-Definition benutzen: @Column(type="tinyintasbool")

      TinyintBooleanType.php
      PHP-Code:
      <?php
      namespace Doctrine\DBAL\Types;

      use 
      Doctrine\DBAL\Platforms\AbstractPlatform;

      /**
       * This type is appropriate when storing boolean values in TINYINT columns.
       */
      class TinyintBooleanType extends Type
      {
          const 
      NAME "tinyintasbool";

          public function 
      convertToDatabaseValue($valueAbstractPlatform $platform)
          {
              if (
      $value === null)
              {
                  return 
      null;
              }
              else return 
      $value0;
          }

          public function 
      convertToPHPValue($valueAbstractPlatform $patform)
          {
              if (
      $value === null)
              {
                  return 
      null;
              }
              else return ((int) 
      $value) === 0false true;
          }

          
      /**
           * Gets the SQL declaration snippet for a field of this type.
           *
           * @param array $fieldDeclaration The field declaration.
           * @param \Doctrine\DBAL\Platforms\AbstractPlatform $platform The currently used database platform.
           *
           * @return string
           */
          
      public function getSQLDeclaration(array $fieldDeclarationAbstractPlatform $platform)
          {
              return 
      $platform->getSmallIntTypeDeclarationSQL($fieldDeclaration);
          }

          
      /**
           * Gets the name of this type.
           *
           * @return string
           *
           * @todo Needed?
           */
          
      public function getName()
          {
              return static::
      NAME;
          }

          public function 
      getBindingType()
          {
              return \
      PDO::PARAM_INT;
          }
      }

      Kommentar

      Lädt...
      X