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

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

  • 00:01:19 - Переименование класса Network
  • 00:02:15 - Какие таблицы нам нужны в базе данных
  • 00:03:20 - Пути сущностей
  • 00:04:45 - Способы написания маппинга
  • 00:05:48 - Привязка к таблице
  • 00:10:28 - Маппинг примитивных типов
  • 00:12:06 - Кастомные типы для простых объектов-значений
  • 00:15:16 - Комментарий типа к полю
  • 00:16:49 - Регистрация в ORM
  • 00:18:58 - Конвертация первичного ключа в строку
  • 00:20:16 - Другие простые объекты-значения
  • 00:23:07 - Embedded для сложных объектов-значений токенов
  • 00:28:07 - Как работает Embedded
  • 00:29:46 - Исправление nullable для Embedded
  • 00:31:56 - Переход на Doctrine Collection
  • 00:34:11 - Неудобство OneToMany связей
  • 00:37:27 - Маппинг коллекции значений через сущность-носитель
  • 00:45:54 - Проверка логики
  • 00:47:02 - Промежуточный обзор
  • 00:48:55 - Устройство сохранения и поиска в Doctrine
  • 00:51:37 - Реализация UserRepository
  • 00:52:53 - Язык DQL
  • 00:54:35 - Получение репозитория
  • 00:56:59 - Определения для Psalm и PhpStorm
  • 01:00:33 - Инъекция репозитория
  • 01:01:20 - Остальные методы
  • 01:02:12 - Реализация Flusher
  • 01:02:25 - Почему не используем напрямую EntityManager
  • 01:04:26 - Что делать дальше
  • 01:05:24 - Проверка валидности маппинга
Скрытый контент (код, слайды, ...) для подписчиков. Открыть →
Дмитрий Елисеев
elisdn.ru
Комментарии (107)
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,
            ],
        ];
    }
Ответить
Roman Korolov

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

Ответить
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() сама оборачивает всё в транзакцию.

Ответить
Алексей

А в чем смысл тогда Доктриновской конструкции?

$this->em->getConnection()->beginTransaction(); 
...
$this->em->getConnection()->commit();
Ответить
Дмитрий Елисеев

Вызов $this->em->getConnection() возвращает нам объект $connection из DBAL. И $em->flush() внутри UnitOfWork как раз и вызывает $this->connection->beginTransaction()

Конструкция полезна для ручного запуска транзакции. Например, если где-то пишем нативные запросы вроде $connection->insert(...) без использования $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.

Ответить
Алексей

А здесь поподробнее. Как я понимаю они мапятся на разные таблицы? А зачем нам разные, можно ли мапить на одну и ту же? Будет далее где то подобное?

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

Да, на разные. Для десяти модулей удобнее иметь 10 своих таблиц по 5 полей, чем одну общую мега-таблицу на 50. Про это будет.

Ответить
Алексей

Дмитрий, как всегда очень круто. Сколько уже лет Вас смотрю, но Вы продолжаете мне открывать новые вещи.

Ответить
Александр

Здравствуйте. Я правильно понимаю, пока что нет видео, где рассказано об этом? Очень интересно как это реализовать.

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

Пока нет.

Ответить
Shaitan

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

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

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

Ответить
Arunas

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

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

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

Ответить
Arunas

спасибо.

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

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

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

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

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

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

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

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

Ответить
Roman Korolov

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

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

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

Ответить
Roman Korolov

Спасибо!

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

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

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

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

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

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

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

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

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

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

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

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

Ответить
Simon

Приветствую, Дмитрий. Вы сами приводите в примеры, а что если вынести код в отдельный сервис или отдельный модуль. Взяли и скопировали, как есть без зависимостей. А тут получается мы завязываемся на доктрину. Т.е. если нам понадобится реализация репы на основе апи к какому-нибудь сервису, ака UserRemoteRepository то придется менять код там, где не надо. Тут скорее идет речь про скрещивание гексогоналки и ДДД. И все же про папки тоже идет речь иначе зачем это все разделение http, entity ... Можно все в одну папку сложить, раз речь про контексты. В дополнение к интерфейсу: в случае с ним нет соблазнов добавлять в него всякую ересь не относящуюся к домену. Типа getByJsonb или isSelectedByLanguage. А с реализацией можно нагадить так, что не разберешь, где правда, а где ложь. Также в User добавили checkEmbeds. Т.е. у нас в сущности есть заяц чисто для доктрины

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

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

Да, я говорю про фреймворконезависимый код. Что взяли и вынесли код в отдельный сервис или модуль. Здесь сейчас так и есть, что можно это вынести. А Assert и Doctrine фреймворками не являются.

Т.е. если нам понадобится реализация репы на основе апи к какому-нибудь сервису, ака UserRemoteRepository то придется менять код там, где не надо.

Если понадобится несколько реализаций, то в IDE нажмём Refactor > Extract Interface и напишем к нему класс UserRemoteRepository. Код юзкейсов у нас не поменяется.

И все же про папки тоже идет речь иначе зачем это все разделение http, entity ... Можно все в одну папку сложить, раз речь про контексты.

Для мелких контекстов из трёх файлов можно всё в одну папку. Но для более крупных удобно отдельные папки. А Http поместить в контекст не получится, так как в контроллерах обычно нужны объединённые данные из многих контекстов.

Ответить
Simon

Не соображу как вы подсвечиваете текст)

Да, я говорю про фреймворконезависимый код. Что взяли и вынесли код в отдельный сервис или модуль. Здесь сейчас так и есть, что можно это вынести. А Assert и Doctrine фреймворками не являются.

Assert не так страшен, он такой же как Uuid, а вот Doctrine все-таки является своего рода framework. Тут как бы тонкая грань, код использует Assert, а Doctrine использует код, а раз она использует код, значит она является фреймворком. Аннотации ни к чему не обязывают, да. Вынес код подключил другую ORM и все также продолжает работать, но аннотации останутся, значит они нам не нужны, значит их надо удалить за не надобностью, значит мы тратим время на чистку домена от зависимостей, а значит изначально что-то пошло не так, если нам приходится этим заниматься. Это просто nit(как в ревью)

Если понадобится несколько реализаций, то в IDE нажмём Refactor > Extract Interface и напишем к нему класс UserRemoteRepository. Код юзкейсов у нас не поменяется.

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

Для мелких контекстов из трёх файлов можно всё в одну папку. Но для более крупных удобно отдельные папки. А Http поместить в контекст не получится, так как в контроллерах обычно нужны объединённые данные из многих контекстов.

Я скорее про IdType и ему подобных в одном уровне с VO в доменных папках. Для не опытного - каша, для опытного - "я тебя выслушал".

Ответить
Ivan

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

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

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

Ответить
Huang

I am Chinese and I don’t understand Russian. The content of this email is translated through Google. I hope you can understand it.

Your video tutorial is very good, I hope I can see all, but I don’t know how to pay as a Chinese?

Of course I can’t understand the voice. I hope there are automatically translated subtitles. I wonder if it can be solved?

Looking forward to your reply! thank you very much!

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

Sorry, but I do not have English subtitles still. I will think about it.

Ответить
Huang

Thank you for your reply, I have realized the payment through some research. I'm watching your video, but I don't understand it at all. I think I will miss the very good explanation, which is a pity. But thank you very much for your reply.

Ответить
Юлия Елисеева

Hello! Subtitles are added to later episodes. Now they are added to episodes 45, 46, 47 and 51.

Ответить
slo_nik

Добрый день.

Дмитрий, подскажите пожалуйста, как можно обозначить дату c учётом timezone?

Сейчас у меня в сущности сделано так.

  /**
 * @var DateTimeImmutable
 * @ORM\Column(type="datetimetz_immutable")
 */
private DateTimeImmutable $date;

public function __construct(/**/ DateTimeImmutable $date)
{
    /**/
    $this->date = $date;
    /**/
}

В миграции дата определилась так

date TIMESTAMP(0) WITH TIME ZONE NOT NULL

При создании записи передаю объект DateTimeImmutalbe как обычно

$post = new Post(new DateTimeImmutable())

В таблицу пишется в таком виде.

2020-12-24 14:11:13 +00:00

Как я понимаю, должно быть ещё смещение записано?

Или я что-то не понял из Вашего видео?

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

Смещение здесь это именно +00:00, так как в php.ini контейнера не переопределена date_timezone и сервер работает в UTC.

Свою таймзону при желании можно передать вторым аргументом в конструктор даты:

new DateTimeImmutable($date, new DateTimeZone($zone))

или поменять после создания через иммутабельный сеттер setTimezone.

Ответить
slo_nik

или поменять после создания через иммутабельный сеттер setTimezone.

Не совсем понял.

Вас не затруднит пример показать?

Ответить
slo_nik

Вас не затруднит пример показать?

Вроде дошло)))

Ответить
slo_nik

У меня ещё один вопрос. Есть несколько сгенерированных и применённых миграций. Таблицы созданы, всё работает.

Создаю новую сущность, к ней миграцию.

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

Например:

    public function up(Schema $schema) : void
    {
        // this up() migration is auto-generated, please modify it to your needs
        $this->addSql('CREATE TABLE post_posts (id UUID NOT NULL, date TIMESTAMP(0) WITH TIME ZONE NOT NULL, status VARCHAR(255) NOT NULL)');
        $this->addSql('COMMENT ON COLUMN post_posts.date IS \'(DC2Type:datetimetz_immutable)\'');
        $this->addSql('ALTER TABLE user_users ALTER id TYPE UUID');
        $this->addSql('ALTER TABLE user_users ALTER id DROP DEFAULT');
        $this->addSql('ALTER TABLE user_users ALTER email TYPE VARCHAR(255)');
        $this->addSql('ALTER TABLE user_users ALTER email DROP DEFAULT');
    }

Почему docrine пытается внести какие-то изменения в существующие таблицы?

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

Ответ в видео с 15:40

Ответить
slo_nik

Благодарю, пересмотрю сейчас. А вот с датой всё-таки что-то не получается. Передаю в конструктор в таком виде и дата для всех записей проставляется в одной timezone.

$order = new Order(
    Id::next(),
    new DateTimeImmutable('now', new \DateTimeZone('Asia/Kamchatka'))
);
Ответить
Дмитрий Елисеев

А что именно нужно сделать в БД? В случае передачи 'now' зона бесполезна.

Если нужно сохранить в БД именно название таймзоны, то сохраняйте её отдельном поле как в последнем примере из документации.

Ответить
slo_nik

Нужно записать время создания записи с учётом timezone пользователя. Если запись создаётся админом, то надо выбрать timezone(выпадающий список), для которой будет запись. Для этого сделал поля date и timezone в таблице.

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

Тогда да, сделать поле даты с сохранением в UTC без tz и отдельное поле для таймзоны. И при выводе на сайт конвертировать в эту зону.

Ответить
slo_nik

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

Пытался прямо в twig подобное сделать, но не хватило знаний)))

Но, если пользователь зайдёт на сайт, то timezone будет сервера.

Как в этом случае можно отловить timezone пользователя?

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

Но, если пользователь зайдёт на сайт, то timezone будет сервера.

Логично. Таймзону пользователя можно определить только в браузере. Серверу в HTTP-запросах она не передаётся.

Как в этом случае можно отловить timezone пользователя?

Автоматически на бэкенде никак. Для этого все сайты в настройках профиля дают вручную выбрать часовой пояс.

И эту таймзону из юзера проставить в Twig.

Ответить
slo_nik

Да, с twig разобрался в тот же день)

Автоматически на бэкенде никак

Это я понимаю)))

Ответить
slo_nik

В случае передачи 'now' зона бесполезна.

Я попробовал код отдельно от сайта и doctrine.

$date = '2020-12-24 17:54:15';
$zone = 'Asia/Kamchatka';

$result = new DateTimeImmutable('now', new DateTimeZone($zone));
echo $result->format('d-m-Y H:i:s');
var_dump($result);

//or

$result = new DateTimeImmutable();
$dtzone = $result->setTimezone(new DateTimeZone($zone));
echo $dtzone->format('d-m-Y H:i:s');

В обоих случаях всё работает, показывает Камчатское время.

//25-12-2020 05:21:08

Теперь бы понять, почему такая конструкция не работает при передаче в объект

new DateTimeImmutable('now', new DateTimeZone($zone))
Ответить
Влад

Здравствуйте Дмитрий!

Интересно было бы услышать где то в уроках методики внесения изменений (множественных) в код, связанных с добавлением свойств или методов в сущности.

Например если взять сущность User и добавить к ней свойство Телефон и сделать его обязательным, или Имя - не важно, вопрос больше теоретический. Нужно изменить в первую очередь тесты, потом идти по ошибкам psalm и phpUnit, менять построители, фикстуры, тесты, команды и обработчики и т.д. - вобщем кусок работы приличный.

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

Собственно вопрос - возможно есть какие то методики как это быстро можно делать, или паттерны - которые стоит использовать, если ты понимаешь что в каких то сущностях могут быть изменения. Возможно есть какие то архитектурные решения под такие случаи. Хотелось бы услышать ваши советы.

Спасибо! Влад

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

вобщем кусок работы приличный

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

а при отсутствии времени ясно что на исправление тестов времени может и не остаться

Это у всех любимая отговорка. Но почти все, у кого нет 10 минут на правку тестов или чистку зубов, со временем начинают тратить часы или дни на лечение поломок.

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

есть какие то архитектурные решения под такие случаи

Для избавления от страха крупных изменений как раз есть практики CI/CD, предполагающие инкрементальную разработку, до которых мы дойдём в следующих эпизодах.

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

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

Ответить
Ruslan

Я понял, что Доктрина оборачивает накопленные действия с сущностями в транзакцию. А что если нам нужен блокирующий SELECT? К примеру мы проверяем баланс и если он подходящий, то списываем сумму, но на медленных соединениях пользователь может послать два запроса и пока первая транзакция не закончится, без блокировки, второй получит по SELECT-у , что баланс еще позволяет (получит тоже число, что и первая транзакция) и за пустит вторую транзакцию, в итоге будет двойное списание.

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

Для доставания сущности с блокировкой можно выполнить find у самого EntityManager:

$em->find(Order::class, 1, LockMode::PESSIMISTIC_WRITE);

А если нужно сделать нативный запрос без сущности, то установить setLockMode у Query.

Ответить
Ruslan

Не совсем это хотел услышать. То что вы говорите выглядит как отдельная операция, а мне не понятно другое есть ли возможность в команды добавить запрос и выполнить как транзакцию через flush(). Абстрактно : BEGIN TRANSACTION:

    $em->find(Order::class, 1, LockMode::PESSIMISTIC_WRITE);
    $em->INSERT();
    $em->UPDATE();

COMMIT; (flush()) Чтоб не только команды изменения входили в Доктриновский менеджер.

Нет ли у вас планов сделать несколько уроков по Доктрине и начать с ДБ фёрст, чтоб более было ясно как это работает, и потом тот же пример на Код фёрст, чтоб можно было сравнить оба подхода. И вообще на Ютубе не так много Доктрине, и чаще там разъясняется подход как с любой другой ORM .

За уроки спасибо.

Ответить
slo_nik

Добрый вечер.

Подскажите, как можно сделать slug для Name?

Сейчас у меня в Name одно поле, определён свой тип данных для Name.

Name используется в другой сущности.

class Region
{
     /*
     * @ORM\Column(type="region_name")
     */
     private Name $name;
     /*
     * @ORM\Column(type="string")
     * @Gedmo\Slug(fields={"name"})
     */
     private string $slug;

     /....../
}

При таком подходе, при сохранении региона я получаю ошибку

Cannot use field - [name] for slug storage, type is not valid and must be 'string' or 'text' in class - 
App\Model\Location\Entity\Region\Region

Пробовал делать Name как Embedded, переносил свойство slug именно в Name. При таком подходе slug создаётся,

Но в @Route я не могу прописать использования slug, получаю ошибку

/*
 * @Route("/region/{slug}", name=".show")
 */

Unable to guess how to get a Doctrine instance from the request information for parameter "region".

Как правильно решить данную задачу?

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

Лучше заполнять $slug самому напрямую через behat/transliterator без Gedmo. Тогда никаких проблем с объектами-значениями не будет.

Ответить
slo_nik

Благодарю за совет. Пока решил проблему с gedmo. Ошибка была в передаваемых параметрах.

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

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

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

Да, стоит плагин "PHP Annotations". Всё открывается по Ctrl+Click.

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

Спасибо. Ctrl+Click я и пытался открыть )))). Поставлю плагин.

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

Все сделал по уроку, но в итоге полчил несуществующие типы полей The field 'App\Auth\Entity\User\User#email' uses a non-existent type 'auth_user_email' То же самое и с ролью и статусом. Только Id в норме. Не пойму в чем прокол? Буду разбираться )))))

Нашел сразу как написал пост. В common/doctrine.php типы полей не зарегистрировал. Семен Семеныч !!! )))))

Ответить
Олег

Дмитрий приветствую. Вопрос такой. Я так понимаю вы используете ddd и cqs. Тогда почему класс User жёстко завязан на доктрину ? Разве не должно быть так, что я могу перенести директорию в другой проект, сделать реализацию необходимых интерфейсов под себя, и всё работает? С точки зрения подхода чистой архитектуры ваш подход, кажется, выглядит неудовлетворительно. Объясните пожалуйста свою позицию, возможно я не прав

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

Тогда почему класс User жёстко завязан на доктрину?

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

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

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

Ответить
Олег

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

Ответить
Олег

Да, вижу ваш комментарий на эту тему. Спасибо

Ответить
Руслан

Дима, добрый день,

очень интересует на Вашем опыте в каких случаях показано применять enum поля БД (PostGres), а в каких это неприемлемо, извините за возможно глупый вопрос.

Пару лет назад столкнувшись с проблемами миграций enum полей (Eloquent/Laravel 5.x) а также их общих ограничений касательно добавления/изменения/удаления, сложилось предубеждение, что это абсолютное зло. Хотелось бы узнать актуальные рекомендации, может эту тему где-то ранее уже разбирали, крайне признателен заранее.

Спасибо.

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

Это удобно когда с БД работают напрямую, если нужно выжать максимум экономии или контролировать целостность на уровне БД, чтобы никто не смог записать туда отсутствующий в ENUM статус. Тогда полезно использовать ENUM-ы, триггеры, кастомные CONSTRAINTs, представления, хранимые процедуры и специфичные типы нужного размера.

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

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

А вообще в PostgreSQL ENUM-ы объявляются типами отдельно от таблицы. Поэтому проблемы производительности с добавлением значений там нет.

Ответить
Руслан

Отлично, то что нужно, спасибо за разъяснения и быстрый ответ

Ответить
Руслан

Дмитрий, здравствуйте,

подскажите, пожалуйста, как можно оптимизировать сохранение в SPA через REST API в Postgres больших JSON объектов? Данный объект - это стейт, который создается/используется сторонним сервисом, его можно не парсить совсем, главное хранить/считывать как есть в оригинале. Может сокеты с передачей бинарных данных или что еще.

Со считыванием особых проблем нет, а вот на запись сильно провисает пропускная способность API.

PS: если были на эту тему статьи/курсы/подкасты ранее, буду очень признателен))

Спасибо!

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

С передачей больших JSON-ами не сталкивался, так что не подскажу.

Ответить
Simon

Добрый вечер, UserNetwork не является сущностью, зачем ему id? Мы опять делаем что-то из-за доктрины. Network не существует без пользователя, соответственно достать Network можно только через пользователя. Т.е. не было БД делали правильно, были поля name, identity и был code first подход. Теперь появилась БД и пришли к тому же db first, потому что теперь мы вынуждены создать никому не нужную сущность UserNetwork ради doctrine. Мне кажется надо было выбрать другую ORM или туже доктрину, но без аннотаций, чтобы были все-таки чистые классы. Ато получается двойные стандарты

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

Мы опять делаем что-то из-за доктрины.

Да, мы немного подпортили сущность ради Doctrine, добавив нужный ей для OneToMany класс-связку UserNetwork с id и user. И добавили checkEmbedds для исправления её работы с пустыми значениями.

Т.е. не было БД делали правильно, были поля name, identity

В Network так и остались только эти поля. И в основном коде и в тестах мы так и работаем с этим классом. Мы просто добавили контейнер UserNetwork для привязки Network к User.

Это не стало DB First, где мы сначала придумываем все таблицы БД, а потом уже по ним пишем классы.

Мне кажется надо было выбрать другую ORM

Другие такие же или ещё неудобнее. Идеальной чистоты можно добиться только написав всё вручную.

Или ту же доктрину, но без аннотаций, чтобы были все-таки чистые классы

Даже если вынесем маппинг в XML или Yaml, то всё равно в код придётся добавить класс-связку UserNetwork. Так что это не полностью поможет.

Ответить
Simon

а для чего нам UserNetwork, нам разве нужен inverseOf в Network на User?

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

Да, OneToMany производится в mappedBy навстречу связи ManyToOne на User.

Ответить
slo_nik

Добрый вечер.

Возникла небольшая проблема.

Есть два приложения, которые используют одну базу данных.

Первое приложение для api, куда будет обращаться frontend, второе это backend.

Оба на symfony 6, в api используется skeleton, в backend полноценное web приложение.

В backend естественно используется больше сущностей, в настройках doctrine.yml указаны куча типов полей.

В api сущностей меньше, только те, которые понадобятся пользователю при обращении с frontend.

Например так:

backend/config/doctrine.yml
     types:
         type_1: 'path/to/class'
         type_2: 'path/to/class'
         type_3: 'path/to/class'
         type_4: 'path/to/class'

 api/config/doctrine.yml
     types:
        type_1: 'path/to/class'
        type_3: 'path/to/class'

При выполнении для api-приложения команды doctrine:schema:validate выводится ошибка:

Unknown column type "type_2" requested. Any Doctrine type that you use has to be registered with

Чтобы её убрать надо скопировать класс в api и прописать соответствующий тип в настройках doctrine, но это же полный бред.

Как можно обойти эту проблему?

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

В простейшем случае миграции и валидацию схемы можно оставить только в backend.

Ответить
slo_nik

А насколько это правильно?

Может создание ещё одной scheme помочь решить проблему?

Или это будет лишнее, создание ещё одной scheme для api?

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

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

Либо есть вариант слить сайт и API в одно приложение с двумя ядрами, использующее одни общие сущности, но разные контроллеры. Но это тоже сложно.

Ответить
slo_nik

Набор миграций как раз один на два приложения. С api идёт только обращение к базе данных, но там свои entity, fetcher-ы, read model-и и т.п. К api обращаются только пользователи, вот и хотел для них вынести всё в отдельные файлы. Второе это backend, вот тут есть миграции.

Получается, что проще сделать frontend для пользователей и frontend для админки, а в api уже работать с базой для всех и решать всё правами доступа?

Да и symfony прислушивается в Вашим советам)))

Creating applications with multiple kernels is no longer recommended by Symfony. Consider creating multiple small applications instead.

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

Получается, что проще сделать frontend для пользователей и frontend для админки, а в api уже работать с базой для всех и решать всё правами доступа?

Именно. В случае работы с JS-фреймворками лучше весь серверный код вынести в один проект с API и сделать к нему два отдельных JS-фронтенда.

А раз у вас там сейчас в админке классическое PHP+HTML приложение, то получится либо слить с одним ядром и сложной общей конфигурацией по префиксу пути /api и для оcтальных путей, либо с несколькими ядрами с отдельными настройками, как там пишут:

An application that defines an API could define two kernels for performance reasons. The first kernel would serve the regular application and the second one would only respond to the API requests, loading less bundles and enabling less features;

Либо ничего не сливать и оставить всё как есть.

Ответить
slo_nik

Либо ничего не сливать и оставить всё как есть.

В api оставить только обращение к базе данных и ответ для frontend, fetcher-ы и usecase для работы с базой, без валидаций схемы базы? Ещё entity, к которым с frontend-a будет обращаться пользователь.

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

Да, оставить весь нужный код, но валидацию не запускать.

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

Или войти через:

Google
GitHub
Yandex
MailRu