Symfony2 Doctrine2の小ネタ(OneToOneリレーション)

お久しぶりです、今日は眠いので大量にブログを投稿しようかと思います。

本日は、Table間のJoinを自動化してくれる、というかきちんと設定しないとまったくDAOの意味をなさないリレーションの設定に関してです。
詳しい中身はこちらの英語ドキュメントを参照しましょう。
http://doctrine-orm.readthedocs.org/en/latest/reference/association-mapping.html

具体例として以下の2つのエンティティを考えてみましょう

TblTicket.php

<?php
namespace Hoge\FugaBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\ArrayCollection;

/**
 *
 * @ORM\Table(name="tbl_ticket")
 * @ORM\Entity(repositoryClass="Hoge\FugaBundle\Repository\TicketRepository")
 */
class TblTicket
{
    /**
     * @var bigint $id
     *
     * @ORM\Column(name="id", type="bigint", nullable=false)
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="IDENTITY")
     */
    protected $id;
    
    /**
     * @ORM\ManyToOne(targetEntity="TblTicket", inversedBy="childs")
     * @ORM\JoinColumn(name="parent_id", referencedColumnName="id", nullable=true)
     */
    private $parent;

    /**
     * @ORM\OneToMany(targetEntity="TblTicket", mappedBy="parent")
     */
    private $childs;

     /**
     * @ORM\OneToOne(targetEntity="TblTicketData", mappedBy="ticket", cascade={"persist"})
     */
    private $data;


    public function __construct(){
        $this->childs = new ArrayCollection();
    }
    
    /**
     * Get id
     *
     * @return bigint
     */
    public function getId()
    {
        return $this->id;
    }

    /**
     * get parent
     * @return TblTicket
     */
    public function getParent ()
    {
        return $this->parent;
    }

    /**
     * setparent
     * @param TblTicket $parent
     * @return TblTicket
     */
    public function setParent ($parent)
    {
        $this->parent = $parent;
        return $this;
    }

    /**
     * get data
     * @return TblTicketData
     */
    public function getData ()
    {
        return $this->data;
    }

    /**
     * set data
     * @param TblTicketData $parentData
     * @return TblTicketData
     */
    public function setParentData ($parentData)
    {
        $this->parentData = $parentData;
        return $this;
    }




    /**
     * Add childs
     *
     * @param TblTicket $childs
     */
    public function addChild(TblTicket $childs)
    {
        $this->childs[] = $childs;
    }

    /**
     * Get childs
     *
     * @return Doctrine\Common\Collections\Collection
     */
    public function getChilds()
    {
        return $this->childs;
    }

 
}

TblTicketData.php

<?php
namespace Hoge\FugaBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\ArrayCollection;

/**
 * TblTicketData
 *
 * @ORM\Table(name="tbl_ticket_data")
 * @ORM\Entity
 */
class TblTicketData
{
    /**
     * @var bigint $id
     *
     * @ORM\Column(name="id", type="bigint", nullable=false)
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="IDENTITY")
     */
    private $id;

    /**
     * @ORM\OneToOne(targetEntity="TblTicket", inversedBy="data")
     * @ORM\JoinColumn(name="parent_id", referencedColumnName="id", nullable=false)
     */
    private $ticket;

    /**
     * @var string $content
     *
     * @ORM\Column(name="content", type="string", length=256, nullable=true)
     */
    private $content;


    /**
     * Get id
     *
     * @return bigint
     */
    public function getId()
    {
        return $this->id;
    }

    /**
     * get parent
     * @return TblTicket
     */
    public function getTicket ()
    {
        return $this->ticket;
    }

    /**
     * setparent
     * @param TblTicket $parent
     * @return TblTicket
     */
    public function setTicket ($ticket)
    {
        $this->ticket = $ticket;
        return $this;
    }

    /**
     * get content
     * @return string
     */
    public function getContent ()
    {
        return $this->content;
    }

    /**
     * set content
     * @param string $content
     * @return TblTicket
     */
    public function setContent ($content)
    {
        $this->content = $content;
        return $this;
    }


}

さて、まずは中身の説明に移る前に、このエンティティから生成されるテーブル定義を見てみましょう。

CREATE TABLE tbl_ticket (
  id bigint(20) NOT NULL AUTO_INCREMENT,
  parent_id bigint(20) DEFAULT NULL,
  PRIMARY KEY (id),
  KEY IDX_1A06C41C727ACA70 (parent_id),
) ENGINE=InnoDB  DEFAULT CHARSET=utf8;

CREATE TABLE tbl_ticket_data (
  id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
  ticket_id bigint(20) unsigned NOT NULL,
  content varchar(255) DEFAULT NULL,
  UNIQUE KEY id (id),
  UNIQUE KEY ticket_id 
(ticket_id)
) ENGINE=InnoDB  DEFAULT CHARSET=utf8;

ここから、判断できるポイントを列挙していきましょう。

  • tbl_ticketは木構造となっており、親チケットを自身のテーブルに持っている
  • tbl_ticketは一番親のチケットを判断するために、parent_idにNullを許可している
  • tbl_ticketはそのチケットに付随するデータticket_dataを1つ持っている

それでは、エンティティの中身を見ていきましょう、

@OneToOne

TblTicket.php と TblTicketData.phpにはそれぞれ@OneToOneアノテーションがありますが中身が少々違うようです。

TblTicket.php

@ORM\OneToOne(targetEntity="TblTicketData", mappedBy="ticket", cascade={"persist"})

TblTicketData.php

@ORM\OneToOne(targetEntity="TblTicket", inversedBy="data")
@ORM\JoinColumn(name="parent_id", referencedColumnName="id", nullable=false)

ポイントは

  • 所属する側のデータ(TblTicketData)にはinversedByとJoinColumnを付与する
  • 所属される側のデータ(TblTicket)にはmappedByを付与する
  • JoinColumnによってTblTicketDataのparent_idとTblTicketのid間のリレーションが作られる
  • nullable=falseによって、すべてのデータは必ずTblTicketへの所属が保障される

です。

では、続いて、このデータのInsertに関して考えてみましょう。

パターン1

public function createAction()
{
    $ticket = new TblTicket();
    $data   = new TblTicketData();
    $data->setContent("aaaa");
    $data->setTicket($ticket);
    $em = $this->getDoctrine()->getEntityManager();
    $em->persist($ticket);
    $em->persist($data);
    $em->flush();

    return new Response('Created product');
}

$dataには、ticket_idが必要なので、TblTicketオブジェクトをセットしないとErrorとなってしまいます。

ただし今回は、cascade={“persist”}をTblTicketに付与しているため、以下のコードによってもInsert可能です。

パターン2

public function createAction()
{
    $ticket = new TblTicket();
    $data   = new TblTicketData();
    $data->setContent("aaaa");
    $data->setTicket($ticket);
    $ticket->setData($data);
    $em = $this->getDoctrine()->getEntityManager();
    $em->persist($ticket);
    $em->flush();

    return new Response('Created product');
}

これは、persistの際にエンティティマネージャーが管理していないオブジェクトに関してもまとめてpersistするという設定によるものです。

注意

public function errorAction()
{
    $em     = $this->getDoctrine()->getEntityManager();
    $repo   = $em->getRepository("HogeFugaBundle:TblTicket");
    
    $ticket = $repo->find(1);
    // エラー出るかも
    echo $ticket->getData()->getContent();

    return new Response('get content');
}

mappedByのOneToOneでは、対象エンティティのデータの存在を保証できない(逆の場合は保証される)ので、DataがNullの場合がありえます

OneToManyもやりたかったのですが、量が多くなってしまったのでそれは次回に回します。

投稿者プロフィール

開発 アルバイト
中の人には主に、
PHP・Symfony2系の人と
Ruby・Rails系の人がいます。
ときどきJavascript・データベースにも手を出すかもしれません。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

Time limit is exhausted. Please reload CAPTCHA.