Doctrine2 - один ко многим

Нюансы работы внешних ключей на примере ORM Doctrine2.

Для начала нам понадобятся 2 сущности, например People и News.

Имеем ввиду, что один пользователь может опубликовать несколько новостей (связь один ко многим).

Давайте опишем эти 2 сущности:

<?php
namespace AppBundle\Entity;

use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Entity
 * @ORM\Table(name="people")
 */
class People
{
    /**
     * переменная нужна, чтобы идентифицировать человека
     * @ORM\Id
     * @ORM\Column(type="integer", options={"unsigned": true, "comment" : "ID человека"})
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    private $id = 0;

    /**
     * @ORM\Column(type="string", length=255, options={"default":""})
     */
    private $name = '';

}

и вторая сущность:

<?php
namespace AppBundle\Entity;

use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Entity
 * @ORM\Table(name="news")
 */
class News
{
    /**
     * @ORM\Id
     * @ORM\Column(type="integer", options={"unsigned": true, "comment" : "ID новости"})
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    private $id = 0;

    /**
     * @ORM\Column(type="string", length=255, options={"default":""})
     */
    private $title = '';

    /**
     * ID человека
     * @ORM\ManyToOne(targetEntity="AppBundle\Entity\People")
     * @ORM\JoinColumns({
     *   @ORM\JoinColumn(name="people_id", referencedColumnName="id")
     * })
     */
    private $people;

}

Обратите внимание, что people это переменная, которая будет содержать объект People, а people_id это имя поля в таблице news.

Теперь создадим таблицы в базе данных:

$ app/console doctrine:schema:update --force

И попробуем добавлять записи в базу:

INSERT INTO people (`name`) VALUES ('ivan');// хорошо, строке присвоен id = 1

INSERT INTO news SET title = 'Привет', people_id = 1;// хорошо, строка добавилась

INSERT INTO news SET title = 'Мир', people_id = 2;// хорошо, строка НЕ добавилась, т.к. в people нет строки с id = 2

INSERT INTO news SET title = 'Труд';// ужас, строка добавилась, поле people_id = NULL

Плохо - целостность потеряна! А ведь нам нужно, чтобы последний запрос не сработал, т.к. мы не указали people_id

Мало того, мы спокойно можем выполнить запросы:

UPDATE news SET people_id = 1 WHERE id = 1;

UPDATE news SET people_id = NULL WHERE id = 1;

Кошмар, значит все новости могут потерять владельцев, и не поверите, но это нормальная ситуация, оказывается NULL это значение по-умолчанию.

Но, решить эту проблему все таки можно, хотя и к сожалению не для тех строк, которые уже были добавлены.

Для решения проблемы, полю people_id нужно указать значение по-умолчанию NOT NULL

В Doctrine2 анотации это делается так:

@ORM\JoinColumn(name="people_id", referencedColumnName="id", nullable=false)

Проверим добавление строки в базу данных:

INSERT INTO news SET title = 'Май';// ура, строка НЕ добавилась

INSERT INTO news SET title = 'Мир', people_id = 0;// ура, строка НЕ добавилась

Теперь консистентность данных в порядке (если не брать в счет тех строк, которые были добавлены до значения по-умолчанию NOT NULL).

Хочется заметить, что обычно не делают анотацию OneToMany (и только по необходимости какие-то более сложных выборок используют ManyToOne):

    /**
     * @var int
     *
     * @ORM\OneToMany(
     *     targetEntity="AppBundle\Entity\Image",
     *     mappedBy="AppBundle\Entity\Album"
     * )
     */
    private $images;

благодаря этому мы сможем поднимать сущности Image очень просто:

$people->getImages()->toArray();

Итак, задача выполнена, и можно уже закрыть эту статью, ну а мне хочется еще рассказать о интересных особенностях yml-схем и выборок на основе их.

Yaml-схемы

Doctrine говорит нам, что для двусторонней связи двух объектов, нужно использовать атрибут:

  • mappedBy для связи OneToMany
  • inversedBy для связи ManyToOne, когда нужно сказать, что родительский объект может быть null
  • joinColumn для связи ManyToOne, когда нужно сказать, что родительский объект не может быть null

Я обычно пишу так:

    booking:
      targetEntity: Booking
      inversedBy: comments
      joinColumn:
        nullable: false
    user:
      targetEntity: Entity\BackofficeUser
      inversedBy: bookingsComments
      joinColumn:
        nullable: false

как Вы поняли, это позволяет не писать имя связующего поля, но говорит, чтобы связующий объект не мог быть null

Итак, создаем yml-файл с описанием схемы

AppBundle\Entity\People:
  type: entity
  table: people
  id:
    id:
      type: integer
      generator:
        strategy: AUTO
  fields:
    name:
      column: name
      type: string
      length: 255
      nullable: false
  oneToMany:
    newsItems:
      targetEntity: News
      mappedBy: peopleItem
      onDelete: CASCADE

Обратите внимание, что мы еще не создали файл описания схемы сущности News, но уже указали peopleItem - это имя, которое мы обязаны указать как обратное (обратите внимание, оно обязано быть уникальным во всех описаниях схем).

AppBundle\Entity\News:
  type: entity
  table: news
  id:
    id:
      type: integer
      generator:
        strategy: AUTO
  fields:
    title:
      column: title
      type: string
      length: 255
  manyToOne:
    peopleItem:
      targetEntity: People
      inversedBy: newsItems
      joinColumn:
        name: people_id
        referencedColumnName: id
        nullable: false

в то же время, мы указываем newsItems как обратную связь, это обязательно. Кстати, описание атрибута joinColumn не является обязательным, т.к. Doctrine может самостоятельно разобраться в именах полей.

name: people_id - имя поля, которое будет использоваться для хранения связи news с people (обратите внимание, что мы не объявили people_id в списке полей, т.к. делать это не нужно, потому что Doctrine умеет строить связи только по поляем типа integer).

referencedColumnName: id - имя поля таблицы people, которое будет проставляться в поле people_id, чтобы сопоставлять строки news с people.

Итак, теперь мы можем выполнить команду:

app/console doctrine:schema:create --dump-sql

которая покажет нам внесенные изменения в схему выше описанных таблиц.

А теперь попробуем сделать выборку данных, согласно выше описанных схем:

$qb = $this->em->createQueryBuilder();
$qb->select('PeopleAlias, NewsAlias')
->from(People::class, 'PeopleAlias')
->innerJoin('People.newsItems', 'NewsAlias')
->getQuery()
->useQueryCache(false)
->useResultCache(false)
->execute();

Обратите внимание, что для выборки новостей, мы использовали указание имени связи из схемы People, а не класса (это очень тонкий момент, который лично до меня дошел далеко не сразу).

Удачи господа.

p.s. В Doctrine есть очень интересная особенность, называется она FETCH_EAGER - подгружать данные связей в момент выборки родительских данных:

$query = $em->createQuery("SELECT p FROM AppBundle\Entity\People p");
$query->setFetchMode("AppBundle\Entity\News", "newsItems", \Doctrine\ORM\Mapping\ClassMetadata::FETCH_EAGER);
$query->execute();

Благодаря такой особенности, Doctrine подгрузит данные News до того, как Вы решите ими воспользоваться (в настоящий момент (2016.02) этот хинт не работает).

Есть и обратный хинт - когда Вам нужно выполнить запрос, но при этом не поднимать связанные сущности:

$query->setHint(\Doctrine\ORM\Query::HINT_FORCE_PARTIAL_LOAD, 1);

Благодаря этому хинту Doctrine поднимет только только сущности People и не поднимет связанные сущности News (даже если Вы в своей схеме укажите fetch="EAGER")

p.s. 2 хочу обратить Ваше внимание, что для people мы использовали инструкцию Delete: CASCADE - это означает, что при удалении объекта People, Doctrine будет удалять сущности News связанные с People. Насколько это хорошая идея - решать вам, но лично мне кажется, что если пхп-скрипт упадет, то мы получим неконсистентность данных и поэтому вместо Delete: CASCADE я предпочитаю использование cascade: ["persist", "remove"]

AppBundle\Entity\People:
  type: entity
  table: people
  id:
    id:
      type: integer
      generator:
        strategy: AUTO
  fields:
    name:
      column: name
      type: string
      length: 255
      nullable: false
  oneToMany:
    newsItems:
      targetEntity: News
      mappedBy: peopleItem
      cascade: ["remove"]

cascade: ["remove"] - говорит Doctrine, что для people и news нужно использовать Foreign Key. Т.е. выполнив команду:

app/console doctrine:schema:create --dump-sql

Вы увидите, что Doctrine попытается создать FK:

ALTER TABLE news ADD CONSTRAINT FK_1 FOREIGN KEY (people_id) REFERENCES people (id)

что означает, что за консистентность данных будет отвечать база данных, а не Doctrine.

p.s. 3: cascade: ["persist"] - означает, что все данные, которые будут связаны, не нужно сохранять в Doctrine $manager->persist($news); перед использованием $manager->flush();

Полезное: 1 - 2

Оцени публикацию:
  • 6,25
Оценили человек: 6

Похожие статьи:

Справочники и учебники:


Комментарии посетителей:
  • "Doctrine2 - один ко многим"... то, что вообще возможна такая фраза - уже говорит о том, что ORM - во многих случаях - излишество. Ведь цель использования ORM - скрыть от разработчика все то, что присуще реляционным БД, а оставить только классы, объекты...

    А здесь - на тебе: вылезают на белый свет отношения между строками в таблицах.
    08 февраля 2017, 07:18 коммент полезен : +1 # --- (гость)
Предложения и пожелания:
Ваше имя:
Ваш E-mail:
Сколько будет Οдин + Τри
Главная
X

Новые заметки:

Про что мы забываем когда делаем оценку задачи по времени

Список вопросов для собеседования разработчика по телефону

Symfony2 авторизация без Doctrine2 для чайника

Phpstorm7 LiveEdit

Жесткий хабр или не хабр, тогда кто?

Яндекс.Деньги мошенничество

Как узнать какие страницы в поиске яндекса или это секрет

Последние комменты:

Yapro CMS:

Здравствуйте, Гость | Войти | Регистрация | Карта сайта | RSS ленты | Ошибка в тексте? Выделите её мышкой и нажмите: Ctrl + Enter

youtube.com/watch?v=7hFivbgIEqk

При полном или частичном использовании материалов данного сайта, ссылка на сайт "yapro.ru" обязательна как на источник информации.
Автоматический импорт материалов и информации с сайта запрещен.
Copyrights © 2007 - 2018 YaPro.Ru

Главная » Веб-мастеру » MySQL »