BeginnerEngineerBlog
中の人
中の人

【Doctrine】データを削除した後のpersist,flushで保存できないentityがあった

公開: 2022-02-23 00:43
更新: 2023-04-06 14:47
3742
symfony4.x doctrine EntityManager ECCUBE4.1 ダイブ
データの保存処理を実装したとき、なぜか保存できないentityがあって、原因がわかるまで結構詰まったのでその時の状況を紹介します。

こんにちは!

中の人です!

ECCUBE4系でentityの保存処理を実装した時に、一部のentityの保存に失敗することがありました。

「失敗」というのは、エラーではなく、処理終了後に保存処理したはずのデータが作成されていないという状況です。

原因が全くわからず解決まで時間かかったのでその時の状況を紹介したいと思います!


保存処理の仕様


  1. csvファイルをアップロード
  2. 対象のテーブルの既存のデータを全て削除する
  3. csvファイルにもとづいて対象のテーブルを更新する

という仕様です。
要するに、csvファイルをアップロードするたびにテーブルの中身全てを更新するという仕様です。

とてもシンプルな仕様ですが、symfony,doctrineの知見が豊富な方であれば、もしかしたらこの時点で不具合の原因の予測がたつかもしれません。


不具合が発生するコード


entityが保存されない不具合が出たコードは以下のようなコードです。

📁 Customize\Controller\Admin\HogeController

<?php

namespace Customize\Controller\Admin;

use Customize\Entity\PiyoEntity;
use Eccube\Controller\AbstractController;
use Customize\Repository\FugaRepository;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Symfony\Component\HttpFoundation\Request;

class HogeController extends AbstractController
{
    private $fugaRepository;

    public function __construct(
        FugaRepository $fugaRepository
    ) {
        $this->fugaRepository = $fugaRepository;
    }

    /**
     * @Route("/%eccube_admin_route%/hoge/upload", name="admin_hoge_upload", methods={"POST"})   
     */
    public function upload(Request $request)
    {
        // requestからアップロードファイルを取得
        $file = $request->files->get('csv_file');
        // ファイルを読み込みでオープン
        if (($fp = fopen($file, 'r')) !== false) {
            // hugeRepositoryのallClearアクションで今のデータを全て削除する
            $this->hugeRepository->allClear();
            // csvファイルを一行ずつ取得
            while (($line = fgetcsv($fp)) !== false) {
                // PiyoEntityを新規に作成
                $piyoEntity = new PiyoEntity();
                // piyoEntityのnameプロパティにcsvファイルのname項目をセットする
                $piyoEntity->setName($line['name']);
                                // entityManagerのpersistアクションで$piyoEntityをentityManagerの管理下に置く
                $this->entityManager->persist($piyoEntity);
                // 保存する
                $this->entityManager->flush();
            }
        }
        return $this->redirectToRoute('foo');
    }
}

📁 Customize\Repository\FugaRepository

<?php

namespace Customize\Repository;

use Customize\Entity\PiyoEntity;
use Eccube\Repository\AbstractRepository;
use Symfony\Bridge\Doctrine\RegistryInterface;

class FugaRepository extends AbstractRepository
{
    public function __construct(RegistryInterface $registry)
    {
        parent::__construct($registry, PiyoEntity::class);
    }

    // テーブル内を全て削除する
    public function allClear()
    {
        $this->createQueryBuilder('p')
            ->delete()
            ->getQuery()
            ->getResult();
    }
}

こんな感じです。

一見すると大丈夫そうに見えますが、このコードだと保存できるときとできないときが発生します。


不具合が発生しないコード


不具合が発生しないコードは以下のようなコードです。

例1


📁 Customize\Controller\Admin\HogeController

<?php

namespace Customize\Controller\Admin;

use Customize\Entity\PiyoEntity;
use Eccube\Controller\AbstractController;
use Customize\Repository\FugaRepository;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Symfony\Component\HttpFoundation\Request;

class HogeController extends AbstractController
{
    private $fugaRepository;

    public function __construct(
        FugaRepository $fugaRepository
    ) {
        $this->fugaRepository = $fugaRepository;
    }

    /**
     * @Route("/%eccube_admin_route%/hoge/upload", name="admin_hoge_upload", methods={"POST"})   
     */
    public function upload(Request $request)
    {
        // requestからアップロードファイルを取得
        $file = $request->files->get('csv_file');
        // ファイルを読み込みでオープン
        if (($fp = fopen($file, 'r')) !== false) {
            // hugeRepositoryのallClearアクションで今のデータを全て削除する
            $this->hugeRepository->allClear();
            // 👇 こいつを追加
            $this->entityManager->clear(PiyoEntity::class);
            // csvファイルを一行ずつ取得
            while (($line = fgetcsv($fp)) !== false) {
                // PiyoEntityを新規に作成
                $piyoEntity = new PiyoEntity();
                // piyoEntityのnameプロパティにcsvファイルのname項目をセットする
                $piyoEntity->setName($line['name']);
                                // entityManagerのpersistアクションで$piyoEntityをentityManagerの管理下に置く
                $this->entityManager->persist($piyoEntity);
                // 保存する
                $this->entityManager->flush();
            }
        }
        return $this->redirectToRoute('foo');
    }
}

📁 Customize\Repository\FugaRepository(変更なし)

<?php

namespace Customize\Repository;

use Customize\Entity\PiyoEntity;
use Eccube\Repository\AbstractRepository;
use Symfony\Bridge\Doctrine\RegistryInterface;

class FugaRepository extends AbstractRepository
{
    public function __construct(RegistryInterface $registry)
    {
        parent::__construct($registry, PiyoEntity::class);
    }

    // テーブル内を全て削除する
    public function allClear()
    {
        $this->createQueryBuilder('p')
            ->delete()
            ->getQuery()
            ->getResult();
    }
}

例2


📁 Customize\Controller\Admin\HogeController(変更なし)

<?php

namespace Customize\Controller\Admin;

use Customize\Entity\PiyoEntity;
use Eccube\Controller\AbstractController;
use Customize\Repository\FugaRepository;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Symfony\Component\HttpFoundation\Request;

class HogeController extends AbstractController
{
    private $fugaRepository;

    public function __construct(
        FugaRepository $fugaRepository
    ) {
        $this->fugaRepository = $fugaRepository;
    }

    /**
     * @Route("/%eccube_admin_route%/hoge/upload", name="admin_hoge_upload", methods={"POST"})   
     */
    public function upload(Request $request)
    {
        // requestからアップロードファイルを取得
        $file = $request->files->get('csv_file');
        // ファイルを読み込みでオープン
        if (($fp = fopen($file, 'r')) !== false) {
            // hugeRepositoryのallClearアクションで今のデータを全て削除する
            $this->hugeRepository->allClear();
            // csvファイルを一行ずつ取得
            while (($line = fgetcsv($fp)) !== false) {
                // PiyoEntityを新規に作成
                $piyoEntity = new PiyoEntity();
                // piyoEntityのnameプロパティにcsvファイルのname項目をセットする
                $piyoEntity->setName($line['name']);
                                // entityManagerのpersistアクションで$piyoEntityをentityManagerの管理下に置く
                $this->entityManager->persist($piyoEntity);
                // 保存する
                $this->entityManager->flush();
            }
        }
        return $this->redirectToRoute('foo');
    }
}

📁 Customize\Repository\FugaRepository

<?php

namespace Customize\Repository;

use Customize\Entity\PiyoEntity;
use Eccube\Repository\AbstractRepository;
use Symfony\Bridge\Doctrine\RegistryInterface;

class FugaRepository extends AbstractRepository
{
    public function __construct(RegistryInterface $registry)
    {
        parent::__construct($registry, PiyoEntity::class);
    }

    // テーブル内を全て削除する
    public function allClear()
    {
        // 👇 以下のように書き換え
        foreach ($this->findAll() as $entity) {
            $this->getEntityManager()->remove($entity);
        }
    }
}

上記の例のように記述すれば、不具合は発生しませんでした。

結局、この不具合は例1のやり方で修正しました。理由は、例2でfindAll()で全てentityを取得してひとつひとつ削除すると時間やらメモリやらが多くなってしまうためです。


先に結論


entityManagerの管理外でデータを削除していたため、削除されたデータのidがentityManagerの管理下にまだある状態になってしまったため、削除済みのentityのidと新たに作成したentityのidの重複が発生し、重複した場合保存処理がされていなかったために今回の不具合に繋がった。
という感じです。


何が起きてたの?


さて、ここからこの不具合の原因についてどう調査していったか紹介していこうと思います。

さぁ今日も元気にソースコードの海の中にダイブしていくぜファァァァ◯ク!


flushアクション後のentityのidがnull


なんで保存されるときと保存されないときがあんねん!

と、flushを通過した後のentityのidを確認したところ、

  • 保存されるentityはちゃんとidが付与されていた
  • 保存されないentityはidがnullのままだった

という状態でした。
ちなみに対象のentityのidフィールドはauto_incrementです。
最初、entityのファイルがおかしいのかな?と思って、セッターの引数に型が指定されている箇所とか、テーブルのフィールドの型とか違う値セットされてんじゃないかとか思ったのですが、違いました。

そもそも型とか違えばエラーが発生するはずですからね。


persistしたのにpersistされてない


次にpersistアクションでentityがentityManagerの管理下にちゃんと置かれたか確認したところ、persistされていないことがわかりました。(管理下に置かれたか確認する方法)

なんでやねんもぉなにがあかんねんと関西に住んだことないですが、「なんでやねん」とか「なにがあかんねん」て「なんで?」の気持ちをとてもいい感じに表現できるいい言葉ですよねってどうでもいいわそんなこと。いい感じに頭バグってきました。


UnitOfWorkに重複したidがあった


さぁここから本格的にダイブしていきます。

そもそもUnitOfWorkて何?って感じですが、私もよくわかりません。🙇‍♂️ < サーセン

よくわかりませんが、デバッグした感じ、entityManagerのコアの処理を担当している印象を受けました。
また、あっているか分かりませんが、UnitOfWorkはentityManagerが作成されたときに、多分ですが今現在のデータベースのデータをidで管理していそうでした。

で、entityManagerのpersistアクションを追っていくと、/vender/doctrine/orm/lib/Doctrine/ORM/UnitOfWork.phpに入っていきます。

📁 /vender/doctrine/orm/lib/Doctrine/ORM/UnitOfWork.php

// ~~ 省略
    /**
     * Persists an entity as part of the current unit of work.
     *
     * @param object $entity The entity to persist.
     *
     * @return void
     */
    public function persist($entity)
    {
        $visited = [];


        $this->doPersist($entity, $visited);
    }
// ~~ 省略

次に

$this->doPersist($entity, $visited);

の処理を見てみます。

📁 /vender/doctrine/orm/lib/Doctrine/ORM/UnitOfWork.php

// ~~ 省略

/**
     * Persists an entity as part of the current unit of work.
     *
     * This method is internally called during persist() cascades as it tracks
     * the already visited entities to prevent infinite recursions.
     *
     * @param object $entity  The entity to persist.
     * @param array  $visited The already visited entities.
     *
     * @return void
     *
     * @throws ORMInvalidArgumentException
     * @throws UnexpectedValueException
     */
    private function doPersist($entity, array &$visited)
    {
        $oid = spl_object_hash($entity);

        if (isset($visited[$oid])) {
            return; // Prevent infinite recursion
        }

        $visited[$oid] = $entity; // Mark visited

        $class = $this->em->getClassMetadata(get_class($entity));

        // We assume NEW, so DETACHED entities result in an exception on flush (constraint violation).
        // If we would detect DETACHED here we would throw an exception anyway with the same
        // consequences (not recoverable/programming error), so just assuming NEW here
        // lets us avoid some database lookups for entities with natural identifiers.
        $entityState = $this->getEntityState($entity, self::STATE_NEW);

        switch ($entityState) {
            case self::STATE_MANAGED:
                // Nothing to do, except if policy is "deferred explicit"
                if ($class->isChangeTrackingDeferredExplicit()) {
                    $this->scheduleForDirtyCheck($entity);
                }
                break;

            case self::STATE_NEW:
                $this->persistNew($class, $entity);
                break;

            case self::STATE_REMOVED:
                // Entity becomes managed again
                unset($this->entityDeletions[$oid]);
                $this->addToIdentityMap($entity);

                $this->entityStates[$oid] = self::STATE_MANAGED;
                break;

            case self::STATE_DETACHED:
                // Can actually not happen right now since we assume STATE_NEW.
                throw ORMInvalidArgumentException::detachedEntityCannot($entity, "persisted");

            default:
                throw new UnexpectedValueException("Unexpected entity state: $entityState." . self::objToStr($entity));
        }

        $this->cascadePersist($entity, $visited);
    }
// ~~ 省略

で、なんやかんや処理が書かれていますが、前半の処理を見ると

        $oid = spl_object_hash($entity);

        if (isset($visited[$oid])) {
            return; // Prevent infinite recursion
        }

        $visited[$oid] = $entity; // Mark visited

        $class = $this->em->getClassMetadata(get_class($entity));

        // We assume NEW, so DETACHED entities result in an exception on flush (constraint violation).
        // If we would detect DETACHED here we would throw an exception anyway with the same
        // consequences (not recoverable/programming error), so just assuming NEW here
        // lets us avoid some database lookups for entities with natural identifiers.
        // 👇 こいつ
        $entityState = $this->getEntityState($entity, self::STATE_NEW);

なんやかんやした後、$entityStateを取得しています。

で後半の処理

        switch ($entityState) {
            case self::STATE_MANAGED:
                // Nothing to do, except if policy is "deferred explicit"
                if ($class->isChangeTrackingDeferredExplicit()) {
                    $this->scheduleForDirtyCheck($entity);
                }
                break;

            case self::STATE_NEW:
                $this->persistNew($class, $entity);
                break;

            case self::STATE_REMOVED:
                // Entity becomes managed again
                unset($this->entityDeletions[$oid]);
                $this->addToIdentityMap($entity);

                $this->entityStates[$oid] = self::STATE_MANAGED;
                break;

            case self::STATE_DETACHED:
                // Can actually not happen right now since we assume STATE_NEW.
                throw ORMInvalidArgumentException::detachedEntityCannot($entity, "persisted");

            default:
                throw new UnexpectedValueException("Unexpected entity state: $entityState." . self::objToStr($entity));
        }

        $this->cascadePersist($entity, $visited);

前半の処理で取得した$entityStateをもとにswitchで処理が分かれています。

で、先に言うとpersistが成功する場合、

   case self::STATE_NEW:
      $this->persistNew($class, $entity);
      break;

この条件に当てはまるとentityManagerの管理下、正しくはUnitOfWorkの管理下に入ります。
self::STATE_NEWの値は2です。

で、persistされない場合、

            case self::STATE_MANAGED:
                // Nothing to do, except if policy is "deferred explicit"
                if ($class->isChangeTrackingDeferredExplicit()) {
                    $this->scheduleForDirtyCheck($entity);
                }
                break;

こっちの処理に飛ばされていました。
self::STATE_MANAGEDの値は1です。

では、保存されるentityと保存されないentityでgetEntityStateアクションはどんな処理をして値を返しているのか

$entityState = $this->getEntityState($entity, self::STATE_NEW);

こいつを見てみます。

/**
     * Gets the state of an entity with regard to the current unit of work.
     *
     * @param object   $entity
     * @param int|null $assume The state to assume if the state is not yet known (not MANAGED or REMOVED).
     *                         This parameter can be set to improve performance of entity state detection
     *                         by potentially avoiding a database lookup if the distinction between NEW and DETACHED
     *                         is either known or does not matter for the caller of the method.
     *
     * @return int The entity state.
     */
    public function getEntityState($entity, $assume = null)
    {
        $oid = spl_object_hash($entity);

        if (isset($this->entityStates[$oid])) {
            return $this->entityStates[$oid];
        }

        if ($assume !== null) {
            return $assume;
        }

        // State can only be NEW or DETACHED, because MANAGED/REMOVED states are known.
        // Note that you can not remember the NEW or DETACHED state in _entityStates since
        // the UoW does not hold references to such objects and the object hash can be reused.
        // More generally because the state may "change" between NEW/DETACHED without the UoW being aware of it.
        $class = $this->em->getClassMetadata(get_class($entity));
        $id    = $class->getIdentifierValues($entity);

        if ( ! $id) {
            return self::STATE_NEW;
        }

        if ($class->containsForeignIdentifier) {
            $id = $this->identifierFlattener->flattenIdentifier($class, $id);
        }

        switch (true) {
            case ($class->isIdentifierNatural()):
                // Check for a version field, if available, to avoid a db lookup.
                if ($class->isVersioned) {
                    return ($class->getFieldValue($entity, $class->versionField))
                        ? self::STATE_DETACHED
                        : self::STATE_NEW;
                }

                // Last try before db lookup: check the identity map.
                if ($this->tryGetById($id, $class->rootEntityName)) {
                    return self::STATE_DETACHED;
                }

                // db lookup
                if ($this->getEntityPersister($class->name)->exists($entity)) {
                    return self::STATE_DETACHED;
                }

                return self::STATE_NEW;

            case ( ! $class->idGenerator->isPostInsertGenerator()):
                // if we have a pre insert generator we can't be sure that having an id
                // really means that the entity exists. We have to verify this through
                // the last resort: a db lookup

                // Last try before db lookup: check the identity map.
                if ($this->tryGetById($id, $class->rootEntityName)) {
                    return self::STATE_DETACHED;
                }

                // db lookup
                if ($this->getEntityPersister($class->name)->exists($entity)) {
                    return self::STATE_DETACHED;
                }

                return self::STATE_NEW;

            default:
                return self::STATE_DETACHED;
        }
    }

    public function getEntityState($entity, $assume = null)
    {
        $oid = spl_object_hash($entity);

        if (isset($this->entityStates[$oid])) {
            return $this->entityStates[$oid];
        }

        if ($assume !== null) {
            return $assume;
        }
// ~~ 省略

まず渡されたentityをspl_object_hashでハッシュ化します。(この関数については後半で説明します。)

で、

        if (isset($this->entityStates[$oid])) {
            return $this->entityStates[$oid];
        }

issetで、$this->entityStatesにハッシュ化されたidがすでに登録されているか判定しています。

で、もしすでに登録されていた場合、

return $this->entityStates[$oid];

その登録されているidの値を返します。

で、この条件に合わない場合は、

        if ($assume !== null) {
            return $assume;
        }

$assumeを返します。
ちなみに$assumeは、この関数の呼び出し元で

self::STATE_NEW // 2

が引数で渡されているため、この条件分岐の以降の処理は実行されません。

で、前述したと思いますが、デバッグして保存されるentityは、

private function doPersist($entity, array &$visited)

このアクションのswitchで

            case self::STATE_MANAGED:
                // Nothing to do, except if policy is "deferred explicit"
                if ($class->isChangeTrackingDeferredExplicit()) {
                    $this->scheduleForDirtyCheck($entity);
                }
                break;

            // 👇 こいつの処理にいくとUnitOfWorkの管理下に入る
            case self::STATE_NEW:
                $this->persistNew($class, $entity);
                break;

self::STATE_NEWの処理にいくとentityが管理下に入るので、要するに

    public function getEntityState($entity, $assume = null)
    {
        $oid = spl_object_hash($entity);

        // 👇 管理下に入らないentityはこの条件で捕まっている
        if (isset($this->entityStates[$oid])) {
            return $this->entityStates[$oid];
        }

        // 👇 こいつの処理にいくとUnitOfWorkの管理下に入る
        if ($assume !== null) {
            return $assume;
        }


保存されないentityは、すでにUnitOfWorkの管理下に入っていると判定された

ということになります。

で、UnitOfWorkの管理下に入っているidをデバッグしてなんの値が返されるか確認したところ、


管理下に置かれているentityのidの値は1になってますね。

で、この値が返ると言うことは、

private function doPersist($entity, array &$visited)

のアクションのswitchの

        switch ($entityState) {
            // 👇 このcaseに処理が飛ぶ
            case self::STATE_MANAGED:
                // Nothing to do, except if policy is "deferred explicit"
                if ($class->isChangeTrackingDeferredExplicit()) {
                    $this->scheduleForDirtyCheck($entity);
                }
                break;

            case self::STATE_NEW:
                $this->persistNew($class, $entity);
                break;

            case self::STATE_REMOVED:
                // Entity becomes managed again
                unset($this->entityDeletions[$oid]);
                $this->addToIdentityMap($entity);

                $this->entityStates[$oid] = self::STATE_MANAGED;
                break;

            case self::STATE_DETACHED:
                // Can actually not happen right now since we assume STATE_NEW.
                throw ORMInvalidArgumentException::detachedEntityCannot($entity, "persisted");

            default:
                throw new UnexpectedValueException("Unexpected entity state: $entityState." . self::objToStr($entity));
        }

case self::STATE_MANAGED:こいつに処理が飛ぶようになりますね。

ちなみにこのcase文の先の処理はちゃんとみてませんが、case self::STATE_MANAGED:に処理が飛んだ場合、基本的にif文はfalseを返すみたい(デフォルトのコメントにもそんな内容が書いてあります。)なので、何か特別な処理はしていないみたいでした。
case self::STATE_NEW:こっちは、$this->persistNew($class, $entity);このアクション名の通り、新しくUnitOfWorkの管理下に、新しくデータベースにインサートするためのentityを追加するような処理をしていました。


一旦、ここまでを整理すると

  • クエリビルダでデータを一括削除する
  • コントローラでentityをnewする
  • entityManagerのpersistアクションでentityManagerの管理下にentityを置こうとする
  • persistの結果がfalseを返す
  • 理由は、persistしたentityはすでにentityManagerの管理下にあるから(管理下にあると判定されているから)
  • すでに管理下にあるentityは特別な処理は実行されない
  • 結果保存されない

という流れですね。

また、

  • UnitOfWorkはentityをspl_object_hashでidとして管理している

ということもなんとなくわかりました。

さぁようやく何が起きてるのか分かりかけてきました。


なぜpersist前のentityがentityManagerの管理下にあると判定されるか


ここでphpの関数

spl_object_hash

について少し調べてみます。

参考リンクを途中で貼り付けましたが、spl_object_hashのドキュメントには

説明
この関数は、オブジェクトの一意な識別子を返します。この ID は、 オブジェクトを保存する際のハッシュのキーとして使えます。 また、オブジェクトが破棄されるまでは、オブジェクトを識別するための値としても使えます。 オブジェクトが破棄されると、そのハッシュが他のオブジェクトで再利用されてしまうことがあります。 この振る舞いは、spl_object_id() に似ています。
戻り値
現在存在する各オブジェクトに固有で、同一オブジェクトに対しては常に同じ値となる文字列を返します。

ふむふむなるほど。なんだかよく分かりませんが、オブジェクトに一意(ユニーク)なidをつけてくれる関数みたいですね。
また、同一オブジェクトに対して常に同じ値となる文字列を返してくれると。

さてここから。

この記事冒頭の保存処理の仕様や、不具合が発生するコードを思い出してください。

一度クエリビルダで対象のテーブル内を全て削除しましたね。

で、クエリビルダの処理を追ったところ、entityManagerには一切?ほぼ?干渉していませんでした。
あくまで私が追ったところまでの内容ではentityManagerからデータベースに接続するためのconnectionを取得するくらいでしょうか。

つまり、クエリビルダでのデータの操作は、entityManagerが管理しているidには影響を与えないということでした。

ではその結果どうなるかというと、データベースからデータは削除したにもかかわらず、entityManagerは削除前のentityのidをずっと保持することになります。

で、こっからはもう推測になってしまいますが、多分新しく作成されたentityをspl_object_hashでハッシュ化すると

  • entityManagerが管理しているentityと同一と判断され同じidが生成される
  • entityManagerが管理しているentityは無視されて、たまたま管理下のidと同じidを生成してしまっている

のどちらかの処理が動いているのではと考えました。正直もうわからん。

ただ、この不具合は保存されるentity、保存されないentityは常に同じだったので、多分前者の推測が正しいのかなと感じます。


不具合の原因


先に結論のところで書きましたが、つまり、クエリビルダでデータを操作(今回は削除)した場合、entityManagerはその処理についてはノータッチのため、クエリビルダで行った処理の前の情報を保持し続けることになりますので、その結果、新しく作成したentityをspl_object_hashでハッシュ化した際、そのobjectがentityManagerの管理下にあるentityと同じと判断された場合、同じidを生成し、persistしたとき、entityManagerの管理下にあるidと同じidが渡されるため、entityManagerの管理下にそのentityが置かれず、結果として保存されないということが起こっていました。
我ながら何言ってるかわからないですが、考えるんじゃない、感じろ精神で読み取ってください。


不具合のないコードの説明


ということで、上記の不具合の原因を考慮して説明すると、
まず例1の場合、

$this->entityManager->clear(PiyoEntity::class);

という処理を追加しました。
(正直、この不具合の原因がよくわからなかっとき、えーいって感じで適当に追加したら不具合が解消したので、以下のように処理を追いました)

これは、処理を追ったところ、entityManagerが管理しているidentityMapというプロパティにあるクラス名で管理されているentityを全てdetachする処理をしていました。

つまり、クラス名を渡したclearアクションは、そのクラスの管理しているentityを全て、entityManagerの管理下から外すという処理をしています。(detach)

つまり、今回の場合、対象のテーブルの中身を全て削除したので、対象のentityのidを全てentityManagerの管理下から削除することにより、新しく作成したentityのidの重複が発生しなくなるというということです。

で、例2の場合、この場合はentityManagerのメソッド(remove)でentityを削除するため、管理下にあるentityのidに、このidのentityは削除しますぜ的な値がidにセットされ、flushで実行されて、結果findAllでgetしたentityをentityManagerの管理から外す(削除)という処理をしています。

結果としていずれも、対象のentityを全てentityManagerの管理から削除しますので、persistをした際に重複するidがなくなるので、不具合が解消されるということです。


既にこの不具合に対する(?)質問があったので参考リンクを貼っておきます。


うーん英語意味わからんぽですが、皆さん???に悩まされているみたいですね。


最終的な結論


データをdelete(updateは大丈夫?)する際は、entityManagerを意識して処理するといい感じになりますよ!
ということです。


おわりに


めちゃ長い記事になってしまいました。
深く潜りすぎて私はもうダメかもしれません。あとは皆さん。。頼みました。。

symfony,doctrineて難しすぎる。正直言うとなるべく触れたくありません🤷‍♂️

ですがまぁ、仕事で使うから触らざるを得ないのですが。

ちなみにですが、今回の不具合、面白い挙動がありまして、この不具合調査をしている最中、デバッグ用の関数(var_dumpなど)を処理の途中に入れると、この不具合起きなかったんですよ!\\٩( ᐛ )و //
流石にその原因はわかりませんでした(というよりもう無視しました)。おそらくコアのコアの部分で何かしらの副作用がはたらいてたんじゃないかと思います。まさにブラックボックス!

ということで今回紹介した内容はあくまで私が追ったところまでなので、もしこの記事と同じ様な不具合が出た方は、ご自身でもっと調べてみるといいかもしれません。

そして、この記事で間違っている箇所など気づいた方はご指摘していただけると嬉しいです。

ということで。

また!
0
1
0
0
通信エラーが発生しました。
似たような記事