Mapping сущностей на таблицы в БД

Конфигурирование мэппинга сущностей и агрегатов на таблицы в БД. Создание собственных типов. Работа со вложенными объектами и коллекциями.

  • 00:01:18 - Переименование класса Network
  • 00:02:13 - Какие таблицы нам нужны в базе данных
  • 00:05:46 - Привязка к таблице
  • 00:10:33 - Мэппинг примитивных типов
  • 00:12:27 - Кастомные типы для простых объектов-значений
  • 00:23:38 - Embedded для сложных объектов-значений токенов
  • 00:32:34 - Мэппинг коллекции объектов-значений networks через сущность-носитель
  • 00:47:40 - Промежуточные итоги
  • 00:49:35 - Устройство сохранения и поиска в Doctrine
  • 00:52:20 - Реализация UserRepository
  • 01:02:54 - Реализация Flusher
  • 01:03:05 - Почему не используем напрямую EntityManager и наследование
Скрытый контент
Комментарии (43)
Arunas

Да, спасибо.

Ответить
Роман

Как то все уж очень сложно получается. Свой велосипед что ли.. Кто как думает?

Ответить
Arunas

Думаю доверять Дмитрию, падождать результата - задействие Doctrine ORM на практике. Свой велосипед лучше, чем чужой неизвестный.

Ответить
elmut

EntityManagerFactory Класс буду внедрять луче чем zend.

    public function getDoctrine(): array
    {
        return [
                'configuration' => [
                    'orm_default' => [
                        'result_cache' => getenv('DOCTRINE_CACHE_TYPE'),
                        'metadata_cache' => getenv('DOCTRINE_CACHE_TYPE'),
                        'query_cache' => getenv('DOCTRINE_CACHE_TYPE'),
                        'hydration_cache' => getenv('DOCTRINE_CACHE_TYPE'),
                    ],
                ],
                'connection' => [
                    'orm_default' => [
                        'driver_class' => Doctrine\DBAL\Driver\PDOPgSql\Driver::class,
                        'params' => [
                            'host' => getenv('POSTGRES_HOST'),
                            'port' => getenv('POSTGRES_PORT'),
                            'user' => getenv('POSTGRES_USER'),
                            'password' => getenv('POSTGRES_PASSWORD'),
                            'dbname' => getenv('POSTGRES_DB_NAME'),
                        ],
                    ],
                ],
                'types' => [
                    UuidType::NAME => UuidType::class,
                ],
                'driver' => [
                    'orm_default' => [
                        'class' => Doctrine\Common\Persistence\Mapping\Driver\MappingDriverChain::class,
                        'drivers' => [],
                    ],
                ],
        ];
    }

ModelUser

    public function getDoctrineEntities(): array
    {
        return [
            'driver' => [
                __NAMESPACE__.'_driver' => [
                    'class' => \Doctrine\ORM\Mapping\Driver\AnnotationDriver::class,
                    'cache' => getenv('DOCTRINE_CACHE_TYPE'),
                    'paths' => [__DIR__.'/Model/Entity'],
                ],
                'orm_default' => [
                    'drivers' => [
                        __NAMESPACE__ => __NAMESPACE__.'_driver',
                    ],
                ],
            ],
            'types' => [
                RoleType::NAME => RoleType::class,
                StatusType::NAME => StatusType::class,
                \Ramsey\Uuid\Doctrine\UuidType::NAME => \Ramsey\Uuid\Doctrine\UuidType::class,
            ],
        ];
    }
Ответить
Ром

Наоборот, всё по полочкам. Структура при которой изменения будут вноситься намного быстрей и безболезненней, деградация кода с внедрением нового функционала не такая быстрая. И это не велосипед, а вполне конкретные шаблоны.

Ответить
Arunas

не заметил, где получаем вошедший-зарегистрированный в систему пользователь - его id?

Ответить
Дмитрий Елисеев

Контроллеров ещё нет, так что пока нигде.

Ответить
Arunas

сегодня папробовал composer update (в Makefile вместо composer install). Make init: всё вроди обновлялось, но застрял vimeo/psalm: ошибка Updating vimeo/psalm (3.8.0 => 3.9.3): Update failed (Could not delete /app/vendor/vimeo/psalm/src/Psalm/Issue:) Это очень плохо?

Ответить
Дмитрий Елисеев

Удалите целиком папку vendor и запустите снова.

Ответить
Arunas

да, спасибо.

Ответить
Shaitan

Дмитрий здравствуйте! Вопрос немного не по теме урока, но все же очень интересно. Нужно ли оборачивать в транзакцию при сохранении? Есть ли в данном действии смысл? На одном из собеседований, меня в этом убеждали. Хотя я не совсем понимаю зачем, разве доктрина даст произвести модификацию в бд, если вылетит какая-либо ошибка? Заранее спасибо!

Ответить
Дмитрий Елисеев

Вручную не нужно, Doctrine внутри $em->flush() сама оборачивает всё в транзакцию.

Ответить
Shaitan

Дмитрий, спасибо за вебинар, вопрос есть еще такого плана. Репозиторий же можно отнаследовать от доктриновского репозитория(EntityRepository), а в анотации к ентити дописать наш репозиторий. Почему вы не использовали такой вариант репозитория?

Ответить
Дмитрий Елисеев

Ответили в четырёх последних минутах видео, что при этом из EntityRepository будет наследоваться куча его методов, которые нам не нужны и которые мы никак не контролируем. У программистов будет соблазн дёрнуть напрямую findOneBy([...]) или createQueryBuilder('t') и понаписать кучу запросов в обход наших методов. Наш же репозиторий от этого защищён.

Ответить
Arunas

привет, не на тему, но случилось такой курьез: сегодня скачал код с репозитория. После make init на ВМ Вагранта (Ubuntu 16.04.6 LTS) все порядки: саит показывает и тесты проходят. Но в линуксовом компютере (Ubuntu 18.04.4 LTS) только тесты проходят, но в браузере localhost:8080 (Chrome): ошибка 403 Forbidden, nginx/1.17.8 - Auction сайт не работает. Как это исправить?

Ответить
Дмитрий Елисеев

А localhost:8081 открывается?

Ответить
Arunas

при localhost:8081 тоже не работает, но пишет: File not found.

Ответить
fedot

Скорее всего что-то с доступом, посмотри логи веб сервера

Ответить
Arunas

помогите решить этот 403 Forbidden при localhost:8080.

Ответить
Дмитрий Елисеев

Чего-то в системе не хватает. Как именно помочь?

Ответить
Arunas

Да, и project-manager на Линуксовом ПК также не запускается, логи nginx:

manager-nginx_1         | 2020/03/04 19:40:07 [crit] 7#7: *1 stat() "/app/public/" failed (13: Permission denied), client: 172.18.0.1, server: , request: "GET / HTTP/1.1", host: "localhost:8080"
manager-nginx_1         | 2020/03/04 19:40:07 [crit] 7#7: *1 stat() "/app/public/" failed (13: Permission denied), client: 172.18.0.1, server: , request: "GET / HTTP/1.1", host: "localhost:8080"
manager-nginx_1         | 2020/03/04 19:40:07 [error] 7#7: *1 FastCGI sent in stderr: "Primary script unknown" while reading response header from upstream, client: 172.18.0.1, server: , request: "GET / HTTP/1.1", upstream: "fastcgi://172.18.0.12:9000", host: "localhost:8080"
Ответить
Arunas

Переинсталировал весь линуксовый компютер, теперь всё ок, спасибо за намёк.

Ответить
Сергей

Дмитрий, у нас есть модуль Auth, где определен entity User. Если мы создадим другой модуль, внутри которого будет связь другой entity с пользователями, то модули не будет изолированы друг от друга. Насколько это правильно?

Ответить
Дмитрий Елисеев

В другом модуле не будет связи с User. Там для связи будет уже своя сущность, но с тем же ID.

Ответить
Shaitan

Дмитрий, вопрос следующий, оправдано ли хранение токенов в отдельной таблице? Или токены лучше хранить в таблице с юзерами? Интересует общепринятая практика. Ну и мне кажется отдельное хранение токенов упрощают сущность юзера? В чем минусы и плюсы обоих подходов? Спасибо!

Ответить
Дмитрий Елисеев

Можно хранить и в отдельной таблице. И даже в отдельном хранилище вроде Redis. Сущность упрощается, но придётся ходить в оба места для каждой операции.

Ответить
Arunas

какую и как завести быструю, легковестную БД на клиетскую часть для хранении параметров (всяких default value, интервалов времени) и для полноценной работы, когда интернет исчезает (при исчезновение связи с материнской базой).

Ответить
Дмитрий Елисеев

Мобильные приложения часто используют SQLite.

Ответить
Arunas

спасибо.

Ответить
Валентин

Доброго времени суток! Дмитрий, могли бы вы объяснить, почему в нашем случае мы сделали прослойку UserNetwork для маппинга, а не использовали "One-To-Many, Unidirectional with Join Table" из документации доктрины, в подходе из документации есть какие-то подводные камни?

Ответить
Валентин

Извиняюсь, если невнимательно посмотрел видел и упустил этот момент)

Ответить
Дмитрий Елисеев

Да, можно так сделать по аналогии Many-To-Many. Но для этого также нужно будет добавить первичный ключ id и потом потребуется делать более сложные JOIN-запросы через эту промежуточную таблицу.

Ответить
Артём

Прекрасные уроки - с каждым уроком сознание расширяется. Спасибо!

Ответить
Ром

Не совсем понимаю, почему в репозиториях в методах has... нельзя ограничить поиск до одного элемента setMaxResults?

Ответить
Дмитрий Елисеев

Если в БД для искомого поля имеется уникальный индекс, то как в случае с поиском по первичному ключу LIMIT можно не указывать.

Ответить
Ром

Спасибо!

Ответить
Евгений

Дмитрий, по какой причине вы отказались от выделения слоя операций с БД? Даже интерфейсы переделали в классы.

Товарищи от DDD обычно советуют в доменный слой не вносить зависимость от сторонних библиотек. А тут жёсткая завязка на Doctrine через аннотации и фактически служебные классы Doctrine (описание типов), лежат рядом с доменными классами. Лично мне больше понравилось разделение на слои как в slim-skeleton, но я бы ещё из контроллеров вынес use case (в принципе как у вас).

Ответить
Дмитрий Елисеев

Про идеи разделения по Clean Architecture отвечал под видео о юзкейсах. Да, можно при желании вынести папку инфраструктуры и все реализации интерфейсов делать там. Но сразу так делать будет избыточно.

От интерфейсов для репозиториев или сервисов будет польза, если в проекте будет несколько их реализаций. Например, если в тестах будем использовать InMemoryUserRepository. Но если везде используем моки, то пользы от отдельного интерфейса нет.

А то, что служебные типы Doctrine лежат в одной папке рядом с сущностью смысл не меняет. Даже если все классы закинуть в одне папку, то всё равно экшен будет логически находиться на уровне HTTP, команда будет на уровне приложения, а EmailType так и остаётся на уровне инфраструктуры.

Главное программировать логически по уровням всё так, чтобы из сущности не дёргался request контроллера и чтобы клиенты не зависели от реализации класса. А в какой папке они будут лежать и будет ли для чего-то выделен отдельный интерфейс - не важно.

Именно про это говорят другие товарищи, что DDD - это не про папки application, domain и infrastructure, а про контексты и смысл.

Ответить
Евгений

Про логическое разделение на уровни при программировании, я полностью согласен. Но есть опасение, что без физического выделения уровней "логика" может быть не понята другими программистами и всё превратится в кашу.

Я сейчас нахожусь на стадии нащупывания "идеальной" архитектуры -- ваш ответ очень помог. Спасибо!

Ответить
Ivan

А почему нельзя токены вынести из юзера в отдельную сущность? Какие минусы у такого решения?

Ответить
Дмитрий Елисеев

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

Ответить
Зарегистрируйтесь или войдите чтобы оставить комментарий