質問: Symfonyで既にDBへ登録されているEntityであるか否かはどのように判断しているのでしょうか? PlanとCourseがoneToManyの関係であるEntityを作成しました。 Planへ、CourseをDBより検索しaddCourse()するのと、既にDBに登録されているEntityと全く同じEntityを手動で作成しaddCourse()するのはどのような違いがあるのでしょうか?
回答: Symfony2でORMとして使われているDoctrine2における、エンティティの状態の扱い方に関連する問題ですね。最初は戸惑うポイントだと思います。 最初に注意が必要なのは、Doctrineでは「データベースのレコード」ではなく、「オンメモリのオブジェクト」を中心に考えなくてはならないということです。 Doctrineでのエンティティ管理の基本 ===================== アプリケーションの側から見ると、 ``` アプリケーションのコード → Doctrineの世界 → DBの世界 ``` というように、明示的にDBの世界との間にDoctrineの世界が設けられます。 この3つの世界間で、 * 新規エンティティ登録 * 既存レコードの読み込み * 既存レコードの変更 は次のようになっています。 新規エンティティの登録時 ``` $plan = new Plan(); // アプリケーションで、新規エンティティのインスタンス ↓ $entityManager->persist($plan); // Doctrineの世界へ登録 // EntityManagerの管理下になる(*1) ↓ $entityManager->flush(); // Doctrineの世界をDBの世界へ同期 // 新しく登録されたエンティティの INSERT が発行される ``` 上記 (*1) の部分で、追加されたインスタンスには `STATE_NEW` という状態が割り当てられ、`EntityManager` 内でこの状態が管理されます。 既存レコードの読み込み ``` $repository = $entityManager->getRepository('Plan'); $plan = $repository->find($planId); // DBからDoctrineの世界へ (*2) // そのインスタンスをアプリケーションへ取得 ``` 上記 (*2) の部分でDoctrineの世界へ読み込まれたエンティティのインスタンスには、`STATE_MANAGED` という状態が割り当てられます。 既存レコードの変更 上の既存レコード読み込みで取得したインスタンスに対して変更を加えて保存するときは次のようになります。 ``` $plan->setName($newPlanName); // (*3) $entityManager->flush(); ``` (*3) で単にプロパティの値を変更し、その後、`EntityManger`の`flush()`を実行すると、ここでの`$plan`はすでにDoctrine管理下にあるため、その変更内容が`EntityManager`により検出され、DBへUPDATE文が発行されます。 強制的にEntityManager管理下におけるか? ========================== すでにDBに存在しているレコードに対応するエンティティを、Doctrineから取得するのではなく、アプリケーション側で手作業でインスタンス化すると、不都合が生じます。 このインスタンスは、DBにすでに存在しているかどうかに関わらず、Doctrineの世界に「新しく追加された」とみなされてしまいます。 ``` $existingPlan = new Plan(1, 'プランA'); // ID的には既存レコードだが手動インスタンス化 $entityManager->persist($existingPlan); $entityManager->flush(); // → INSERTになる ``` このように新規に手作業でインスタンス化したものは、Doctrineに「既存のオブジェクトだ」として処理させる方法はありません。無駄になりますが、再度Doctrine経由でDBから該当するエンティティを取り出し直して使うしかありません。 入力画面等でセッションに保存したものを確認画面で使う 入力画面→確認画面のようなフローがあって、入力画面ではDoctrine経由で読み込み、それをセッションに保存しておいて、確認画面ではセッションから復元したものを使いたい場合があります。手作業でインスタンス化したのと違うところは、セッションに入っているのは「もともとDoctrine管理下にあったインスタンスだ」ということです。 このようなインスタンスであれば、再びDoctrine管理下へ入れることができます。`EntityManager#merge()` メソッドを使います。 * 8.6. Merging entities (8. Working with Objects) - Doctrine ORM Document 次のように使います。 ``` $existingPlan = unserialize($serializedPlan) ; // セッション等にシリアライズされたデータから復帰 $managedPlan = $entityManager->merge($existingPlan); $managedPlan->setPlanName('名前変更'); $entityManager->flush(); // → UPDATEになる ``` インスタンスがDoctrineの管理下かどうかを調べるには? ============================== `EntityManager`から`UnitOfWork`オブジェクトを取得し、`getEntityState()`メソッドを使って、エンティティの管理状態を調べることができます。 ``` $state = $entityManager->getUnitOfWork()->getEntityState($targetPlan); ``` 戻り値は以下の4つの値で、UnitOfWorkクラスに定数定義されています。 * 1: UnitOfWork::STATE_MANAGED * 2: UnitOfWork::STATE_NEW * 3: UnitOfWork::STATE_DETACHED * 4: UnitOfWork::STATE_REMOVED まだDoctrineの管理下にないエンティティ(DBに存在するかどうかではない)の場合は、3 (STATE_DETACHED) が返されることになります。