Джентльменский набор Doctrine 2 для Symfony 3.3.6: Создание сущности, ассоциации и рекурсивные связи

  • Tutorial


Доброго дня, читатель!

Что мы будем делать с вами по ходу чтения статьи


  • Создадим простые сущности
  • Немного поговорим об ORM аннотациях
  • Реализуем ассоциации:
    1. Двунаправленные связи Один к Одному
    2. Двунаправленные связи Один ко Многим
    3. Двунаправленные связи Многие ко Многим
    4. Рекурсивные связи

  • Поиграемся этими сущностями с помощью фикстур


Кому интересна будет статья:


Читателю будет интересно, если он уже достаточно хорошо ориентируется в Symfony — уже был сделан хотя бы один простенький проект. Интерес к этой информации я встречал и у продвинутых разработчиков, так что и им можно будет пройтись «по диагонали». Собственно статья и пишется как ответ на частые вопросы ребят, с которыми я работаю. Теперь я смогу просто кидать им ссылку на эту статью.

Создание сущности


Буду все консольные команды писать в манере, как если Composer не установлен в системе.

Установим для начала Symfony:
# Не забудьте на этом этапе указать верные пароль и логин для базы данных.
php composer.phar create-project symfony/framework-standard-edition ./gentlemans_set "v3.3.6"
# Переходим в папку проекта после установки
cd gentlemans_set/
# Запускаем создание базы данных
php bin/console doctrine:database:create


Кто как привык, но я не люблю писать собственноручно много кода. Если есть возможность использовать автогенерацию чего либо в Symfony, то я ее использую и вам советую, так как это минимизация человеческого фактора, да и просто разгружает ваш ум. Сгенерируем две простенькие сущности через консольные команды Symfony:
# Создаем сущность User
php bin/console doctrine:generate:entity --entity=AppBundle:User --fields="username:string(127) password:string(127)" -q
# Создаем сущность Product
php bin/console doctrine:generate:entity --entity=AppBundle:Product --fields="name:string(127) description:text" -q

В результате у нас создались два класса сущности:
  • src/AppBundle/Entity/Product.php
  • src/AppBundle/Entity/User.php


И два класса репозиториев для соответствующих сущностей:
  • src/AppBundle/Repository/ProductRepository.php
  • src/AppBundle/Repository/UserRepository.php


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

Сущность src/AppBundle/Entity/Product.php сразу после генерации:
// ...

/**
 * Product
 *
 * @ORM\Table(name="product")
 * @ORM\Entity(repositoryClass="AppBundle\Repository\ProductRepository")
 */
class Product
{
    /**
     * @var int
     *
     * @ORM\Column(name="id", type="integer")
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    private $id;

    /**
     * @var string
     *
     * @ORM\Column(name="name", type="string", length=127)
     */
    private $name;

// ...


Проверяем какой будет создан SQL запрос для создания структуры базы данных:
php bin/console doctrine:schema:create --dump-sql
CREATE TABLE user (id INT AUTO_INCREMENT NOT NULL, username VARCHAR(127) NOT NULL, password VARCHAR(127) NOT NULL, PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE = InnoDB;
CREATE TABLE product (id INT AUTO_INCREMENT NOT NULL, name VARCHAR(127) NOT NULL, description LONGTEXT NOT NULL, PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE = InnoDB;



В результате генерации сущности создается избыточные аннотации и на данном этапе сущность будет иметь абсолютно такое же поведение, если мы сократим аннотации до варианта:

Почищенный вариант
// src/AppBundle/Entity/Product.php
// ...

/**
 * @ORM\Entity(repositoryClass="AppBundle\Repository\ProductRepository")
 */
class Product
{
    /**
     * @var int
     *
     * @ORM\Column(type="integer")
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    private $id;

    /**
     * @var string
     *
     * @ORM\Column(type="string", length=127)
     */
    private $name;

// ...

Проверяем какой будет создан SQL запрос для создания структуры базы данных и видим в точности тот же результат:
php bin/console doctrine:schema:create --dump-sql
CREATE TABLE user (id INT AUTO_INCREMENT NOT NULL, username VARCHAR(127) NOT NULL, password VARCHAR(127) NOT NULL, PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE = InnoDB;
CREATE TABLE product (id INT AUTO_INCREMENT NOT NULL, name VARCHAR(127) NOT NULL, description LONGTEXT NOT NULL, PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE = InnoDB;



На что я хотел обратить внимание приводя такой пример? В подавляющем
большинстве случаев мы создаем структуру базы данных автоматически используя при этом сущность как образец. Я удалил все части аннотаций, где явно указывалось как назвать таблицу для этой сущности и как назвать каждое из полей. Эти конфигурационные данные нужны только когда мы работаем с уже существующей базой данных и, по какому-то стечению обстоятельств, имена полей не соответствуют наименованию свойств в сущности. Если же их не указывать, то Doctrine назовет таблицу в соответствии с именем класса сущности, и поля назовет в соответствии с наименованием свойств сущности. И это правильный подход, так как это разработка по пути наименьшего удивления — имена полей в базе данных совпадают со свойствами сущности и при этом нет какой-то третьей стороны, которая на это влияет и может «удивить».

Но вы скажете, что мы тогда лишаемся уровня абстракции и при каждом рефакторинге сущности нам придется обновлять и структуры баз данных. Ответом на это, будет: как ни крути, но неисправленное расхождение названий свойств сущности и полей в базе данных в результате рефакторинга — это лишний пункт в копилку технического долга проекта.

Утрирую ситуацию: по прошествую пары лет, когда ваш проект разросся до мирового масштаба и вы уже разнесли базу данных по целому облаку серверов, чтобы справляться с непосильной нагрузкой, вы можете обнаружить в базе данных таблицу с именем 'this_may_work' и полями: 'id', 'foo', 'bar' и 'some_field_2'. Оправдание, что в сопоставленной сущности названия имеют более глубокий смысл, окажется несущественным.

Запускаем генерацию структуры базы данных:

php bin/console doctrine:schema:create


Теперь у нас есть две сущности, сопоставленные с созданными таблицами в базе данных. Мы можем создавать их экземпляры, сохранять в базе данных, а потом производить выборку из базы данных. Для демонстрации создания экземпляров сущностей в этой статье я решил использовать фикстуры, а выборку буду демонстрировать в методах репозиториев сущностей. Репозитории сущностей у нас уже имеются, а механизма работы с фикстурными данными у нас еще нет.

Устанавливаем фикстуры



Затягиваем зависимость doctrine/doctrine-fixtures-bundle в наш проект:
php composer.phar require --dev doctrine/doctrine-fixtures-bundle


Подключаем бандл зависимости в ядре Symfony:
    // app/AppKernel.php
    // ...
    if (in_array($this->getEnvironment(), array('dev', 'test'))) {
	  // ...
        $bundles[] = new Doctrine\Bundle\FixturesBundle\DoctrineFixturesBundle();
    }
    // ...


Теперь мы готовы писать фикстуры. Создадим для них директорию:

mkdir -p src/AppBundle/DataFixtures/ORM


И первоначальный вид фикстурных данных:

src/AppBundle/DataFixtures/ORM/LoadCommonData.php
<?php
namespace AppBundle\DataFixtures\ORM;

use AppBundle\Entity\Product;
use AppBundle\Entity\User;
use Doctrine\Common\DataFixtures\FixtureInterface;
use Doctrine\Common\Persistence\ObjectManager;

class LoadCommonData implements FixtureInterface
{
    public function load(ObjectManager $manager)
    {
        $user = new User();
        $user
            ->setPassword('some_password')
            ->setUsername('Аноним');

        $manager->persist($user);

        $product = new Product();
        $product
            ->setName('Подушка для программиста')
            ->setDescription('Подушка специально для программистов. Мягкая и периодически издает звук успешно собранного билда.');

        $manager->persist($product);

        $manager->flush();
    }
}



Теперь такой командой мы можем загрузить эти фикстурные данные в базу данных:

php bin/console doctrine:fixtures:load


Двунаправленная связь «один к одному»



Выше мы получили приложение с двумя несвязанными сущностями. Что, по сути, делает это приложение абсолютно бессмысленным. Большинство данных в реальных приложениях так или иначе связано с другими данными. Каждый ваш палец вверх, каждый комментарий на странице социальных сетей связан с сущностью пользователя, иначе они бесполезны.

Связь «один к одному» — мне в проектах встречается еще реже чем связь «многие ко многим». Что естественно, если следовать нормальным формам. Но знать как она реализуется нужно.

Для примера создадим сущность продавца Seller, которая будет связана один к одному с сущностью пользователя User, если данный пользователь является продавцом:

php bin/console doctrine:generate:entity --entity=AppBundle:Seller --fields="company:string(127) contacts:text" -q


Затем изменяем сущность пользователя, для создания связи с сущностью продавца:
src/AppBundle/Entity/User.php
<?php
// ...
class User
{
    // ...
    /**
     * @ORM\OneToOne(targetEntity="Seller")
     */
    private $seller;
    // ...
}



Изменяем сущность продавца, добавляя связь с сущностью пользователя:
src/AppBundle/Entity/Seller.php
<?php
// ...
class Seller
{
    // ...
    /**
     * @ORM\OneToOne(targetEntity="User", mappedBy="seller")
     */
    private $user;
    // ...
}



Заметьте, что я сразу привел пример двунаправленной связи… вообще не вижу смысла делать однонаправленные связи. Базу данных двунаправленная связь не меняет, а в программном коде дает большое удобство. По моему, возможно субъективному, мнению даже в плане оптимизации по быстродействию и использованию ОЗУ однонаправленная связь ничего не дает. (Оговорка: в последнем разделе статьи, где рассказываю про рекурсивную связь, я привожу единственно мне известный удачный пример однонаправленной связи.)

Так же заметьте, что я опять опустил аннотации. На этот раз аннотации @JoinColumn. Эти аннотации нужны в точности для того же, для чего нужны были и удаленные мною ранее аннотации — для указания с каким именем создастся поле в базе данных и на какое именно поле в базе данных будет создан внешний ключ. Все это сделается и без этой аннотации в лучшем виде.

Запускаем автогенерацию getter/setter методов во всех сущностях нашего бандла. Эти методы создадутся для новых свойств сущностей:
php bin/console doctrine:generate:entities AppBundle


Так же в нужно привести структуру базы данных в соответствие с нашими сущностями:
php bin/console doctrine:schema:update --force


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

Ну и в фикстурах создадим еще одного пользователя, который уже будет связан с сущностью продавца.
src/AppBundle/DataFixtures/ORM/LoadCommonData.php
<?php

namespace AppBundle\DataFixtures\ORM;

use AppBundle\Entity\Product;
use AppBundle\Entity\Seller;
use AppBundle\Entity\User;
use Doctrine\Common\DataFixtures\FixtureInterface;
use Doctrine\Common\Persistence\ObjectManager;

class LoadCommonData implements FixtureInterface
{
    public function load(ObjectManager $manager)
    {
        $user = new User();
        $user
            ->setPassword('some_password')
            ->setUsername('Аноним');

        $manager->persist($user);

        $seller = new Seller();
        $seller
            ->setCompany("Рога и копыта")
            ->setContacts("Кудыкина Гора");

        $manager->persist($seller);

        $seller_user = new User();
        $seller_user
            ->setPassword('some_password')
            ->setUsername('Барыга')
            ->setSeller($seller);

        $manager->persist($seller_user);

        $product = new Product();
        $product
            ->setName('Подушка для программиста')
            ->setDescription('Подушка специально для программистов. Мягкая и периодически издает звук успешно собранного билда.');

        $manager->persist($product);

        $manager->flush();
    }
}



Запускаем загрузку фикстурных данных и в базе данных можем полюбоваться на созданные записи.

Если у вас проблемы с пониманием каким образом этими аннотациями была создана связь между сущностями, и если официальная документация Doctrine 2 вам не помогла, то смотрим на картинку:



Здесь стрелочками указано откуда что бралось, чтобы быть вписанным в аннотацию.

Стоит отметить, что аннотации в обоих сущностях получились очень похожи. Синтаксически они отличаются лишь атрибутами mappedBy и inversedBy. Но это отличие принципиальное. Сущность со стороны которой стоит атрибут inversedBy обычно считается подчиненной сущности, у которой стоит атрибут mappedBy. Выходит так, что у нас сущность User подчинена сущности Seller. Выражается это в том, что в базе данных именно таблица user содержит внешний ключ на таблицу seller, а не наоборот. Так же это влияет и на тот код, который мы написали в фикстурных данных — заметьте, что мы назначили продавца пользователю методом setSeller, а не пользователя продавцу. Последний вариант попросту никаким образом не отобразится в базе данных. То есть надо понимать, что именно объекту подчиненной сущности указывается с кем она связана.

Двунаправленная связь «один ко многим»



Самый распространенный вид связи. Следовательно нужно уметь реализовать его даже в состоянии алкогольного опьянения без Интернета где-нибудь на тропических островах будучи заваленным в системе сообщающихся пещер без компьютера, без рук, без головы. В общем одним своим существованием вы должны нести связь «один ко многим» в проекты на Symfony.

Как и было выше — будем писать пример двунаправленной связи. Для этого свяжем, уже существующие сущности: Product и Seller.

Отвечаем на вопрос: кого будет в этой связи много, а кого один. Обычно получается, что много продуктов у одного продавца. Таким образом у нового свойства сущности Seller будет аннотация @ORM\OneToMany, а свойства сущности Product — @ORM\ManyToOne. В остальном все так же как и у вида связи «один к одному». Однако тут уже не получится свободно менять местами атрибуты mappedBy и inversedBy, так как сущность со стороны связи «много» всегда подчинена сущности со стороны «один». Соответственно и внешние ключи в базе данных всегда будут только у продуктов. Продолжая логику: именно продукту, как подчиненной сущности, будут назначаться продавцы методом setSeller, который мы напишем ниже, чтобы это назначение сохранилось в базе данных.

Изменяем сущность продавца, для создания связи с сущностью продукта:
src/AppBundle/Entity/Seller.php
<?php
// ...
class Seller
{
    // ...
    /**
     * @ORM\OneToMany(targetEntity="Product", mappedBy="seller")
     */
    private $products;
    // ...
}



Изменяем сущность продукта, добавляя связь с сущностью продавца:
src/AppBundle/Entity/Product.php
<?php
// ...
class Product
{
    // ...
    /**
     * @ORM\ManyToOne(targetEntity="Seller", inversedBy="product")
     */
    private $seller;
    // ...
}



Снова запускаем автогенерацию getter/setter методов во всех сущностях нашего бандла.
Так же опять запускаем команду на обновление структуры базы данных. (Смотреть выше, если не запомнили команды).

Назначаем продавца продукту, который уже создается у нас в фикстурных данных:

// src/AppBundle/DataFixtures/ORM/LoadCommonData.php
// ...
$product = new Product();
        $product
            ->setSeller($seller)
            ->setName('Подушка для программиста')
            ->setDescription('Подушка специально для программистов. Мягкая и периодически издает звук успешно собранного билда.');
// ...


Запускаем загрузку фикстурных данных и в базе данных можем полюбоваться на то, что запись продукта 'Подушка для программиста' обзавелась значением в поле seller_id. Это то чего мы и добивались — теперь в нашем продукте у каждого продавца может быть много товаров.

Код для проверки в любом экшэне любого контроллера:
        // ...
        $product = $this->getDoctrine()
            ->getRepository("AppBundle:Product")
            ->find(6);

        dump($product->getSeller()->getCompany());
        // ...


6 — это id моего продукта на момент написания статьи (при запуске загрузки фикстурных данных старые записи в таблицах базы данных удаляются, но автоинкремент первичного ключа не сбрасывается). Здесь мы получили продукт из репозитория по значению его первичного ключа (поле id), и методом getSeller получили связанную с ним сущность продавца. На выходе получаем:
«Рога и копыта»

Попробуем теперь найти все продукты продавца. В пример код для любого экшэна любого контроллера:
        // ...
        $seller = $this->getDoctrine()
            ->getRepository("AppBundle:Seller")
            ->find(5);

        $names = [];
        foreach($seller->getProducts() as $product) {
            $names[] = $product->getName();
        }
        dump($names);
        // ...


Вывод функции dump:
array:1 [▼
0 => "Подушка для программиста"
]


Как по мне, результат достигнут.

Двунаправленная связь «многие ко многим»



Очень полезно знать как такая связь организовывается, хотя она и встречается гораздо реже связи «один ко многим». Полагаю вы уже должны знать, что связь «многие ко многим» в базе данных организовывается путем связывания двух таблиц посредством третьей, сводной таблицы. Сущность для этой третьей таблицы создавать ненужно — Doctrine самостоятельно создаст нам эту таблицу при запуске команды на создание или на обновление структуры базы данных.

В нашем разрастающемся проекте мы создадим сущность Category и свяжем ее «многие ко многим» с Product. Тем самым мы создадим возможность одному продукту быть сразу в нескольких категориях. Связь «многие ко многим» тут выявляется просто: многие продукты могут лежать в одной категории, один продукт может лежать во многих категориях — что со стороны продукта, что со стороны категории стоит знак «много», значит нужна связь «многие ко многим».

Создаем сущность категории:
php bin/console doctrine:generate:entity --entity=AppBundle:Category --fields="name:string(127)" -q


Изменяем сущность категории, для создания связи с сущностью продукта:
src/AppBundle/Entity/Category.php
<?php
// ...
class Category
{
    // ...
    /**
     * @ORM\ManyToMany(targetEntity="Product", mappedBy="categories")
     */
    private $products;
    // ...
}



Изменяем сущность продукта, добавляя связь с сущностью категории:
src/AppBundle/Entity/Product.php
<?php

// ...
class Product
{
    // ...
    /**
     * @ORM\ManyToMany(targetEntity="Category", inversedBy="products")
     */
    private $categories;
    // ...
}



Запускаем автогенерацию getter/setter методов во всех сущностях нашего бандла. Так же опять запускаем команду на обновление структуры базы данных. (Смотреть выше, если не запомнили команды).

Фикстуры дополняются созданием двух категорий создадим еще один продукт и назначим категории продуктам. Вышло так, что все продукты лежат во всех категориях. Да простит нас Бог Маркетинга, но это было сделано просто для наглядности.

src/AppBundle/DataFixtures/ORM/LoadCommonData.php
<?php

namespace AppBundle\DataFixtures\ORM;

use AppBundle\Entity\Category;
use AppBundle\Entity\Product;
use AppBundle\Entity\Seller;
use AppBundle\Entity\User;
use Doctrine\Common\DataFixtures\FixtureInterface;
use Doctrine\Common\Persistence\ObjectManager;

class LoadCommonData implements FixtureInterface
{
    public function load(ObjectManager $manager)
    {
        $user = new User();
        $user
            ->setPassword('some_password')
            ->setUsername('Аноним');

        $manager->persist($user);

        $seller = new Seller();
        $seller
            ->setCompany("Рога и копыта")
            ->setContacts("Кудыкина Гора");

        $manager->persist($seller);

        $seller_user = new User();
        $seller_user
            ->setPassword('some_password')
            ->setUsername('Барыга')
            ->setSeller($seller);

        $manager->persist($seller_user);

        $category = new Category();
        $category->setName('Главная');

        $manager->persist($category);

        $category2 = new Category();
        $category2->setName('Неглавная');

        $manager->persist($category2);

        $product = new Product();
        $product
            ->setSeller($seller)
            ->setName('Подушка для программиста')
            ->setDescription('Подушка специально для программистов. Мягкая и периодически издает звук успешно собранного билда.');

        $manager->persist($product);

        $product2 = new Product();
        $product2
            ->setSeller($seller)
            ->setName('Часы для программиста')
            ->setDescription('Часы, в которых каждая минута содержит 64 секунды, каждый час - 64 минуты, а в сутках 16 часов');

        $manager->persist($product2);

        $product->addCategory($category);
        $product->addCategory($category2);

        $product2->addCategory($category);
        $product2->addCategory($category2);

        $manager->flush();
    }
}



Тут важно заметить, что не смотря на равенство сущностей участвующих в связи, мы все равно должны использовать атрибуты mappedBy и inversedBy. И это неожиданно, но поведение, которое мы наблюдали при создании связи «один ко многим» здесь сохраняется — объект сущности, со стороны которой был указан атрибут mappedBy, должна назначаться объекту сущности, со стороны которой указан атрибут inversedBy. Иначе записи в сводной таблице не появятся. Выходит так, что волей-неволей нам приходится выделять и держать в уме какая из сущностей в этой связи является подчиненной и именно ее объектам назначать объекты второй сущности. В данном случае подчиненная сущность — Product и мы ее объектам назначаем объекты сущности Category. Если кто знает как это обойти без костылей, изменяя только аннотации — напишите в комментариях. Я же обычно обхожусь небольшой модификацией setter-а главной сущности (как я недавно обнаружил — в официальной документации Doctrine2 описывается такой же вариант решения проблемы):

    // ...
    /**
     * Add product
     *
     * @param \AppBundle\Entity\Product $product
     *
     * @return Category
     */
    public function addProduct(\AppBundle\Entity\Product $product)
    {
        $product->addCategory($this);  // Эта строчка дает мне уверенность, что не только категории назначен продукт, но продукту назначена категория
        $this->products[] = $product;

        return $this;
    }
    // ...


Запускаем загрузку фикстурных данных в базу данных, проверяем базу данных и видим, что сводная таблица product_category содержит 4 записи каждая из которых устанавливает связь между определенной категорией и определенным продуктом:



Рекурсивные связи


Рекурсивной связь называется, если она выстроена у сущности с ней же самой. И эта связь может быть «один к одному», «один ко многим» и «многие ко многим». Есть где разгуляться. Разберем для начала вариант, который не затрагивается в официальной документации — однонаправленная рекурсивная связь «многие ко многим».

Изменяем сущность пользователя, добавляя связь «многие ко многим» с самой собой:
src/AppBundle/Entity/User.php
<?php
// ...
class User
{
    // ...
    /**
     * @ORM\ManyToMany(targetEntity="User")
     */
    private $friends;
    // ...
}



Это пример удачного использования однонаправленной связи, так сопоставление сущности происходит с самой собой по первичному ключу посредством сводной таблицы — значит и двунаправленности не требуется. Запустите генерацию сущностей для бандла, обновление структуры данных и можно будет дополнить наши фикстуры строками:

// ...
    public function load(ObjectManager $manager)
    {
        // ...
        $user->addFriend($seller_user);
        $seller_user->addFriend($user);

        $manager->flush();
    }
// ...


Теперь каждый пользователь у нас может иметь сколько угодно пользователей-друзей. Это прекрасный пример неиерархичной структуры когда нет отношения главный/подчиненный, родитель/потомок как в древовидной структуре.

А теперь разберем древовидную структуру, путем создания рекурсивной связи. Изменяем сущность категории, добавляя связь «один ко многим» с самой собой:
src/AppBundle/Entity/Category.php
<?php
// ...
class Category
{
    // ...
    /**
     * @ORM\OneToMany(targetEntity="Category", mappedBy="parent")
     */
    private $children;

    /**
     * @ORM\ManyToOne(targetEntity="Category", inversedBy="children")
     */
    private $parent;
    // ...
}



Думаю, что тут все и так наглядно — у категории появились родитель и потомки.
После обновления структуры базы данных и генерации getter/setter методов дополните фикстурные данные указанием кто кому является родителем и кто кому является потомком:

// ...
    public function load(ObjectManager $manager)
    {
        // ...
        $category2->setParent($category);
        $category->addChild($category2);

        $manager->flush();
    }
// ...


Рекурсивную связь «один к одному» разбирать не буду — если такое потребуется, то методом аналогии, думаю, не проблема будет и самим написать.
Построение иерархической недревовидной структуры, где родителей как и потомков может быть больше одного, методом той же аналогии сделать, думаю, тоже никакой проблемы нет.

Полезно


Пользуйтесь панелью инструментов Symfony, которая отображается в нижней части страницы, когда вы запускаете ваше приложение в окружении разработчика:



Если вы напутали с аннотациями в сущностях, то с большой вероятностью Doctrine это заметит и выдаст вам здесь ошибку. Кликните по этой иконке и сможете детально почитать об ошибке, что должно вас вывести из затруднений.

На посошок


Планировал затронуть темы полиморфных связей вместе с Signle Table Inheritance, но статья и без того выросла непомерно. Так что оставлю все это про запас. Пишите в комментариях, если я где чего напутал, при таком объеме текста глаз сильно мылится.

Проект, получившийся в результате написания статьи, выложу тут:


Но тем, кто только изучает тему связей между сущностями, советую проделать всю работу самому, а не брать проект готовым — это неплохая тренировка. В статье есть все, чтобы сделать проект с нуля.
Метки:
Поделиться публикацией
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама
Комментарии 8
  • 0

    Вообще, двусторонние связи не очень хорошо, особенно связи ManyToMany. Помнится, об этом говорил Marco Pivetta в своих докладах.
    Самая простая проблема с которой мы можем столкнуться, это синхронизация связей объектов. Хотя может её пофиксил. Я с ней сталкивался около 4 лет назад.


    Рекомендую всем к прочтению статью Marco Pivetta о гидрации данных:
    https://ocramius.github.io/blog/doctrine-orm-optimization-hydration/

    • 0
      Additionally, reduce the amount of bi-directional associations to the strict necessary.

      After all, code that is not required should not be written in first place.


      Спасибо за статью, данная цитата заставляет задуматься. Я вернулся к работе над PHP проектами около года назад и с тех пор пользовал Доктрину и в хвост и в гриву даже с большими базами данных. Нареканий даже в плане быстродействия не заметил — все в рамках приемлемого.

      С другой стороны помню печальный опыт семилетней давности. Я очень часто натыкался на превышения допустимых объемов используемой памяти при работе с таблицами от 10к записей. Местами даже самим приходилось допиливать. Ограничивали буквально все. В одной критической сущности вообще от связей отказались, так как она была завязана на скрипт-краулер (он работал не менее минуты) и, как ни удаляй уже не нужные объекты из памяти, где-то внутри все равно происходило паразитное накопление приводящее к фатальным ошибкам.

      С тех пор я вижу три фактора, которые на мой взгляд могли исключить эту проблему:
      • существенная эволюция PHP, а именно появление Слабых ссылок
      • сильно не вникал, но раз уж существует ReactPhp, то и со сборщиком мусора не хило поработали в новых версиях
      • семилетний этап развития самой библиотеки Doctrine


      Вообще поднятая вами тема заинтриговала. С пол пинка я не нашел достаточно информации. Если кто-то найдет пруфы к текущему состоянию дел на этот счет или сможет воспроизвести проблему — будет круто, поразбираем.
    • 0
      php bin/console doctrine:generate:entity --entity=AppBundle:User --fields=«username:string(127) password:string(127)» -q

      Это ад.
      Вы сущность на 2-3 десятка полей так же задаёте?

      • 0
        Вы сущность на 2-3 десятка полей так же задаёте?

        Разумеется нет! Но чаще большие сущности не появляются сразу с огромным количеством полей. Они такими становятся в процессе разработки. Так что создать через консоль сущностью с 3-мя полями на этапе прототипизирования проще чем вручную — сами создаются файл сущности и файл репозитория сущности + папка для репозиториев сущностей, если не создана до того. А потом уже эта заготовка подходит для заполнения произвольным набором полей.
        • 0
          Это зависит от того какой у вас есть опыт.

          Моя практика показывает, что при наличии хорошей экспертизы в предметной области ваши сущности сразу будут большими. Но только те, кто действительно это требует.

          Чаще сущность появляется сразу маленькой у человека уже с опытом в разработке, но без опыта в предметной области.

          Но в любом случае проектировать БД посредством ERM диаграмм проще чем писать в консоли что-то невнятное.
          А уж потом по готовой схеме генерировать классы Entity.
          Консольная команда для этого имеется
          image
          • 0

            Добавлю свои 5 копеек.
            Этот подход называется Data-Driven Design.
            С недавних пор, я предпочитаю использовать Domain-Driven Design методологию.
            Не буду утверждать, что она лучше. Пусть каждый решает сам как и где ее применять.
            Скажу лишь, что я в процессе разработки описываю доменные сущности и бизнес транзакции и лишь потом описываю маппинг на БД и генерю миграцию.
            Чтоб не повторяться, вот хорошая статья по сравнению этих методологий.

            • 0
              Для DDD у вас в первую очередь должна быть хорошая экспертиза в прикладной области (т.е. в той области, которую вы автоматизируете).

              В этом случае оно взлетает отлично.
      • 0
        Мы тут помогаем ребятам без опыта, так что давайте поделимся с ними ссылочкой на

        А уж потом по готовой схеме генерировать классы Entity.
        Консольная команда для этого имеется


        How to Generate Entities from an Existing Database

        Называется это Reverse Engineering — так же называется и аналогичная функция в MySQLWorkbench. MySQLWorkbench переводится как «MySQL скамейка» (Шутка. Называю ее так за исключительную глючность в Линукс среде)

        Спасибо Fortop за дополнительную инфу для читателей. Хоть я и не разделяю таких подходов — они могут быть полезны для многих в определенных ситуациях.

        Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.