前回タイトル「未解決! 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 |
+----+------+