DoctrineのorphanRemoval=true がようやく理解できた。

前回タイトル「未解決! Doctrine、OneToOneでorphanRemoval=true が使えない・・・」にしてました。

その後試行錯誤したけっかようやくorphanRemoval=trueについて理解出来ました。
orphanRemovalって翻訳機にかけると孤児除去なのでてっきり子(外部キーを持った方)を削除すると勘違いしてました。逆でした。

削除はしませんが書き消し選つけておきます。

書き消し選以降が今回分かったことです。

この機能が何のためにあるの?

まずエンティティの定義

use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Entity
 */
class A
{

    /**
     * @ORM\Id
     * @ORM\Column
     * @ORM\GeneratedValue
     */
    private int $id;
    public function getId(){ return $this->id; }
    public function setId(int $id){ $this->id = $id; }

    /**
     * @ORM\OneToOne(targetEntity="B", mappedBy="a", orphanRemoval=true)
     */
    private B $b;
    public function getB(): B { return $this->b; }
    public function setB(B $b){ $this->b = $b;  }

}

/**
 * @ORM\Entity
 */
class B
{

    /**
     * @ORM\Id
     * @ORM\Column
     * @ORM\GeneratedValue
     */
    private int $id;
    public function getId(){ return $this->id; }
    public function setId(int $id){ $this->id = $id; }

    /**
     * @ORM\OneToOne(targetEntity="A", inversedBy="b")
     */
    private A $a;
    public function getA(): A  { return $this->a; }
    public function setA(A $a){ $this->a = $a; }

}

作成されたSQLのスキーマ

CREATE TABLE A(
    id INT AUTO_INCREMENT NOT NULL,
    PRIMARY KEY(id)
) DEFAULT CHARACTER
SET
    utf8 COLLATE `utf8_unicode_ci` ENGINE = InnoDB
;
CREATE TABLE B(
    id INT AUTO_INCREMENT NOT NULL,
    a_id INT DEFAULT NULL,
    UNIQUE INDEX UNIQ_4AD0CF313BDE5358(a_id),
    PRIMARY KEY(id)
) DEFAULT CHARACTER
SET
    utf8 COLLATE `utf8_unicode_ci` ENGINE = InnoDB
;
ALTER TABLE B ADD CONSTRAINT FK_4AD0CF313BDE5358 FOREIGN KEY(
    a_id
) REFERENCES A(
    id
)
;

実行するコード

*事前にエンティティを読み込んでいてください。

$manager = require 'createManager.php';

// A(1)を作成、それにB(1)を設定
$a = new A();
$b1 = new B();
$a->setB($b1);
#$b1->setA($a);
$manager->persist($b1);
$manager->persist($a);
$manager->flush();

// ちょっと待機
sleep(5);

// A(1)に新しくB(2)を設定する
$b2 = new B();
$a->setB($b2);
#$b2->setA($a);
$manager->persist($b2);
$manager->flush();

以下はSQLの出力結果を取得し加工したもの

"START TRANSACTION" 
INSERT INTO A (id) VALUES (null) 
INSERT INTO B (a_id) VALUES (?)  :: (NULL)
"COMMIT" 
"START TRANSACTION" 
INSERT INTO B (a_id) VALUES (?)  :: (NULL)
DELETE FROM B WHERE id = ?  :: (1)
"COMMIT"

以下はBのテーブルで、1回目のflush()と2回目のflush()で取得したもの。

mysql> SELECT * FROM B;
+----+------+
| id | a_id |
+----+------+
|  1 | NULL |
+----+------+
1 row in set (0.00 sec)

5秒後

mysql> SELECT * FROM B;
+----+------+
| id | a_id |
+----+------+
|  2 | NULL |
+----+------+
1 row in set (0.00 sec)

外部キーを設定しないようなコードを書いてたので、当然外部キーは設定されてない。
ここでorphanRemoval=trueの効果発動!

お気づきだろうか! B(1)が削除されている点に。

一回目で親A(1)にB(1)が設定
二回目で親A(1)にB(2)が設定(この時、もうB(1)の存在意義無いよね、じゃあ削除!)

ただ外部キーが設定されないと意味が無いので設定しようか(コメントアウト)!

// 省略・・・

// A(1)を作成、それにB(1)を設定
$a = new A();
$b1 = new B();
$a->setB($b1);
$b1->setA($a);
$manager->persist($b1);
$manager->persist($a);
$manager->flush();

// ちょっと待機
sleep(5);

// A(1)に新しくB(2)を設定する
$b2 = new B();
$a->setB($b2);
$b2->setA($a);
$manager->persist($b2);
$manager->flush();
"START TRANSACTION" 
INSERT INTO A (id) VALUES (null) 
INSERT INTO B (a_id) VALUES (?)  :: (1)
"COMMIT" 
"START TRANSACTION" 
INSERT INTO B (a_id) VALUES (?)  :: (1)
"ROLLBACK" 
PHP Fatal error:  Uncaught PDOException: SQLSTATE[23000]: Integrity constraint violation: 1062 Duplicate entry '1' for key 'UNIQ_4AD0CF313BDE5358' 

エラーになりました。

一回目のA(1)を作成し、B(1)を追加するには成功します。

mysql> SELECT * FROM B;
+----+------+
| id | a_id |
+----+------+
|  1 |    1 |
+----+------+

二回目で失敗してます。
前回と今回の二回目を比較します。

"START TRANSACTION" 
INSERT INTO B (a_id) VALUES (?)  :: (NULL)
DELETE FROM B WHERE id = ?  :: (1)
"COMMIT"

前回ではB(2)を追加した後、B(1)を削除している。

"START TRANSACTION" 
INSERT INTO B (a_id) VALUES (?)  :: (1)
"ROLLBACK" 

今回ではB(2)を追加した時点で失敗している。

例外内容は当然外部キーのユニーク制約。

1対1なのでBの外部キーの値はユニークなのはうなずけるんですが、

B(1)の外部キーがA(1)のもの
B(2)の外部キーがA(1)のもの

B(1)を削除する前にB(2)の外部キーを設定するもんだからこの制約に引っかかったんだな!

じゃあユニークにする必要ないんじゃ? でもそれってDBレベルでは1対1なの?って疑問が。

で、なんか10年くらい前の見つけたんだけど、これいまどうなってんの・・・?

https://github.com/doctrine/orm/issues/2310

ユニーク制約あるんだけど・・・。
ちょっとお手上げ!

ここから本文

use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Entity
 */
class A
{

    /**
     * @ORM\Id
     * @ORM\Column
     * @ORM\GeneratedValue
     */
    private int $id;
    public function getId(){ return $this->id; }
    public function setId(int $id){ $this->id = $id; }

    /**
     * @ORM\OneToOne(targetEntity="B", inversedBy="b", orphanRemoval=true)
     */
    private B $b;
    public function getB(): B { return $this->b; }
    public function setB(B $b){ $this->b = $b;  }

}

/**
 * @ORM\Entity
 */
class B
{

    /**
     * @ORM\Id
     * @ORM\Column
     * @ORM\GeneratedValue
     */
    private int $id;
    public function getId(){ return $this->id; }
    public function setId(int $id){ $this->id = $id; }

    /**
     * @ORM\OneToOne(targetEntity="A", mappedBy="a")
     */
    private A $a;
    public function getA(): A  { return $this->a; }
    public function setA(A $a){ $this->a = $a; }

}

// --- 以下実行内容 ---

// B(1)の追加
$a = new A();
$b1 = new B();
$a->setB($b1);
$manager->persist($a);
$manager->persist($b1);
$manager->flush();

sleep(15);

// B(2)の追加
$b2 = new B();
$a->setB($b2);
$manager->persist($b2);
$manager->flush();

B(1)の追加についてはどちらも同じ結果です。

mysql> select * from A;
+----+------+
| id | b_id |
+----+------+
|  1 |    1 |
+----+------+

mysql> select * from B;
+----+
| id |
+----+
|  1 |
+----+

A(1)がB(1)を持っている感じです。
ここまではどちらも同じですが。

orphanRemoval=false

mysql> select * from A;
+----+------+
| id | b_id |
+----+------+
|  1 |    2 |
+----+------+

mysql> select * from B;
+----+
| id |
+----+
|  1 |
|  2 |
+----+

A(1)がB(1)を持つ
A(1)がB(2)を持つ
この時B(1)はA(1)から参照はされませんが生き残ってます。
通常の

発行されたSQLです。

"START TRANSACTION" 
INSERT INTO B (id) VALUES (null) 
INSERT INTO A (b_id) VALUES (?)  :: (1)
"COMMIT" 
"START TRANSACTION" 
INSERT INTO B (id) VALUES (null) 
UPDATE A SET b_id = ? WHERE id = ?  :: (2, 1)
"COMMIT"

orphanRemoval=true

mysql> select * from A;
+----+------+
| id | b_id |
+----+------+
|  1 |    2 |
+----+------+
1 row in set (0.00 sec)

mysql> select * from B;
+----+
| id |
+----+
|  2 |
+----+
1 row in set (0.01 sec)

A(1)がB(1)を持つ
B(1)がB(2)を持つがこの時B(1)は削除されます。
SQLを見てみます。

"START TRANSACTION" 
INSERT INTO B (id) VALUES (null) 
INSERT INTO A (b_id) VALUES (?)  :: (1)
"COMMIT" 
"START TRANSACTION" 
INSERT INTO B (id) VALUES (null) 
UPDATE A SET b_id = ? WHERE id = ?  :: (2, 1)
DELETE FROM B WHERE id = ?  :: (1)
"COMMIT" 

最後にB(1)が削除されていることが確認できます。

1対多の場合は?

まずAエンティティの$bsプロパティにorphanRemoval=falseを設定。
orphanRemovalを使用しない場合です。

use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\ArrayCollection;

/**
 * @ORM\Entity
 */
class A
{

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

    /**
     * @ORM\Id
     * @ORM\Column
     * @ORM\GeneratedValue
     */
    private int $id;
    public function getId(){ return $this->id; }
    public function setId(int $id){ $this->id = $id; }

    /**
     * @ORM\OneToMany(targetEntity="B", mappedBy="a", orphanRemoval=false)
     */
    private $bs;

    public function addB(B $b): self
    {
        $this->bs->add($b);
        $b->setA($this);
        return $this;
    }

    public function removeB($bid)
    {
        echo implode(', ', array_map(fn($b) => $b->getId(), $this->bs->toArray())) . "\n";

        $this->bs->remove($bid);

        echo implode(', ', array_map(fn($b) => $b->getId(), $this->bs->toArray())) . "\n";
    }
}

/**
 * @ORM\Entity
 */
class B
{

    /**
     * @ORM\Id
     * @ORM\Column
     * @ORM\GeneratedValue
     */
    private int $id;
    public function getId(){ return $this->id; }
    public function setId(int $id){ $this->id = $id; }

    /**
     * @ORM\ManyToOne(targetEntity="A", inversedBy="b")
     */
    private A $a;
    public function getA(): A  { return $this->a; }
    public function setA(A $a){ $this->a = $a; }

}

次にSQLの出力結果です(中にArrayCollectionの中身チェックが入ってます)。

  "START TRANSACTION" 
  INSERT INTO A (id) VALUES (null) 
  INSERT INTO B (a_id) VALUES (?)  :: (1)
  INSERT INTO B (a_id) VALUES (?)  :: (1)
  INSERT INTO B (a_id) VALUES (?)  :: (1)
  "COMMIT" 
1, 2, 3
1, 3
  "START TRANSACTION" 
  "COMMIT"
mysql> select * from B;
+----+------+
| id | a_id |
+----+------+
|  1 |    1 |
|  2 |    1 |
|  3 |    1 |
+----+------+

プログラム上のコレクションに変更を加えてもDBに変化はありません。

orphanRemoval=trueに変更すると

use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\ArrayCollection;

/**
 * @ORM\Entity
 */
class A
{

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

    /**
     * @ORM\Id
     * @ORM\Column
     * @ORM\GeneratedValue
     */
    private int $id;
    public function getId(){ return $this->id; }
    public function setId(int $id){ $this->id = $id; }

    /**
     * @ORM\OneToMany(targetEntity="B", mappedBy="a", orphanRemoval=true)
     */
    private $bs;

    public function addB(B $b): self
    {
        $this->bs->add($b);
        $b->setA($this);
        return $this;
    }

    public function removeB($bid)
    {
        echo implode(', ', array_map(fn($b) => $b->getId(), $this->bs->toArray())) . "\n";

        $this->bs->remove($bid);

        echo implode(', ', array_map(fn($b) => $b->getId(), $this->bs->toArray())) . "\n";
    }
}

/**
 * @ORM\Entity
 */
class B
{

    /**
     * @ORM\Id
     * @ORM\Column
     * @ORM\GeneratedValue
     */
    private int $id;
    public function getId(){ return $this->id; }
    public function setId(int $id){ $this->id = $id; }

    /**
     * @ORM\ManyToOne(targetEntity="A", inversedBy="b")
     */
    private A $a;
    public function getA(): A  { return $this->a; }
    public function setA(A $a){ $this->a = $a; }

}

B(2)を削除するクエリが発行されていました。

  "START TRANSACTION" 
  INSERT INTO A (id) VALUES (null) 
  INSERT INTO B (a_id) VALUES (?)  :: (1)
  INSERT INTO B (a_id) VALUES (?)  :: (1)
  INSERT INTO B (a_id) VALUES (?)  :: (1)
  "COMMIT" 
1, 2, 3
1, 3
  "START TRANSACTION" 
  DELETE FROM B WHERE id = ?  :: (2)
  "COMMIT"

ちなみに前回がこちらです。

  "START TRANSACTION" 
  INSERT INTO A (id) VALUES (null) 
  INSERT INTO B (a_id) VALUES (?)  :: (1)
  INSERT INTO B (a_id) VALUES (?)  :: (1)
  INSERT INTO B (a_id) VALUES (?)  :: (1)
  "COMMIT" 
1, 2, 3
1, 3
  "START TRANSACTION" 
  "COMMIT"

コレクション上の削除が反映されました。

DBを確認します。

mysql> select * from B;
+----+------+
| id | a_id |
+----+------+
|  1 |    1 |
|  3 |    1 |
+----+------+

certificate Docker Gutenberg Hyper-V openssl PHP React ReduxToolkit REST ubuntu WordPress オレオレ認証局 フレームワーク