О паттернах проектирования для работы с РСУБД

Введение


Работа с РСУБД является одной из важнейших частей разработки веб-приложений. Дискусcии о том, как правильно представить данные из БД в приложении ведутся давно. Существует два основных паттерна для работы с БД: ActiveRecord и DataMapper. ActiveRecord считается многими программистами антипаттерном. Утверждается, что объекты ActiveRecord нарушают принцип единственной обязанности (SRP). DataMapper считается единственно верным подходом к обеспечению персистентности в ООП. Первая часть статьи посвящена тому, что DataMapper далеко не идеален как концептуально, так и на практике. Вторая часть статьи показывает, как можно улучшить свой код используя существующие реализации ActiveRecord и несколько простых правил. Представленный материал относится главным образом к РСУБД, поддерживающим транзакции.


О неполноценности DataMapper или несоответствие реального заявленному


Апологеты DataMapper отмечают, что этот паттерн предоставляет возможность абстрагироваться от БД и программировать в "терминах бизнес-объектов". Предписывается создавать абстрактные хранилища, в которых сохраняются т.н. "сущности" — обьекты, содержащие персистентные данные приложения. Фактически предлагается эмулировать БД в приложении, создав над РСУБД объектное хранилище. Якобы это должно позволить достичь отделения бизнес логики от БД. Однако в любом серьезном приложении требуются операции на множестве записей. Их реализация в виде работы с объектами как правило гораздо менее эффективна, чем SQL-запросы. В качестве решения этой проблемы предлагается часть кода делать в терминах объектов, а где это не удается, использовать SQL или какой-нибудь собственный язык запросов, транслируемый в SQL (HQL, DQL). Идея не работает в полной мере, поэтому и предлагается фактически возвращаться к SQL.


Сущности, несмотря на отсутствие внутри SQL-кода, все равно зависят от БД. Особенно это проявляется при программировании связей (одна сущность объявляется главной, другая подчиненной). Реляционные отношения так или иначе протекают в объектную структуру. На самом деле сущности это никакие не "бизнес-объекты", а "пассивные записи". Более того, это вообще не объекты, а структуры данных, которые должны обрабатываться специальным объектом-преобразователем для сохранения и извлечения из БД. Особенно хорошо это заметно в CRUD-приложениях. Сущности в таких приложениях вырождаются в контейнеры для данных без какой-либо функциональности и известны как анемичные сущности. В качестве решения предлагается помещать в сущности бизнес-логику. Это утверждение также вызывает сомнения. Во-первых, сущности в CRUD-приложениях так и останутся анемичными. Негде взять бизнес-логику, чтобы заполнить пустые классы. DataMapper не работает в CRUD-приложениях. Во-вторых, для бизнес-логики почти всегда нужны зависимости. Редко какая настоящая бизнес-логика будет работать только на данных самой сущности. Правильный способ получения зависимостей — внедрение через конструктор. Однако, большинство реализаций DataMapper ограничивают конструирование, делая недоступным внедрение конструктора. Использования внедрения метода в качестве замены внедрению конструктора делает объект неполноценным. Такой объект ничего не может делать сам, ему всегда нужно передавать все необходимые зависимости. Как следствие, происходит загрязнение клиентского кода повсеместной передачей зависимостей.


Самая известная реализация DataMapper в PHP — Doctrine ORM. Чтобы использовать эту библиотеку нужны либо аннотации, либо дополнительные файлы, задающие отображение. Первый способ хорошо показывает связь сущности и БД, пусть и неявную. Само преобразование основано на использовании интерфейса отражения (Reflection API). Данные помещаются и извлекаются без какого-либо участия самого объекта — работа с объектом ведется как со структурой данных. Очевидно, что это нарушает инкапсуляцию — один из базовых принципов ООП. API Doctrine ORM довольно объемный, подводных камней достаточно много. На обучение эффективному использованию этой библиотеки требуется время. Все вышесказанное в разной степени относится и к другим реализациям DataMapper. Учитывая приведенные аргументы, DataMapper представляется избыточным, тем более, что от знания SQL и РСУБД он все равно не избавляет, никакой реальной независимости от БД не дает. Код, использующий Doctrine ORM как правило навсегда останется привязанным к ней.


Использование ActiveRecord


Практически каждый из популярных PHP-фреймворков предлагает собственный способ работы с БД. Большинство используют собственную реализацию ActiveRecord. Как правило, ради скорости разработки типовых приложений на ActiveRecord возлагаются не только обязанности по взааимодействию с БД, но и роль бизнес-объекта, валидатора, а иногда и формы. Проблемы такого использования ActiveRecord известны и хорошо описаны во многих статьях. В качестве решения как правило предлагается переписать весь код используя DataMapper. В данной статье предлагается использовать ActiveRecord с которого снимается часть обязанностей путем соблюдения нескольких простых правил.


Решение описывается далее с примерами псевдокода. Некоторые вызовы могут быть составлены некорректно, цель статьи — показать идею, а не конкретную рабочую реализацию. Конструкторы и некоторые очевидные методы, а также часть проверок опущены для краткости. В качестве реализации AR используется Yii. Данный фреймворк выбран для примеров потому, что на нем написано немало проектов, которые надо рефакторить, поддерживать, с ними нужно считаться.


Подход применим и для других фреймворков и независимых реализаций ActiveRecord. Сначала будет показан код, применимый в проектах, полностью зависимых от Yii. Он довольно прост. Далее будут показаны примеры с внедрением зависимостей и использованием Yii как библиотеки для реализации интерфейсов объектов взаимодействующих с БД.


Вставка и модификация данных


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


class YiiARUserRepository
{
    public function add(string $email, string $name, array $phones, DateTimeInterface $created_at)
    {
        return $this->transaction->call(function () use($email, $name, $phones, $created_at) {

            //в БД есть уникальный индекс на email, проверка для UI проверка произвводится в форме с помощью обращения к внедренному репозиторию
            $ar = new YiiARUser([
                'email'      => $email,
                'name'       => $name,
                'created_at' => $created_at->format('Y-m-d H:i:s')
            ]);
            $ar->insert();
            foreach ($phones as $phone) {
                $ar->addPhone($phone['phone'], $phone['description']);
            }

            return $ar;
        });

    }

}

class YiiDbTransaction
{

    public function call(callable $callable)
    {
        $txn = \Yii::$app->db->beginTransaction();
        try {

            $result = $callable();

            $txn->commit();

            return $result;

        } catch (\Exception $e) {
            $txn->rollback();
            throw $e;
        }
    }

}

class YiiARUser extends yii\db\ActiveRecord
{
    //...
    public function addPhone(string $phone, string $description)
    {
        $ar = new YiiARPhone([
            'user_id'     => $this->id,
            'phone'       => $phone,
            'description' => $description
        ]);
        $ar->insert();

        return $ar;
    }

}

Желательно, чтобы в методах добавления не было никакого кода, кроме присваивания и вставки в БД. Все необходимое нужно вычислить в клиентском коде. Никаких beforeSave() или других вызовов жизненного цикла быть не должно. Сразу можно вставлять в базу только обязательные поля, остальное можно добавить в других методах в рамках транзакции. В качестве бонуса — нет никаких проблем с использованием автоинкрементных ключей. В статьях и докладах по Symfony, Doctrine и DDD все чаще можно встретить тирады про недостатки автоникрементных ключей БД, про то, что сущность при создании без ключа — не сущность и надо использовать генераторы UUID. Это еще один шаг к эмуляции функционала БД в приложении — то, чего в данной статье предлагается избегать.


class YiiARUser extends yii\db\ActiveRecord
{
    //...
    public function changePassword(string $password)
    {
        $this->updateAttributes([
            'password' => md5($password)
        ]);
    }

    public function rename(string $name)
    {
        $this->updateAttributes([
            'name' => $name
        ]);
    }

}

class RegisterForm
{
    public function register(DateTimeInterface $created_at): YiiARUser
    {

        if ( ! $this->validate()) {
            throw new \DomainException($this->errorMessage());
        }

        return $this->transaction->call(function () use($created_at) {
            $user = $this->user_repo->add($this->email, $this->name, [], $created_at);
            $user->changePassword($this->password);
            $user->changeSomething($some_data);
            foreach ($this->phone_forms as $form) {
                $user->addPhone($form->phone, $form->description);
            }

            return $user;
        });

    }
}

Основная идея заключается в том, что мы не накапливаем изменения в единице работы на стороне приложения, а производим их с помощью единицы работы на стороне БД. Транзакция БД это единица работы. Многие известные проблемы AR как раз вызваны тем, что разработчики пытаются предварительно собрать граф из AR в памяти и при вызове save() сохранить все их сразу. Для этого в Yii даже существует расширение WithRelatedBehavior. Все это заблуждение. Слово "Active" в ActiveRecord как раз и предназначено для того, чтобы показать, что объекты могут выполнять запросы при обращении к их методам. Какое-либо разделение на "до сохранения" и "после сохранения" недопустимо.


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


Выполнение нескольких запросов вместо одного при вставке или обновлении данных в БД не должны вызвать проблем с производительностью. СУБД хорошо оптимизирует последовательные INSERT и UPDATE на одну и ту же строку в одной транзакции. Однако, последнего все равно лучше не допускать, и если производится массовое обновление данных строки, лучше создавать соответствующие методы, типа YiiARUser::changeInfo($phones, $addresses, $name, $email).


Выборка


Код запросов на выборку необходимо помещать в класс репозитория. Использовать статические методы для этой цели не стоит. Статические методы Yii в примере используются в качестве реализации просто потому что по-другому в Yii трудно или невозможно. В идеале нужно внедрять соединение с БД в репозиторий через конструктор, а затем и во все создаваемые объекты AR. От статики лучше держаться подальше — она вредит проектам с большим объемом кода или длительным жизненным циклом.


class YiiARUserRepository
{
    //...
    public function findOne($id)
    {
        return YiiARUser::findOne($id);
    }

    public function findUsersWithGroups($limit)
    {
        return YiiARUser::find()->with('groups')->all();
    }

    //можно вернуть и DataProvider, если клиентский код тоже зависит от Yii
    public function findAll(): DataProviderIterface
    {
        //...
    }

    //если нужно много пользователей
    public function findAll(): \Iterator
    {
        //...
        return new class($reader) implements \Iterator
        {
            //...
            public function current()
            {
                $data = $this->reader->current();

                return YiiARUser::instantiate($data);
            }
        }
    }

}

Как видно из примеров, можно сохранить DataProvider и data widgets (RAD-инструменты Yii). Это возможно только в том случае, если проект можно сделать полностью зависимым от Yii.


Модификация данных в связанной таблице


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


//поведение осуществляет загрузку данных в основную и связанные записи
$user->with_related_behavior->setAttributes($request->post());

//путем array_diff(), AR::isNewRecord() определяются новые и измененные записи, генерируются соответсвующие запросы
//очевидно, что между методами существует temporal coupling
$user->with_related_behavior->save();

Разбор всех недостатков представленного подхода выходит за пределы статьи. В рамках данной темы стоит отметить лишь то, что при вызове setAttributes() фактически теряется информация о том, какие записи были добавлены или обновлены, а при вызове save() эта информация восстанавливается. К тому же, данные вызовы тесно связаны. В качестве альтернативы предлагается следующее. Обязанности по определению того, что было добавлено, обновлено или удалено необходимо возложить на форму. Лучше всего строить UI, которые генерирует HTTP запросы на уделение связанных записей по конретным идентификаторам.


class UserUpdateForm
{

    public function update(YiiARUser $user)
    {

        $this->transaction->call(function () use ($user) {

            //...
            foreach ($this->changedPhones() as $phone)
                $user->changePhone($phone['id'], $phone['phone'], $phone['description'])

            $user->addPhones($this->addedPhones());

        });

    }

}

class YiiARUser extends yii\db\ActiveRecord
{

    //...

    public function changePhone(int $phone_id, $phone, $description)
    {
        $phone = YiiARPhone::findOne(['id' => $phone_id, 'user_id' => $this->id]);
        if ($phone == null) {
            throw new \DomainException('Телефон не найден.');
        }
        $phone->updateAttributes([
            'phone'       => $phone,
            'description' => $description
        ]);
    }

    public function addPhones($phones)
    {
        YiiARUser::$db->createCommand()->barchInsert('phones', ['phone', 'description'], $phones)->execute();
    }

}

При изменении связанных записей необходимо либо сбросить, либо перезагрузить ранее загруженные связанные записи. Жаль, что в Yii нет публичного метода типа resetRelation($name). Остается либо перезагрузить связанные даннные, либо убедиться в том, что загруженных данных нет или они нигде не используются (плохой код), ну или ответвиться.


Подход, основанный на непосредственных вызовах к РСУБД имеет еще одно преимущество в плане работы со связанными данными — простота обеспечения констант. Например, имеется ограничение — пользователь не должен иметь более пяти номеров телефонов. Простая проверка при добавлении не обеспечивает соблюдения этой константы в условиях одновременного использования ресурса несколькими клиентами. Необходимы блокировки. Работая с БД напрямую можно легко использовать механизмы блокировок движка БД.


public function addPhones(array $phones)
{

    $this->transaction->call(function () {

        $id = YiiARUser::$db->query('SELECT id FROM users FOR UPDATE;')->queryScalar();

        if ($id === null) {
            throw new \DomainException('Пользователь не найден.');
        }

        if ($this->phoneCount() + count($phones) > 5) {
            throw new \DomainException('Телефонов слишком много!');
        }

        YiiARUser::$db->createCommand()->batchInsert('phones', ['phone', 'description'], $phones);

    });

}

При использовании Doctrine ORM реализовать подобное будет сложнее, если не блокировать всю запись сразу при выборке.


Удаление


Удаление осуществляется через объект, представляющий строку.


$user->delete();

class YiiARUser extends yii\db\ActiveRecord
{

    public function delete()
    {
        self::$db->createCommand()->delete('phones', ['user_id' => $this->id]);
        $this->delete();
        //если нужно событие на удаление, здесь можно добавить обращение к шине событий (о внедрении далее)
    }

}

При использовании DataMapper часто приходится делать такие связанные вызовы:


//чтобы сущность отправила событие в шину:
$user->delete();

//удаление записи:
$em->delete($user);

Это один из примеров, показывающий противоречивость DataMapper — даже удалить одним вызовом не получается. Сущность несамостоятельна — работа с ней тесно связана с использованием преобразователя (менеджера сущностей).


Внедрение зависимостей и обеспечение независимости клиентского кода от кода, взаимодействующего с БД


Как известно, приложения гораздо легче обновлять/переписывать по частям, чем полностью. Старое монолитное приложение лучше сначала разделить на слои. Данный подход позволяет как это сделать даже в монолитном коде Yii-приложений.
Бизнес-логика, которая не нуждается в непосредственном выполнении SQL-запросов, может быть реализована без явной зависиости от классов, взаимодействующих с БД. Этого можно достичь благодаря тому, что репозитории являются фабриками AR. Код, приведенный выше, можно модифицировать, добавив интерфейсы и реализации. Это позволяет отделить код, взаимодействующий с БД от остального, а затем, если возникнет такая необходимость, переписать его используя другую библиотеку для работы с БД. Таким образом, legacy-проект можно обновлять частями без полного переписывания.


interface UserRepository
{

    public function add(string $name, string $email, array $phones, \DateTimeInterface $created_at): User;

    public function findOne($id);

}

interface User
{

    public function addPhones($phones);

    public function rename($name);

    public function changePassword($pwd);

}

class YiiDbUserRepository
{

    public function add(string $name, string $email, array $phones, \DateTimeInterface $created_at): User
    {
        $ar = $this->transaction->call(function () use($name, $email, $phones, $created_at) {

            $ar = new YiiARUser([
                'name'       => $name,
                'email'      => $email,
                'created_at' => $created_at->format('Y-m-d H:i:s')
            ]);
            $ar->addPhones($phones);

            return $ar;

        });

        return new YiiDbUser($ar);

    }

    public function findOne($id)
    {
        $ar = YiiARUser::findOne($id);
        if ($ar === null) {
            return null;
        }

        return new YiiDbUser($ar);

    }

}

class YiiDbUser implements User
{

    private $ar;

    public function addPhones(array $phones)
    {
        //multiple insert command
    }

    public function rename(string $name)
    {
        //запрос только при необходимости
        if ($this->ar->name !== $name) {
            $this->ar->updateAttributes(['name' => $name]);
        }
    }

    public function changePassword(string $pwd)
    {
        $this->ar->updateAttributes(['password' => md5($pwd)]);
    }

    public function phones(): \Iterator
    {
        //в YiiARUser объявлена Yii-реляция на YiiARPhone
        $phone_ars = $this->ar->phones;

        $iterator = new ArrayIterator($phone_ars);

        return new class($iterator, $this->dependency) implements \Iterator
        {

            //...
            public function current()
            {
                $ar = $this->iterator->current();

                //объект YiiDbPhone инкапсулируeт объект YiiARPhone
                return new YiiDbPhone($ar, $this->dependency);
            }

        }

    }

}

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


class YiiARUser extends \yii\db\ActiveRecord implements User
{
  //...
}

Внедряя репозитории в клиентский код с помощью интерфейсов, а также не используя в возвращаемых значениях методов зависимые от БД форматы, можно разорвать явную зависимость между классами, работающими с БД и клиентским кодом. Это даст возможность менять реализации в будущем и повысит возможность повторного использования. Также становится возможным разного рода декорирование. Если в клиентском коде понадобится транзакция — ее также можно внедрить воспользовавшись интерфейсом. Данный метод дает возможность внедрять зависимости в репозитории и объекты, работающие со строками. Без инкапсуляции объекта-записи или использования синглтона в Yii этого пока что сделать нельзя. Очевидно, при использовании композиции объекта AR теряется возможность использовать DataProvider, RAD-возможности снижаются. Однако, использование интерфейсов и композиции снизит вероятность ляпов новичков или злоупотреблений, связанных с открытостью интерфейса Yii AR. Стоит отметить, что в последнем утверждении гибкость и открытость рассматриваются как преимущество, позволяющее использовать реализацию AR Yii в своих целях. Если необходимо что-то ограничить — можно использовать композицию.


Работа с межмодульными связями


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


Сначала в модуле блогов необходимо объявить интерфейсы для статей, авторов и их репозиториев. Для своей работы модуль будет требовать реализации интерфейсов Author и AuthorRepository.
Весь остальной код содержится в самом модуле.


interface Post
{

    public function id(): int;

    public function title(): string;

    public function author(): Author;

    public function authorId(): int;

}

interface PostRepository
{
    public function findAllWithAuthors(int $limit): array;
}

class YiiARPost extends \yii\db\ActiveRecord
{
  //...  
}

class YiiDbPostRepository implements PostRepository
{

    private $author_repository;

    public function findAllWithAuthors(int $limit): \Iterator
    {
        $ars = YiiARPost::findAll(['limit' => $limit]);

        $iterator = new \ArrayIterator($ars);

        $ids = [];

        foreach ($ars as $ar) {

            $ids[] = $ar->id;

        }

        $authors = $this->author_repository->findAll($ids);

        return new class($iterator, $this->author_repository, $authors) implements \iterator
        {

            private $iterator;

            private $author_repository;

            private $authors;

            //...
            public function current()
            {
                $ar = $this->iterator->current();

                return new AuthoredPost(
                    new YiiDbPost($ar, $this->author_repository),
                    $this->authors
                );
            }

        }

  }

}

class YiiDbPost implements Post
{

    private $ar;

    private $author_repository;

    public function title(): string
    {
        return $this->ar->title();
    }

    public function content(): string
    {
        return $this->ar->content();
    }

    public function author(): Author
    {
        return $this->author_repository->findOne($this->ar->author_id);
    }

    public function authorId(): int
    {
        return $this->ar->id;
    }

}

class AuthoredPost implements Post
{

    private $post;

    private $authors;

    public function title(): string
    {
        return $this->post->title();
    }

    public function content(): string
    {
        return $this->post->content();
    }

    public function author(): Author
    {

        foreach ($this->authors as $author) {
            if ($author->id() == $this->post->authorId()) {
                return $author;
            }
        }
        throw new DomainException('Статья без автора! Нарушена целостность БД!');

    }

}

interface Author
{

    public function id(): int;

    public function name(): string;

}

interface AuthorRepository
{

    public function fundOne(int $id);

    public function findAll(array $ids): array;

}

Класс AuthoredPost необходим для оптимизации — предзагрузки авторов для списка статей. Реализации интерфейсов находятся в корне компоновки — самом приложении. Только приложению известно какие модули у него есть и как они работают вместе. Модулям друг о друге ничего не известно.


class UserAuthor implements Author
{

    private $user;

    public function id(): int
    {
        return $this->user->id();
    }

    public function name(): string
    {
        return $this->user->name();
    }

}

class UserAuthorRepository implements AuthorRepository
{

    private $repository;

    public function findOne(int $id)
    {

        $user = $this->repository->findOne($id);

        if ($user === null) {
            return null;
        }

        return new UserAuthor($user);

    }

    public function findAll(array $ids): \Iterator
    {
        $users = $this->repository->findAll($ids);

        return new class($users) implements \Iterator
        {

            //..
            public
            function current()
            {
                $user = $this->iterator->current();

                return new UserAuthor($user);
            }

        };
    }

}

На конференциях по PHP слышал вопросы — как в Yii организовать связь между моделями в разных модулях. Приведенный пример кода является вариантом ответа. В представленном случае статьи и авторы могут храниться в разных БД. Безусловно абстракция имеет свою цену. Если у вас маленький одноразовый проект — лучше использовать первый вариант без интерфейсов и разбиения на модули. В этом случае у вас не возникнет необходимости в межмодульных связях и проще будет работать с реляциями.


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


Бизнес-логика


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


class SomeLogicUser
{

    private $user;

    //...

    public function doSomething()
    {

        $name = $this->calculateName();

        //этого лучше избегать
        $this->transaction->call(function () {
            $user->rename($name);
            $user->changeSomething($data);
        });

        //проектировать методы по требованиям бизнес-логики, не придется внедрять транзакцию - она будет внутри
        $user->changeEverythingRequiredUsingOneMethod($name, $data);

    }

}

Отличия между предложенным подходом и популярными реализациями DataMapper


Главное отличие заключается в том, где находится единица работы (Unit of Work). В DataMapper объекты, представляющие данные из БД играют роль структур данных, обрабатываемых объектом-преобразователем. Изменения данных отслеживаются с помощью прокси-объектов (которые, к тому же делают невозможным использование final). В предложенном подходе запросы составляются и выполняются сразу. Нет никакого отслеживания изменений на стороне приложения, прокси, использования Reflection API. Все устроено проще. Можно делать классы final, можно внедрять зависимости через конструктор.


Одним из недостатков является невозможность тестирования без БД объектов, генерирующих SQL-запросы. Тем не менее код, который призван взаимодействовать с БД лучше тестировать вместе с БД. Это позволит избежать получения ложноположительных результатов.


На использование данного подхода натолкнуло определение транзакции в глоссарии MySQL, а также усмотренное сходство между задачами, которые решают программный Unit of Work Doctrine ORM и нативные транзакции БД. Это одно и то же — накопление и управляемое применение/откат изменений.


Заключение


Производительность разработчика тесно связана с простотой и мощью используемых им инструментов. РСУБД и их интерфейсы/драйверы/библиотеки, язык SQL, сами по себе являются фреймворками, позволяющими решать широкий спектр задач. Конкретный движок РСУБД является важнейшей частью проекта. Он должен тщательно подбираться на стадии проектирования. Библиотеки, использующие DataMapper и программный Unit of Work фактически дублируют имеющиеся в БД функции, являются громоздкими и требуют тщательного изучения не только теории РБД, SQL и движка РСУБД, но и собственного API, и, что очень нередко, особенностей своей внутрнней реализации. При этом пользы от них немного. Обеспечение переносимости между БД это очень дорогая задача, которая редко когда требуется и решается до конца. Рассмотренный подход предлагает отказаться от избыточного функционала, изучения оверинжиниринговых технологий и рекомендует использовать всем знакомые простые инструменты и библиотеки.

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

Подробнее
Реклама
Комментарии 168
  • +12
    DataMapper считается единственно верным подходом к обеспечению персистентности в ООП.

    "хорошо", "плохо", "верным" и т.д. не очень инженерные понятия. Скажем в случае со сложной бизнес логикой вам вообще "верным" будет CQRS + ES и подобные подходы.


    Однако в любом серьезном приложении требуются операции на множестве записей.

    Так, давайте не путать. "на множестве записей", то есть сотни и тысячи записей, это как правило агрегации, репорты, то есть по большей части — операции чтения. ORM же (и data mapper как один из вариантов реализации) выгодны только на запись в контексте OLTP.


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


    На самом деле сущности это никакие не "бизнес-объекты", а "пассивные записи".

    Зависит от реализации. У меня к примеру — это самые настоящие бизнес объекты, содержащие бизнес логику и все такое. Да. у существующих решений (если брать конкретно Doctrine) есть немало минусов и ограничений. К тому же подобные инструменты обладают огромной сокрытой сложностью. Любой может реализовать active record уровня yii за пару вечеров, а что-то уровня doctrine реализовать будет уже намного сложнее. Это сильн уменьшает количество людей которые в состоянии поддерживать и развивать такие инструменты.


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


    Особенно хорошо это заметно в CRUD-приложениях.

    Если мыслить CRUD-ом то выйдет CRUD. Это же логично. Как-то в одном таком обсуждении я накидал пример развития "сущности в контексте CURD операций" что бы это не выраждалось в анемичную модель. Посмотреть можно тут, развитие модели можно проследить по ревизиям.


    В качестве решения предлагается помещать в сущности бизнес-логику.

    Ммм… скорее не потому что это какое-то решение, а потому что мы понапридумывали целую кучу принципов, вроде Creator и Information Expert из GRASP, information hiding и т.д.


    Редко какая настоящая бизнес-логика будет работать только на данных самой сущности.

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

    Прошу ознакомиться с такой интересной штукой как double dispatch:


    /**
     * Пример метода в сущности с бизнес логикой:
     *  - позволяет менять пароль только если старый верен
     *  - не разрешает использовать пароль если оный использовался последние 5 раз
     */
    public function changePassword(string $oldPassword, string $newPassword, PasswordEncoder $encoder)
    {
        if (!$encoder->isValid($this->password, $oldPassword)) {
            throw new incorrectPassword();
        }
    
        $lastFivePasswords = array_slice(array_merge($this->previouslyUsedPassword, [$this->password], -5);
        foreach ($lastFivePasswords as $usedPassword) {
             if ($encoder->isValid($usedPassword, $newPassword) {
                  throw new PasswordAlreadyUsedException();
             }
        }
    
        $this->previouslyUsedPassword[] = $this->password;
        $this->password = $encoder->encode($password);
    
        // для того что бы например слать нотификации и тд.
        // можно применить концепт доменных ивентов
        EventStore::remember(PasswordChanged::occurred($this->id));
    }

    большинство реализаций DataMapper ограничивают конструирование

    Есть такая штука, которой руководтсвуются все эти любители доктрин — persistance ignorance. То есть с точки зрения сущности они существуют всегда как если бы они просто в памяти лежали. Это значит что если вы вызвали конструктор сущности — он больше не должен вызываться. Никогда.


    тем более, что от знания SQL и РСУБД он все равно не избавляет, никакой реальной независимости от БД не дает

    независимость от БД это оооочень большое ограничение на которое могут пойти только если реально нужна переносимость. В большинстве же случаев достаточно изоляции, что бы при добавлении например еще одной базы данных (что бы оптимизировать какие-то выборки к примеру) не нужно было вообще трогать бизнес логику. Качество абстракций больше проявляется в способности вносить изменения не влияя на клиентский код. И тут как бы все хорошо. То что перейти с какого postgresql на что-то типа orientdb нам уже будет не так легко — ну да… целью никогда не было "замена базы".


    Так… это то что касается только вопроса data mapper… Отдохну и пойду обозревать что д вы там наизобретали за дикую смесь из row data gateway и репозиториев (которые на самом деле не репозитории а какие-то менеджеры)

    • 0
      Но повторюсь. В ситуациях когда ограничения ORM начинают мешать нам делать чистые модели, имеет смысл думать в сторону несколько других подходов и в принципе отказаться от идеи писать и читать из одного места, оптимизировав модель данных для удобного решения задачи.
      Вы и сами знаете, что с разными хранилищами появляется множество других издержек. Часто введение второго хранилища неприемлемо/невыгодно, но существующий код как-то надо в порядок привести. Вот я и искал решение.

      Прошу ознакомиться с такой интересной штукой как double dispatch

      Вы имеете ввиду внедрение encoder через метод (внедрение метода)? Как я уже отметил в статье, я рассматривал такую возможность — но тогда придется для вызова метода везде таскать за собой encoder. Неудобно.

      Есть такая штука, которой руководтсвуются все эти любители доктрин — persistance ignorance. То есть с точки зрения сущности они существуют всегда как если бы они просто в памяти лежали. Это значит что если вы вызвали конструктор сущности — он больше не должен вызываться. Никогда.
      Пресловутый persistence ignorance — вот эту концепцию я считаю в корне неверной. Объекты начинают изображать из себя структуры данных — строки БД, документы или еще что. Легче инкапсулировать взаимодействие с внешним источником данных (базы таковыми и являются), это легче ляжет на имеющиеся драйверы/API, чем попытка представить все это как объектное хранилище в памяти приложения — слишком широкая абстракция. А чем она шире — тем больше вероятность, что потечет.
      • +2
        Часто введение второго хранилища неприемлемо/невыгодно

        я больше в контексте новых проектов. Для того что бы было не невыгодно/неприемлимо — нужно что бы разработчик сразу мог выбирать подходы которые нужны для данного конкретного проекта. ВСе просто и все понятно — можно смотреть в сторону того что удобно сразу. Ничего не понятно и все сложно — стоит сохранять вообще все (стрим изменений, доменные ивенты) — тогда вы в любой момент времени сможете строить проекции данных так как вам удобно. Да, это требует намного более высокого уровня разработки нежели умение делать 3-ю нормальную форму. И это проблема обучения разработчиков.


        Объекты начинают изображать из себя структуры данных — строки БД, документы или еще что.

        вы уверены что вы правильно понимаете эту идею? Попробуйте мне ее объяснить.

        • +1
          persistence ignorance — подход в разработке ПО, предлагающий старт и ведение разработки в терминах бизнес-модели. С помощью ООП реализуется основная бизнес-логика, образующая ядро системы. После этого выбирается хранилище и под бизнес-слоем строится персистентный слой.

          Я сторонник обратного подхода — начинаем с выбора БД и тщательно проектируем ее структуру.
          • +2
            persistence ignorance — подход в разработке ПО, предлагающий старт и ведение разработки в терминах бизнес-модели. С помощью ООП реализуется основная бизнес-логика, образующая ядро системы.

            Ну и как из этого следует "Объекты начинают изображать из себя структуры данных — строки БД, документы или еще что."?


            Я сторонник обратного подхода — начинаем с выбора БД и тщательно проектируем ее структуру.

            Самое занятное, что при использовании Data Mapper эти два подхода друг другу не противоречат.

            • 0
              Это было теоретическое определение. По-моему это утопи, попытки реализации которой выливаются в обозначенные «пассивные записи».
              • +2
                По-моему это утопи, попытки реализации которой выливаются в обозначенные «пассивные записи».

                Да нет, это жизнь такая.


                Впрочем, есть и другое, более узкое (и потому более эффективное) понимание persistence ignorance — это когда сущность в доменной модели не имеет никакой зависимости от используемой persistence-технологии. То есть — никакого обязательного базового класса, никаких маркировочных атрибутов, никаких обязательных свойств/полей/методов, никаких ограничений на конструктор и так далее. Иными словами, можно взять только модуль с доменными сущностями, вставить в другоей проект, где нет перзиста вообще — и он будет работать.

                • +1
                  И перед вставкой этого кода написать целый инфраструктурный слой по сохранению этих сущностей. Который в добавок будет протекать логика, реализация которой в объектах будет сильно тормозить.
                  • 0
                    И перед вставкой этого кода написать целый инфраструктурный слой по сохранению этих сущностей.

                    Зачем?..

                    • 0
                      Должны же они как-то сохраняться в новом окружении, где может быть совсем иное хранилище. Не Doctrine единой.
                      • 0

                        Нет, не должны, зачем? Речь же не о том, что мы взяли кусок и перенесли в другой проект, где он будет делать все то же самое. Речь — в основном — о простом техническом критерии, позволяющем отличить persistence-ignorant-реализации от persistence-aware-реализаций.


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

                        • 0
                          Иными словами, можно взять только модуль с доменными сущностями, вставить в другоей проект, где нет перзиста вообще — и он будет работать.

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

                          А для чего тогда нужен этот критерий и соответствующий ему код?

                          • 0

                            Чтобы проверить, что технология хранения не оказывает избыточного влияния на домен (как с точки зрения логики, так и с точки зрения практик разработки).

                          • 0

                            Угу. Или банально тесты "хочется" писать попроще.

                      • 0

                        Приведите пример задачи где у вас такое происходило. И тогда будет более предметная дискуссия.

                        • 0
                          Не происходило, потому что таких задач перед собой не ставлю. Хранилище не меняю, в том числе и по результатам анализа возникающего в этом случае перечня необходимых работ. DQL же придется переписывать? Все сложные выборки и модифиакции, не влезающие в объекты придется. Да, если с MySQL на Postgres переезжать — поедем вместе с Doctrine ORM и DQL, в этом случае да, почти ничего переписывать не придется.
                          • 0

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

                            • 0

                              Да, восприятие бизнес-логики облегчается, а вот восприятие слоя персистентности, соответственно, усложняется.

                              • 0

                                да, но поскольку в моем случае БД это лишь тупое хранилище — не сказать что сильно.

            • +2
              > но тогда придется для вызова метода везде таскать за собой encoder.

              Казалось бы, в 2017 пробрасывать контекст в монаде умеет даже необразованная молодежь из глубинки.
              • 0
                Неудобно.

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

                • 0
                  Это если говорить конкретно про $encoder. Но могут быть и другие, более востребованные зависимости. Нельзя из-за ограничений ORM игнорировать/избегать внедрения зависимостей через конструктор.
                  • 0
                    Нельзя из-за ограничений ORM игнорировать/избегать внедрения зависимостей через конструктор.

                    1. вы должны понимать что зависимости приводят к повышению связанности
                    2. раз вы умеете выделять контексты, значит разделение на уровень инфраструктуры и уровень логики для вас не новость
                    3. слой логики не должен зависеть от инфраструктуры. Наоборот — можно (пример — архитектура портов и адаптеров).
                    4. штуки вроде encoder это по большей части протечка инфраструктуры в доменную логику, тут стоит тогда отделить те данные которые нужны для аутентификации в некую отдельную сущность (нам же пароль всеравно только для этого нужен). Тогда все будет хорошо.
                    5. другие вещи вроде "запустить финансовую операцию", "сходить в сторонний сервис для синхронизации", "отправить нотификации" прекрасно отделяются от бизнес логики за счет использования доменных ивентов.
                    • 0
                      Интерфейс PasswordHasher определяется в слое бизнес-логики. Почему его нельзя внедрить в сущность User через конструктор? Что это нарушит? Как появится зависимость от инфраструктуры?
                      Почему нужно выделять в события? Почему нельзя через интерфейсы внедрять в сущности зависимости через конструкторы?
                      Для себя пока вижу одну причину — ограничения реализаций ORM.
                      • 0
                        Почему его нельзя внедрить в сущность User через конструктор?

                        потому что он нужен только для одной конкретной операции. Вы же не переживаете что мы передаем в какие-то методы аргументы а в какие-то не передаем.


                        Хэшер паролей не является чем-то что влияет на жизненный цикл пользователя. То есть если мы запихнем его в конструктор — мы явно увеличим связанность системы.


                        Почему нужно выделять в события?

                        Это удобно.


                        Для себя пока вижу одну причину — ограничения реализаций ORM.

                        всему виной принципы проектирования GRASP и SOLID. А вот желание всюду внедрять зависимости как раз таки и порождает проблемы.


                        Есть даже такой тезис — "dependencies is a code smell". Суть его заключается в том что бы всеми силами снижать количество всего что пихается в конструктор. Яркий пример — любители "юнит тестить" людят заводить всякие классы типа Clock для предоставления времени. В то же время можно просто передавать это самое время через аргумент и получить такой же результат уменьшив количество зависимостей и упростив клиентский код.

                        • 0
                          потому что он нужен только для одной конкретной операции. Вы же не переживаете что мы передаем в какие-то методы аргументы а в какие-то не передаем.

                          Хэшер паролей не является чем-то что влияет на жизненный цикл пользователя. То есть если мы запихнем его в конструктор — мы явно увеличим связанность системы.


                          А если есть зависимости, которые влиявют? Например репозиторий связанной записи (если делать без прокси) и ли какой-нибудь шлюз к API?


                          Есть даже такой тезис — "dependencies is a code smell". Суть его заключается в том что бы всеми силами снижать количество всего что пихается в конструктор.

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

                          • 0
                            Например репозиторий связанной записи (если делать без прокси) и ли какой-нибудь шлюз к API?

                            заменяем репозиторий на коллекцию и вуаля. Шлюз к APi — тут уже интереснее, тут нужны конкретные примеры что бы можно было продолжать.


                            что этот принцип введен для того, чтобы оправдать ограничение конструирования.

                            нет, это общий принцип, он не про ORM а просто про объекты. Я дал вам ссылочку на статью которая разбирает уровни юнит тестов. Идея заключается в том что бы декомпозировать логику таким образом, что бы уменьшить количество зависимостей. Это больше относится к вопросам low coupling/high coheasion.


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

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


                            А если в классе больше одного метода, где нужна одна и та же зависимость?

                            нужны конкретные примеры. На моей памяти мне приходилось передавать только хэшер паролей и какие-то калькуляторы чего-нибудь (стратегии по сути разные). Это где-то 3-4 метода на 30-40 сущностей.

                        • 0

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

                          • 0
                            Я знаком с ней. У меня был опыт использования DDD. Очень многословно получается (много кода), но да, ubiqutious language появляется. По событиям делал рассылки и уведомления всякие.
                            • 0
                              Очень многословно получается (много кода)

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

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

                                Избыточными считаю Application-сервисы и кучу классов исключений к ним, DM с нявностью, маппингами и ArrayCollection и прокси.
                                • 0
                                  Избыточными считаю Application-сервисы

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


                                  кучу классов исключений к ним

                                  можете заменить это на события, тут ничего криминального нет)


                                  DM с нявностью

                                  неявность в этом случае — полное отделение domain от persistence. То есть отказ от CRUD модели, information hiding, самодокументируемый код, возможность писать нормальные юнит тесты.


                                  ArrayCollection

                                  Вы наверное не представляете насколько проще становится работать с коллекциями когда у вас они вообще есть и вы используете их не просто как массивы.


                                  прокси

                                  А вот это чистой воды трэйдоф к скорости разработки, когда вы еще не вполне понимаете как именно вы работаете с данными.

                                  • 0
                                    Вы наверное не представляете насколько проще становится работать с коллекциями когда у вас они вообще есть и вы используете их не просто как массивы.

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

                                    • +2

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


                                      Почему вы настолько все в абсолют возвели? Не бывает хороших/плохих подходов, у всех есть плюсы минусы и ограничения. Все призваны решать разные задачи. Из нашего общения как я понимаю у вас нет задач для которых хорош DM (как минимум потому что используемый вами подход к проектированию ставящий базу на первое место идет в разрез с целями для которых я к примеру использую DM), ну о хорошо. Но то что вы не умеете DM готовить и для вас это сложно. И это нормально! Люди разные и разными моделями мыслят, я вот не представляю как можно жить с active record и подобными концептами, не представляю как это может быть хоть кому-нибудь удобно, но знаю людей которым это удобно.


                                      Это как с ООП. Вроде как все называют эти три буквы и думают что они имеют в виду одно и то же, но кто-то мыслит "это процедуры и данные в одном месте" а кто-то "это когда система разделена на "штуки" и они общаются посредствам обмена сообщениями".

                                      • 0
                                        Да конечно, есть. Не спорю.
                  • +2
                    Пресловутый persistence ignorance — вот эту концепцию я считаю в корне неверной. Объекты начинают изображать из себя структуры данных — строки БД, документы или еще что.

                    Вообще-то, persistence ignorance совершенно про другое (и, иногда, приводит к строго обратному — объект перестает хоть как-то напоминать структуру данных, в которой он хранится).

                • +3

                  Хорошая тема, жуткий код, и дикая смесь понятий.

                  • +3

                    Ух… продолжим.


                    В данной статье такие классы называются репозиториями

                    Зачем вводить людей в заблуждение неправильной терменологией? Вы же сами говорите — это шлюз. Тоесть TableGateway или DAO.


                    Для вставки и создания должен использоваться репозиторий.

                    Для вставки (аналогия с положить на полочку) — да. Для создания — нет, это не его ответственность.


                    Есть такой паттерн — Row Data Gateway. С его помощью мы можем отделить модель данных от бизнес логики:


                    class User
                    {
                        private $attributes;
                    
                        public function __construct(string $email, string $name)
                        {
                            $this->attributes = new UserGateway(); // по сути та же AR
                            $this->attributes->email = $email;
                            $this->attributes->name = $name;
                            $this->attributes->save();
                        }
                    }

                    вместо save() можно на самом деле не сохранять ничего в базу, а скажем добавлять в очередь на вставку/апдейт, делая своего рода разделение отдельных операций которые можно потом будет закоммитить одной транзакцией. Это уже по вкусу и как удобнее.


                    Делать же эти "сущности" при выборках будут компоненты — finder-ы:


                    class UserFinder
                    {
                        public function get(int $id): User
                        {
                            $userGateway = UserGateway::model()->find($id);       
                            if (!$userGateway) {
                                throw new UserNotFound();
                            }
                    
                            // гидрируем и возвращаем инстанс `User`.
                        }
                    }

                    md5($password)

                    Вы уверены? Я понимаю что это не суть но все же...


                    Какое-либо разделение на "до сохранения" и "после сохранения" недопустимо.

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


                    То есть да, я согласен что процесс работы с сущностями, особенно сложным графом сущностей, стоит разделять на логические транзакции (можно про Saga шаблон почитать), но как-то вот так — нет уж.


                    В представленном случае статьи и авторы могут храниться в разных БД.

                    Я бы не стал делать упор на "могут храниться в разных БД". Это тоже скорее как показатель изоляции контекстов. То есть да, бесспорно что контексты нужно отделять, но я не до конца понимаю как вам тут помогает интерфейс Author. Скажем я понимаю введение на уровне модуля, которому нужно на автора ссылаться, некого VO с названием Author или AuthorID даже, который будет только идентификатор хранить. А вот интерфейсы…


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

                    Этот способ создает неявную связанность модулей на уровне базы данных. Очень плохой вариант. Вместо этого имеет смысл сделать на уровень выше модуль отвечающий за UI (в случае апишек этот паттерн завется Api Gateway, то же применимо и для обычных страничек). В этом случае этот модуль будет работать с другими модулями через их API (не важно какое, на уровне объектов или через сеть, если вы микросервисы любите)


                    выразить непосредственно на языке БД.

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


                    проектировать методы по требованиям бизнес-логики

                    Методы, а точнее интерфейсы, нужно проектировать исходя из требований клиентского кода. Бизнес логика для этого кода все равно сокрыта внутри.


                    где находится единица работы (Unit of Work).

                    Ну, у вас его как бы нет. Я задумку понимаю — делать свой UoW под каждую операцию, а не один универсальный (и это к слову довольно правильная мысль, просто трудозатратно так делать). Но в вашем случае вы просто говорите "транзакция в базе это и есть UoW" и ничего не делаете что бы это было удобно.


                    Изменения данных отслеживаются с помощью прокси-объектов

                    Как бы, нет. При загрузке сущности оная добавляется в UoW, которая внутри имеет identity map. Эдакая мэпы id => сущность. Помимо сущности UoW так же хранит дегидрированный стэйт на момент загрузки, и когда вы вызываете flush в той же доктрине, она втупую достает стэйт из сущностей и вычисляет изменения. Никакой магии.


                    А вот прокси классы, которые не дают вам возможность использовать final используются только для ленивой подгрузки данных. Если она вам не нужна — вас никто не остановит поставить везде final.


                    можно внедрять зависимости через конструктор.

                    Как вы думаете, насколько сложно работать с объектами, которые не только от своего стэйта работают, но и еще и от внешних зависимостей? пробовали когда-нибудь под такое тесты писать? Старайтесь никогда так не делать.


                    Одним из недостатков является невозможность тестирования без БД объектов

                    У классического row data gateway нет этого недостатка. Вы спокойно можете заменить имплементацию хранилища на что-то in-memory для хранения стэйта (естественно не в случае этих Finder-ов)


                    Тем не менее код, который призван взаимодействовать с БД лучше тестировать вместе с БД.

                    Но бизнес логика не должна взаимодействовать с БД. Мы как раз таки должны отделить ее от страшного внешнего мира.


                    Библиотеки, использующие DataMapper и программный Unit of Work фактически дублируют имеющиеся в БД функции

                    UoW — да, он позволяет нам на уровне приложения объявить границу транзакции и сделать это неявно. Хорошо это или плохо — зависит от контекста. А вот DataMapper — его суть только в том что бы денормализованный результат SQL запроса замэпить на объекты. И все.


                    Фактически вы очень хорошо описали суть проблемы при работе с базой данных — ограниченность разработчиков. "Только data mapper!", "Data Mapper не работает, только active record!". Как способ уйти от боли с AR вы переизобрели свой row data gateway (а есть и еще table gateway). И это только если мы все еще полагаем что нам хватит реляционной базы данных для решения любой проблемы.


                    А проблема то в большинстве случаев — чтение данных. То есть, у нас данных хранятся в 3-ей нормальной форме к примеру, то есть в виде максимально оптимизированнном на запись, а не на чтение. В итоге и страдаем, а быть может нам вообще стоит хранить стрим изменений (типа WAL в базе данных, еще более эмулировать работу оной) и строить по ним проекции так как нам удобно в любую удобную документно-ориентированную базу (что бы не париться с релейшенами).


                    В случае с доктриной например, я могу писать в одни объекты и делать выборку этих же данных в другие объекты путем простой подмены гидратора. Например работать на запись с красивым API, а на чтение писать все в другие объекты с публичными пропертями что бы не городить миллион геттеров. А еще есть Atlas.Orm к примеру, весьма интересный концепт. Видел так же просто реализации data mapper, без лэйзи лоад и с более простой схемой работы UoW позволяющей больше. Но это не популярно потому что "а зачем думать то".

                    • 0
                      Зачем вводить людей в заблуждение неправильной терменологией? Вы же сами говорите — это шлюз. Тоесть TableGateway или DAO.

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

                      То есть да, я согласен что процесс работы с сущностями, особенно сложным графом сущностей, стоит разделять на логические транзакции (можно про Saga шаблон почитать), но как-то вот так — нет уж.

                      Почему невалидные сохраним? Валидация проведется выше. Для записи должны приходить валидные данные. Если нужно, исключение можно выбросить — транзакция БД откатится.
                      При вызове `Repository::add($name, $email...)` производится вставка обязательных данных. Если после этого нужно сделать с этой записью еще что-то, например добавить реляции, транзация БД может быть запущена снаружи до `add()` и завершена после, например `addPhones()`:
                      $txn->begin();
                      try {
                      $user = $repo->add($name, $email);
                      $user->addPhones($phones);
                      $txn->commit();
                      } catch (PossibleException $ex) {
                      $txn->rollback();
                      }
                      • 0
                        Вспомнил, почему не стал называть классы TableGateway и RowGateway. Я знаю про эти паттерны. Репозитории в статье не являются TableGateway, так как могут обращаться к группе связанных таблиц — таблица пользователей, их телефонов, адресов и пр. TableGateway — шлюз к одной таблице Это же верно и для RowGateway — шлюза к одной строке. В статье объекты могут давать доступ не только к строке таблицы пользователей, но и к связанным с ней телефонам, адресам и пр.
                        • 0
                          шлюза к одной строке.

                          в целом понятие строк может быть сокрыто за интерфейсом и тогда все будет вполне валидно. Мне лично нравится название DAO. Оно просто и понятно и можно хоть весь агрегат так дергать.

                          • 0
                            У меня DAO всегда ассоциировался с QueryBuilder и более низкоуровневыми вещами.
                            • +1

                              Рекомендую пересмотреть свои взгляды. По вашей ссылке термин DAO используется некорректно. Все что вы перечислили DAO должно скрывать а не предоставлять для клиентского кода.

                              • 0

                                Спасибо за рекомендацию. Посмотрим.

                        • 0
                          Почему невалидные сохраним?

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


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

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


                          Что мне не нравится — это то что у вас эти операции все же связаны между собой и частичное выполнение нам может быть не подходит. В этом случае нужно либо что бы СУБД умело вложенные транзакции, либо иметь возможность компенсировать неудачи (паттерн Сага).

                          • 0

                            Я использую одну и ту же транзакцию для всех операций.


                            $user->rename($name);
                            
                            $user->changePassword($passwd);
                            

                            Здесь будет две транзакции.


                            $transaction->call(function() {
                                $user->rename($name);
                                $user->changePassword($passwd);
                            });

                            Здесь будет одна. Код в методах rename() и changePassword() не меняется между первым и вторым примером. Если есть транзакция снаружи, внутренние не будут запускаться. Объект транзакции один и тот же внутри и снаружи.

                            • 0

                              вы пробовали в таком стиле запилить хоть сколько нибудь большой проект (хотя бы 3 разработчика)? Я конечно не уверен, но такой стиль управления транзакциями будет скорее всего приводить к большему количеству проблем, ибо легко использовать неправильно.

                              • 0
                                Пробовали. Как раз трое работает. Все хорошо. Проблем не было.
                                • +1
                                  Подход оправдан, использовал его в нескольких проектах. Изменить адрес пользователя и ИНН вполне себе атомарные операции. Если с клиента данные приходят всем скопом — оборачиваем в транзакцию последовательные вызовы.
                                  Сталкивался только с одной проблемой — в уже написанный криво код просто транзакции не вставишь (всплывают локи и таймауты). Если пишешь новый модуль и придерживаешься этой логики — полет стабильный.
                                  • 0
                                    И еще одна проблема в этом подходе: длинные транзакции.
                                    В сложной бизнес логике (с большим количеством расчетов) приходилось отказываться от «обернуть в транзакцию все» (каждый отдельный метод при этом — транзакция).
                                    Приходилось выбирать какой функционал самый «критичный» и вызывать его первым. Если дальнейшие вызовы методов фейлились — первая транзакция все равно была выполненной.
                                    В принципе проблема решается вынесением сложных расчетов «на потом», но код усложняется.
                                    • 0
                                      Спасибо за ценные комментарии
                            • 0

                              Как и UoW, транзакция у нас одна Чтобы было понятно, приведу такой кусочек кода:


                              $transaction->call(function() use($repo) {
                                  $user = $repo->add($name, $email, $phones);  
                              });
                              
                              //код метода add()
                              
                              public function add($name, $email, $phones) {
                                  $this->transaction->call(function() use($name, $email, $phones) {
                                      $ar = new YiiARUser([
                                          'name' => $name,
                                          'email' => $email
                                      ]);
                                      $ar->insert();
                                      $ar->addPhones($phones);
                                  ));
                              
                              }
                              

                              Так как транзакция у нас одна, вызов call() внутри add() не приведет к старту новой транзакции, и не будет ее коммитить, это будет делать первый вызов call(). Таким образом, все пойдет в одну транзакцию, будет один UoW на стороне БД.

                              • 0

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

                                • +1

                                  Разрулить это можно обернув все последовательные вызовы в транзакцию.


                                  $transaction->call(function() {
                                      //внутри add() транзакция не стартанет и не закомитится, хотя вызов call() есть внутри add(), объект транзакции один и тот же, он знает, где она была запущена и где ее закоммитить.
                                      $user = $repo->add($name, $email, $password);
                                      //то же самое внутри changePassword()
                                      $user->changePassword($passwd);
                                      //и addPhones()
                                      $user->addPhones($phones);
                                  });
                                  • 0

                                    ну как в доктрине короче.

                                    • 0
                                      Да! Я и говорю — по большому счету вся разница — где UoW. В Doctrine ORM он программный, здесь он на стороне базы.
                                      • 0

                                        я люблю воспринимать базу данных как умную хранилку. Я принципиально не люблю процедуры (хотя если будет задача где они будут хорошо ложиться — я буду их юзать), я не люблю подходы когда штуки вроде того же ревизионирования перекладываются на UoW и т.д. Я за более явные подходы.


                                        Тот факт что доктрина использует програмный UoW позволяет более явно контролировать цикл жизни сущностей.

                              • +1
                                Как вы думаете, насколько сложно работать с объектами, которые не только от своего стэйта работают, но и еще и от внешних зависимостей? пробовали когда-нибудь под такое тесты писать? Старайтесь никогда так не делать.

                                Разве стабы тут не помогут? При конструировании передаем стаб в конструктор и тестируем, разве нет? Или вы статику Yii имеете ввиду?


                                У классического row data gateway нет этого недостатка. Вы спокойно можете заменить имплементацию хранилища на что-то in-memory для хранения стэйта (естественно не в случае этих Finder-ов)

                                В представленном в статье способе тоже так можно. Если в клиентский код репозитории (шлюзы к множествам объетов) и отдельные объекты (шлюзы к единицам данных) попадают при помощи итерфейсов, например PostRepository и Post, можно написать их реализации, работающие с памятью.

                                • 0
                                  Разве стабы тут не помогут? При конструировании передаем стаб в конструктор и тестируем, разве нет? Или вы статику Yii имеете ввиду?

                                  моки*. Если вы хотите проверить как происходит взаимодействие вашего тестируемого кода с внешними зависимостями — это моки. и их должно быть мало.


                                  Я для своих ребят делал перевод статьи: Возвращаясь к основам: почему юнит тесты это сложно. Рекомендую почитать.

                              • 0
                                но я не до конца понимаю как вам тут помогает интерфейс Author.

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


                                Модуль блогов

                                интерфейсы Author и AuthorRepository


                                Модуль пользователей

                                User и UserRepostirory и их реализации (внутренние)


                                Приложение, которое компонует модули

                                реализации интерфейсов Author и AuthorRepository на основе User и UserRepository.

                                • 0

                                  Author и User ссылаются на один и тот же ряд в базе или у вас есть какое-то разделение? Если так — как вы синхронизируете.

                                  • 0

                                    Реализация интерфейса Author выполнена с помощью User. В конечном счете используется одна AR и одна запись в БД.

                                    • 0

                                      ммм… а интерфейс то зачем? и где тут разделение? Контексты же вообще не должны пересекаться. Да и вы там что-то о разных БД говорили.

                                      • 0
                                        Сначала было два переносимых модуля, которые работали в одном контексте. Модуль User, модуль Blog. Приложение включает оба модуля, они работают на одной БД. Для того, чтобы организовать работу модуля Blog, приложение внедряет в него требуемую реализацию интерфейса Author на основе модуля User.
                                        Далее я указал, что при таком подходе модулю Blog все равно, где лежат авторы, в той же БД или в другой. О контекстах здесь речи не шло.
                                        Затем я написал, что если потребуется JOIN или транзакция, сквозная между этими модулями — это плохо. разделение на модули потекло и потому лучше упредить это и не делать разделение на независимые модули если в обоих используется одна и та же бд.
                                        И тут уже я упомянул о контекстах. Что они не должны пересекаться на БД (одна бд — один контекст)
                                        • 0
                                          приложение внедряет в него требуемую реализацию интерфейса Author на основе модуля User.

                                          реализация гле лежит? в каком модуле?


                                          где лежат авторы, в той же БД или в другой.

                                          приведите пожалуйста пример ситуации когда нам нужно выплюнуть json со списком постов + автор поста. И как у вас реализовано это разделение по вашей схеме.


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

                                          • 0
                                            реализация гле лежит? в каком модуле?

                                            В корне компоновки — приложении. Оно ведь знает, какие у него есть модули.


                                            В статье есть метод получения статей с авторами findAllPostsWithAuthors()


                                            class YiiDbPostRepository implements PostRepository
                                            {
                                            
                                                private $author_repository;
                                            
                                                public function findAllWithAuthors(int $limit): \Iterator
                                                {
                                                    //вытаскиваем статьи
                                                    $ars = YiiARPost::findAll(['limit' => $limit]);
                                            
                                                    $iterator = new \ArrayIterator($ars);
                                            
                                                    $ids = [];
                                                    //собираем id-шки авторов
                                                    foreach ($ars as $ar) {
                                            
                                                        $ids[] = $ar->id;
                                            
                                                    }
                                                   //по id-шкам авторов получаем авторов через внедренный репозиторий (AuthorRepository)
                                                    $authors = $this->author_repository->findAll($ids);
                                            
                                                    return new class($iterator, $this->author_repository, $authors) implements \iterator
                                                    {
                                            
                                                        private $iterator;
                                            
                                                        private $author_repository;
                                            
                                                        private $authors;
                                            
                                                        //...
                                                        public function current()
                                                        {
                                                            $ar = $this->iterator->current();
                                            
                                                           //в декоратор статьи записывается список авторов, который предотвращает запрос при обращении за автором и берет его из ранее вытащенного списка по идентификатору.
                                                            return new AuthoredPost(
                                                                new YiiDbPost($ar, $this->author_repository),
                                                                $this->authors
                                                            );
                                                        }
                                            
                                                    }
                                            
                                              }
                                            
                                            }
                                            
                                            class AuthoredPost implements Post
                                            {
                                            
                                                private $post;
                                            
                                                private $authors;
                                            
                                                public function title(): string
                                                {
                                                    return $this->post->title();
                                                }
                                            
                                                public function content(): string
                                                {
                                                    return $this->post->content();
                                                }
                                            
                                                public function author(): Author
                                                {
                                            
                                                    foreach ($this->authors as $author) {
                                                        if ($author->id() == $this->post->authorId()) {
                                                            return $author;
                                                        }
                                                    }
                                                    throw new DomainException('Статья без автора! Нарушена целостность БД!');
                                            
                                                }
                                            
                                            }
                                            

                                            Преобразование списка в json — дело техники.

                                            • 0

                                              я все еще не понимаю зачем вам имплементить интерфейсы… Я не вижу профита. Вы так DTO делаете?

                                              • 0
                                                У меня нет DTO, есть DAO. Возможна и реализация при помощи DTO.
                                • 0
                                  Для вставки (аналогия с положить на полочку) — да. Для создания — нет, это не его ответственность.

                                  Почему же? Это поможет скрыть конкретную реализацию User, если мы используем интерфейсы, убирая new из клиентского кода. "Не его ответственность" — не аргумент. Нет точного определения единой ответственности — где она начинается и где она заканчивается неизвестно. Я считаю, что вставка записи в БД и создание объекта, представляющего эту запись неотделимы логически, поэтому и решил делать это неразрывно, в рамках одного метода. Как можно представлять шлюз к записи/записям в БД, если в БД нет этих записей?

                                  • 0
                                    Это поможет скрыть конкретную реализацию User

                                    у вас не должно быть необходимости в том что бы это скрывать, если это бизнес объект.


                                    Нет точного определения единой ответственности — где она начинается и где она заканчивается неизвестно.

                                    единая ответственность = единая прчина для внесения изменений. У вашего репозитория — две ответственности. Первая — отвечает за соблюдение бизнес ограничений вроде "что есть обязательные данные", другая — работа со слоем персистентности.


                                    $user = new User('Bob');
                                    $userRepository->add($user);

                                    а по поводу подмены реализаций — можно внутри юзать статические методы фабрики.

                                    • 0
                                      у вас не должно быть необходимости в том что бы это скрывать, если это бизнес объект.

                                      А я и не говорил, что у меня бизнес-объекты. У меня есть приложение, есть данные из БД, есть объекты, обеспечивающие доступ к ним, есть клиентский код, использующий эти объекты. Бизнес-объектов вроде и нет.

                                      • +1

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

                                        • 0
                                          Почему сразу транзакционные скрипты? Логика то в объектах, взаимодействующих с БД и объектах, которые в свою очередь инкапсулируют эти объекты. Нет тут транзакционных скриптов.
                                          • 0
                                            Логика то в объектах, взаимодействующих с БД и объектах, которые в свою очередь инкапсулируют эти объекты.

                                            так все же где она? Зачем это разделение если бизнес логика и там и там? Вы инджектите зависимости в свои "сущности"? Если так, чем это отличается от транзакционных скриптов?

                                            • 0

                                              Такая двоякость наблюдается и при использовании DataMapper. Часть логики в виде независимых манипуляций с объектами, часть в виде запросов, там где объекты невыгодны. В этом плане у меня тоже может быть как зависимая, так и независимая логика. Транзакционный скрипт — это процедурный паттерн. У меня объекты.

                                              • 0
                                                Такая двоякость наблюдается и при использовании DataMapper.

                                                не наблюдается если нормально делать.


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

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


                                                Транзакционный скрипт — это процедурный паттерн. У меня объекты.

                                                где стэйт хранится в отдельном объекте. То есть если мы будем говорить в терминах обычного AR — это те же транзакционные скрипты.

                                • 0
                                  Вы вот говорите, что производительность не проседает.
                                  А как насчет локов в БД при обновлении сущности?
                                  Если у нас есть какой-то UoW, то все изменения группируются в одном запросе.
                                  А в случае AR, у нас будет множество сетевых вызовов, на протяжении которых строчки в базе будут висеть под exclusive lock-ом. А это существенно повышает вероятность deadlock.
                                  • –1
                                    Висеть они будут не намного дольше. На практике не сталкивался с подобными проблемами.
                                    • +2
                                      Вы сталкивались с проектами с полмиллиона трафика в сутки?
                                      Тогда дедлоки возникают даже в довольно тривиальных частях кода…
                                      Поэтому замечание gnaeus имеет большой смысл
                                  • 0
                                    Не сталкиваюсь. Скажу честно. Могу лишь предположить, что само по себе наличие exclusive-lock, не может являться причиной deadlock'ов. Простой UPDATE также требует exclusive lock. Что теперь, не делать UPDATE?
                                    • 0

                                      один update запрос сам по себе является атомарной операцией. А вот 3 update запроса которые разнесены во времени (скажем 20-30 милисекунд между каждой из ваших логических транзакций) — это уже может быть проблемой.


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

                                    • +2
                                      Первая часть — у меня одни CRUD-приложения где все таблицы маппятся на обьекты один к одному, дата-маппер мне там не нужен, поэтому он плохой.
                                      Вторая часть — обмажем AR кучей интерфейсов у которых никогда не будет больше одной реализации и «абстракций» которые на самом деле ничего не скрывают и не понижают связанности.

                                      Если у вас простой CRUD — не надо пытаться писать код, который хочет казаться умнее, чем от него требуется.
                                      • 0

                                        Вы слишком все упростили. В первой части не только про CRUD. Во второй — почему считаете, что не скрывают? Скрывают взаимодействие с БД, реализацию на основе какой-либо библиотеки/фреймворка.

                                        • 0
                                          То, что ваш YiiDbUser имплементит какой-то интерфейс, не значит, что вы потом можете легко подменить реализацию. Вы же не через DI контейнер будете этого юзера инжектить? Он будет явно создаваться везде где идет с ним работа. В чем вообще смысл делать интерфейс на абсолютно стейтфул классы? И интерфейс User у вас сильно утрированный, это будут мостры с кучей методов, прощай ISP, где на каждое изменение в реализации вы будете идти править бесполезный интерфейс. У вас бизнес-логика не от базы данных пытается абстрагироваться а от самой себя.
                                          • 0

                                            Смогу. new на YiiDbUser производится только в репозитории. Репозиторий тоже закрыт интерфейсом и может быть внедрен через контейнер. Клиенский код будет работать с интерфейсом User.
                                            По поводу множества методов — это не проблема представленного подхода. Можно разбить на несколько классов. Интефейс не бесполезный, так как он помогает изолировать клиенский код от реализации с помощью Yii или чего-то еще.

                                      • +1

                                        А еще, при использовании UoW достаточно легко реализуются всякие инфраструктурные штуки, как:


                                        • Audit Logging,
                                        • Optimistic Concurrency,
                                        • ручная реализация WAL,
                                        • etc.

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

                                        • 0

                                          Здесь то же есть UoW, только он на стороне БД. Audit Logging, Optimistic Concurrency — это тоже можно сделать при таком подходе. Определение транзакции MySQL. Как и UoW ее тоже можно откатить. Зачем дублировать то, что уже есть в БД?

                                          • +3

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

                                            • 0
                                              Сначала напишу в одной транзакции с изменением данных строку в таблицу событий. Воркером потом вытащу ее и поставлю в очередь, сделаю закладку, что событие с таким-то id поставлено в очередь. А как вы сделаете запись в очередь совместно с модификацией данных в БД, причем консистентно? Что если у вас событие в очередь ушло а потом транзакция откатилась? Или наоборот, транзакция закоммитилась, а очередь оказалась недоступна. С таблицей событий в одной БД с данными это разрулится.
                                              • 0
                                                Что если у вас событие в очередь ушло а потом транзакция откатилась?

                                                как раз таки програмный UoW позволяет мне четко трекать когда у меня логическая транзакция завершилась успешно и именно в этот момент события кидаются в очередь.


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


                                                а очередь оказалась недоступна.

                                                это как так то?

                                                • 0
                                                  это как так то?

                                                  сеть упала?

                                                • 0
                                                  Тут вопрос не только в консистентности, а еще и в способе формирования этой строки в таблице событий. Как Вы будете ее формировать? Вручную?
                                                  А если автоматически – так это у Вас уже получается что-то похожее на UoW. Только изобретенное заново.
                                                  • 0
                                                    Сначала напишу в одной транзакции с изменением данных строку в таблицу событий.

                                                    То есть уже лишняя таблица в БД. Которая станет ботлнеком.


                                                    Воркером потом вытащу ее и поставлю в очередь

                                                    То есть еще лишний воркер.


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

                                                    А где-то было требование консистентности?


                                                    Впрочем, есть очереди, поддерживающие распределенные транзакции. И есть паттерны, где очередь до БД.


                                                    С таблицей событий в одной БД с данными это разрулится.

                                                    На самом деле, нет. Вам придется делать ту же самую распределенную транзакцию, только в воркере.

                                                    • 0
                                                      А где-то было требование консистентности?

                                                      А как же без нее? Данные терять? Не выполнять/пропускать что-то? Консистентность скорее редко когда не нужна, чем нужна. Можно сказать, что она желательна всегда.


                                                      На самом деле, нет. Вам придется делать ту же самую распределенную транзакцию, только в воркере.

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


                                                      И есть паттерны, где очередь до БД.

                                                      Это другая архитектура. Ее применимость зависит от задачи.


                                                      Впрочем, есть очереди, поддерживающие распределенные транзакции. И есть паттерны, где очередь до БД.

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

                                                      • –1
                                                        А как же без нее?

                                                        А легко.


                                                        Консистентность скорее редко когда не нужна, чем нужна. Можно сказать, что она желательна всегда.

                                                        … вот только консистентность бывает разная.


                                                        Ну и как бы да, есть же тривиальный паттерн: пишем в очередь до и после транзакции в БД, в воркере (все равно же у вас воркер) делаем корреляцию, жизнь прекрасна.

                                                        • 0
                                                          В итоге все сводится к «зависит от задачи»
                                                          • +2

                                                            Ну да. Но чем больше у вас cross-cutting concerns, тем сложнее их сделать на БД (особенно когда вы пытаетесь сделать БД максимально быстрой, или вам надо делать работу с более чем одной СУБД и так далее).

                                            • 0
                                              Возможно лезу не в свою песочницу. К Yii или PHP отношения не имею… ИМХО. Разрабатываю обычные Клиент-Сервер БД приложения — Декларативная компоновка интерфейса + чистый SQL для обработки событий.

                                              Обсуждаемые ActiveRecord и DataMapper конечно мощные, но никак не простые инструменты разработки.

                                              Не готов пока обсуждать какую-то иную архитектуру, прошу лишь обратить внимание на интересное наблюдение:
                                              1. Не обязательно тащить связи из БД и эмулировать на клиенте сложные иерархические структуры данных;
                                              2. Достаточно создавать архитектуру приложения из компонентов, которые самостоятельно (независимо друг от друга) работают с БД, при этом обмениваются друг с другом данными исключительно в табличном виде (даже сообщение о некоем событии посылают друг другу в виде набора однородных записей);
                                              3. Код такого приложения превращается в декларативное описание слабосвязанных объектов;
                                              4. Главное преимущество: любые два компонента, созданные независимыми разработчиками, могут быть связаны друг с другом без единой строчки (императивного) кода, так как при посыле сообщения между объектами структура данных сообщения (таблица) отправителя автоматически конвертируется в структуру данных сообщения (таблицу) получателя, для этого достаточно (декларативно) описать соответствия имен полей двух таблиц и гарантировать преобразование простых типов данных.
                                              • 0
                                                при этом обмениваются друг с другом данными исключительно в табличном виде

                                                Зачем нужно это ограничение?


                                                Код такого приложения превращается в декларативное описание слабосвязанных объектов;

                                                А логика-то как описывается?


                                                так как при посыле сообщения между объектами структура данных сообщения (таблица) отправителя автоматически конвертируется в структуру данных сообщения (таблицу) получателя

                                                Enterprise Message (Data) Bus. Были сильно популярны лет пять и дальше назад. Ну или, если более обобщенно, SOA вообще.


                                                Основная проблема состоит в том, что логика никуда не пропадает — она просто переносится в мапперы/фильтры/роутеры, поддерживать которые не так уж и просто.

                                              • 0
                                                при этом обмениваются друг с другом данными исключительно в табличном виде

                                                Зачем нужно это ограничение?


                                                Мысль о том, что данное ограничение дает преимущество — прозрачную, простую архитектуру описанную в декларативной парадигме.

                                                Если интересно, кинте в личку какую-нибудь задачу, распишу её решение в собственной интерпретации.
                                                • 0
                                                  Мысль о том, что данное ограничение дает преимущество — прозрачную, простую архитектуру описанную в декларативной парадигме.

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


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

                                                  • 0
                                                    На вопрос:
                                                    А почему нельзя описать прозрачную простую архитектуру в декларативной парадигме, не используя данные в исключительно табличном виде?

                                                    как и на вопрос: А что такое прозрачная архитектура?
                                                    бесполезно отвечать без конкретного примера. С вас пример, с меня решение. Окружающие пусть сами решают, кому, какое решение прозрачнее.

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

                                                    Мы же говорим «О паттернах проектирования для работы с РСУБД», где все данные уже представлены в табличном виде.
                                                    • 0
                                                      С вас пример, с меня решение.

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


                                                      Мы же говорим «О паттернах проектирования для работы с РСУБД», где все данные уже представлены в табличном виде.

                                                      Эм… нет. Мы говорим об архитектуре приложения ("Достаточно создавать архитектуру приложения из компонентов"). Что и как там хранится в РСУБД разработчиков компонентов волнует мало.

                                                      • 0
                                                        Ну например: через систему проходит поток сообщений (произвольной структуры)...

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

                                                        Эм… нет. Мы говорим об архитектуре приложения («Достаточно создавать архитектуру приложения из компонентов»). Что и как там хранится в РСУБД разработчиков компонентов волнует мало.

                                                        Эм… тема статьи "… работа с РСУБД".
                                                        • 0
                                                          Эм… тема статьи "… работа с РСУБД".

                                                          Общение двух компонентов в приложении — если они не общаются через РСУБД (а вы явно пишете, что нет) — не может быть "работой с РСУБД". Поэтому вы прямо в первом же комментарии вышли за пределы темы статьи.

                                                          • 0
                                                            О как! Под такое Ваше представление проблематики «работы с РСУБД» подпадет только CRUD.
                                                            В ОРМ связи между объектами могут быть описаны и без явной реализации в СУБД.
                                                            Да и сами объекты могут быть из разных РСУБД, а связи между ними как-то реализовывать надо — эта проблема тоже к теме статьи не относится?
                                                            • +1
                                                              Под такое Ваше представление проблематики «работы с РСУБД» подпадет только CRUD.

                                                              А какой еще присущий всем РСУБД сценарий я упустил?


                                                              В ОРМ связи между объектами могут быть описаны и без явной реализации в СУБД.

                                                              Да, и что?


                                                              Да и сами объекты могут быть из разных РСУБД, а связи между ними как-то реализовывать надо — эта проблема тоже к теме статьи не относится?

                                                              Это вопрос к автору статьи, а не ко мне.


                                                              Только не надо путать связь между объектами и общение между компонентами. Это не одно и то же.

                                                • 0

                                                  Разработчик бд смотрит на ваш код как на кучу ненужного говна.

                                                  • 0

                                                    Как и пхпшники часто смотрят на код датабазников)

                                                • +3
                                                  Апологеты DataMapper отмечают, что этот паттерн предоставляет возможность абстрагироваться от БД и программировать в "терминах бизнес-объектов"… Якобы это должно позволить достичь отделения бизнес логики от БД.

                                                  Да, DataMapper предоставляет возможность абстрагироваться от БД и её структур. В отличие от ActiveRecord, где поля в объекте равны полям в БД по определению.


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

                                                  Операции на множестве записей требуются в основном на чтение для построения отчётов и других выборок с витиеватыми JOIN-ами.


                                                  Обычный программист хочет выбрать одну единственную ORM. И вдруг пытается сделать отчёт на Doctrine и попадает в тупик неудобства и непроизводительности из-за своей неопытности. И ругается потом в комментариях, что кругом тормоза.


                                                  Ведь он ещё не знает, что чтобы не таскать тяжёлые доменные сущности можно использовать разделение на мощный Doctrine с сущностями с бизнес-логикой для доменных процессов и на отдельную лёгкую ReadModel с голым SQL и DTO для листингов. Тогда и страницы открываются за 0.005 с, и все прелести мощной ORM для бизнес-логики остаются.


                                                  Сущности, несмотря на отсутствие внутри SQL-кода, все равно зависят от БД. Особенно это проявляется при программировании связей (одна сущность объявляется главной, другая подчиненной). Реляционные отношения так или иначе протекают в объектную структуру.

                                                  Даже в Doctrine ORM ничего не протекает. Связь там реализована простым приватным полем со связанной сущностью или ArrayCollection, подменяемыми на Proxy-объект. Если же внешние связи между агрегатами делать по прямому присваиванию идентификаторов вместо присваивания объектов, то неудобств слежения за всеми связями не останется, так как настоящие связи останутся только внутри агрегатов.


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

                                                  На самом деле это как раз два альтернативных подхода: "бизнес-объекты" с бизнес-логикой для сложных приложений и анемичные "пассивные записи" без логики для CRUD-приложений.


                                                  ORM — это Object-Relation Mapping — вещь, нужная для преобразования объекта в реляционные таблицы и обратно.


                                                  Для "пассивных записей" (это как раз не полноценные объекты, а голые структуры или ассоциативные массивы полей из БД без бизнес-логики) идеально подходит ActiveRecord (паттерн "Активная запись", представляющий активную строку из БД).


                                                  А для сложных "бизнес-объектов" (полноценных объектов со сложной структурой, порой несовпадающей со структурой БД) как раз и нужен Data Mapper (паттерн "Преобразователь данных" для преобразования данных объекта в БД и обратно).


                                                  Просто ActiveRecord парсит всё банально "один к одному", а в DataMapper можно вручную настроить сколь угодно замысловатое преобразование по "карте преобразования". Там можно описать, какое поле из БД в какое поле сущности попадёт. Например, чтобы created_at INTEGER(11) из БД попало в $this->createDate = new DateTimeImmutable в сущности.


                                                  Особенно хорошо это заметно в CRUD-приложениях.

                                                  Так в этом и весь смысл. Есть простые CRUD-приложения со структурами данных без логики, где хватит простого ActiveRecord. И есть сложные приложения с бизнес-логикой и полноценными ООП сущностями, где уже нужен Data Mapper для конвертации. И использовать мощный DM в простом CRUD — это оверхед.


                                                  Поэтому тезис "У меня есть только CRUD-ы и я не пишу сущностей-объектов, поэтому не вижу смысла в DataMapper" данной статьи не является общеприменимым.


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

                                                  Это правильный способ лишь для сервисов. А сущности — это не сервисы. Для них DIC и Constructor Injection не используют.


                                                  В статьях и докладах по Symfony, Doctrine и DDD все чаще можно встретить тирады про недостатки автоникрементных ключей БД, про то, что сущность при создании без ключа — не сущность и надо использовать генераторы UUID. Это еще один шаг к эмуляции функционала БД в приложении — то, чего в данной статье предлагается избегать.

                                                  Это не эмуляция БД в приложении, а отделение доменного кода от БД. Наличие $model->id перед вставкой даёт возможность не только присвоить связи сразу, но и позволяют разрабатывать как мобильные приложения для работы в оффлайне, так и децентрализованные микросервисные проекты. Если бы исторически в БД не придумали автоинкремент, то использование генераторов проблемой бы никто не считал. Например, в PostgreSQL это не проблема, так как все автоинкременты берутся из отдельных от таблиц секвенций, и следующий id можно легко запросить вручную из этой секвенции перед вставкой записи в таблицу.


                                                  Многие известные проблемы AR как раз вызваны тем, что разработчики пытаются предварительно собрать граф из AR в памяти и при вызове save() сохранить все их сразу…

                                                  В таком случае и БД не дёргается лишними запросами, и транзакцией можно обернуть только метод save. Не вижу, в чём здесь проблема.


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

                                                  Здесь уж определитесь с целями. Либо через простой ActiveRecord в таком Eloquent-подобном стиле вперемешку с SQL работаете напрямую с полями БД (прописывая логику проверки инвариантов в запросах и тестируя с БД), либо пишите чистые объектно-ориентированные сущности и логику на голом PHP и всю работу с БД выносите куда-нибудь в отдельные классы.


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

                                                  Если нужны именно независимые модули, то по связям можно обойтись простой синхронизацией как в http://www.elisdn.ru/blog/86/module-relations-on-yii2. В случае же наличия репозиториев достаточно будет натравить AuthorRepository на таблицу users и метод getPostsWithAuthors репозитория на JOIN этой же users для заполнения Author или AuthorView без всяких AuthorInterface. И всё.


                                                  Апологеты DataMapper отмечают, что этот паттерн предоставляет возможность абстрагироваться от БД и программировать в "терминах бизнес-объектов"… Якобы это должно позволить достичь отделения бизнес логики от БД.

                                                  Да, повторюсь, именно позволяет программировать в "терминах бизнес-объектов", не думая ни о каких ограничениях конкретной БД. Code First вместо DB First.


                                                  Сначала напишем и протестируем бизнес-объект, а потом уже пропишем, какие поля и как в каую таблицу БД попадут: хоть в одну таблицу, хоть в две, хоть в JSONB-поле.


                                                  Почему не подходит голый ActiveRecord? Как только в не-CRUD-приложении понадобится сделать хоть какие-то оличия от структуры БД, так в ActiveRecord сразу получаем геморрой с реализацией этого соответствия в виде внедрения своего Mapping-а вроде "как мне массив в AR перегнать в JSON в БД". Именно здесь "тупо" использовать ActiveRecord уже не получится и придётся наворотить вручную больше кода, чем бы это заняло с DataMapper.


                                                  Во-первых, сущности в CRUD-приложениях так и останутся анемичными.

                                                  Про CRUD уже говорили, что в мире есть и не-CRUD-приложения.


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

                                                  Это, собственно, то, для чего и придумали ООП. Для инкапсуляции состояния и поведения в объектах и для разделения ответственностей. Призываете отказаться от ООП отделением состояния от поведения в пользу процедурного или функционального подхода?


                                                  Если хотите примеров по проектированию сложных бизнес-сущностей и сравнением репозиториев/Doctrine/ActiveRecord можете посмотреть цикл статей http://yiiframework.ru/forum/viewtopic.php?f=34&t=43009. Там сущность спрограммирована и протестирована в первой статье, а к БД привязана уже в последних. И как раз рассматривается способ внедрения маппинга поверх AR для отделения логики от БД.

                                                  • 0
                                                    Это правильный способ лишь для сервисов. А сущности — это не сервисы. Для них DIC и Constructor Injection не используют.

                                                    Кто сказал что он правильный? Разработчики Doctrine? Почему нельзя внедрять через конструктор, а через методы можно? Чем сущность хуже/лучше других объектов? Такое вот разделение как раз и возвращает нас к процедурному подходу, где сервисы это на самом деле функции, а сущности это структуры данных.


                                                    Да, DataMapper предоставляет возможность абстрагироваться от БД и её структур. В отличие от ActiveRecord, где поля в объекте равны полям в БД по определению.

                                                    В статье не пропагандируется чистый ActiveRecord. В случае использования интерфейсов и композиции (вторая часть статьи) также можно не оглядываться на структуру БД и названия колонок.


                                                    Ведь он ещё не знает, что чтобы не таскать тяжёлые доменные сущности можно использовать разделение на мощный Doctrine с сущностями с бизнес-логикой для доменных процессов и на отдельную лёгкую ReadModel с голым SQL и DTO для листингов. Тогда и страницы открываются за 0.005 с, и все прелести мощной ORM для бизнес-логики остаются.

                                                    А может быть наоборот, хорошо знает, пользовался и потому считает излишним, с кучей оверхеда. DataMapper меньше дает чем требует. SQL и РБД знаем — этого достаточно. Зачем Doctrine учить? Чтоб потом программистов дольше искать и вводить в курс дела?


                                                    На самом деле это как раз два альтернативных подхода: "бизнес-объекты" с бизнес-логикой для сложных приложений и анемичные "пассивные записи" без логики для CRUD-приложений.

                                                    Не будут они с бизнес-логикой без внедрения зависимостей. Логика на данных одной сущности может весьма ограничена. Про внедрение метода было сказано.


                                                    Это не эмуляция БД в приложении, а отделение доменного кода от БД. Наличие $model->id перед вставкой даёт возможность не только присвоить связи сразу, но и позволяют разрабатывать как мобильные приложения для работы в оффлайне

                                                    Как потом будете синхронизировать? Пример для примера. В реальности не делают такого.


                                                    В таком случае и БД не дёргается лишними запросами, и транзакцией можно обернуть только метод save. Не вижу, в чём здесь проблема.

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


                                                    Здесь уж определитесь с целями. Либо через простой ActiveRecord в таком Eloquent-подобном стиле вперемешку с SQL работаете напрямую с полями БД (прописывая логику проверки инвариантов в запросах и тестируя с БД), либо пишите чистые объектно-ориентированные сущности и логику на голом PHP и всю работу с БД выносите куда-нибудь в отдельные классы.

                                                    Я определился. Запросы, без собирания графа объектов в памяти. Тестирование с БД. Из статьи это непонятно?


                                                    Да, повторюсь, именно позволяет программировать в "терминах бизнес-объектов", не думая ни о каких ограничениях конкретной БД. Code First вместо DB First.

                                                    Основная масса веб-приложений пишется начиная с БД. БД надо подбирать даже тщательнее чем язык. Code First — красивая сказка, о которой хорошо статьи и книги писать.


                                                    Именно здесь "тупо" использовать ActiveRecord уже не получится и придётся наворотить вручную больше кода, чем бы это заняло с DataMapper.

                                                    class SomeClass 
                                                    {
                                                    
                                                        public function someData(): array
                                                        {
                                                            return json_decode($this->ar->data, true);
                                                        }
                                                    
                                                    }

                                                    Это много?


                                                    Это, собственно, то, для чего и придумали ООП. Для инкапсуляции состояния и поведения в объектах и для разделения ответственностей. Призываете отказаться от ООП отделением состояния от поведения в пользу процедурного или функционального подхода?

                                                    Ни в коем случае. Как писать нормальную бизнес-логику в сущностях без полноценного конструктора с внедрением зависимостей? Пользоваться внедрением метода и всюду таскать зависимости для вызовов?


                                                    Если хотите примеров по проектированию сложных бизнес-сущностей и сравнением репозиториев/Doctrine/ActiveRecord можете посмотреть цикл статей http://yiiframework.ru/forum/viewtopic.php?f=34&t=43009. Там сущность спрограммирована и протестирована в первой статье, а к БД привязана уже в последних. И как раз рассматривается способ внедрения маппинга поверх AR для отделения логики от БД.

                                                    Как раз видел и читал. Пробовал использовать. Получается очень многословно. Не знаю, насколько богата должна быть предметная область, чтобы оно себя оправдало. В интернет-магазинах и досках объявлений выглядит излишним и все усложняет.


                                                    Про нарушение основного принципа ООП при использовании рефлексии для маппинга что-нибудь скажете?

                                                    • +3
                                                      Наличие $model->id перед вставкой даёт возможность не только присвоить связи сразу, но и позволяют разрабатывать как мобильные приложения для работы в оффлайне
                                                      Как потом будете синхронизировать?

                                                      Да легко. Вот у нас мобилка, в офлайне. "Заливаем" фотографию — присвоили id, положили в очередь. Теперь "заливаем" пост с этой фотографией — создали пост, поставили в нем id фотографии, присвоили id посту, положили в очередь. Теперь правим пост — создали сообщение с обновлением, проставили id поста, положили в очередь. И так до бесконечости (ну то есть пока место в очереди есть).


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

                                                      • 0
                                                        А если ресурс разделяемый и другой пользователь статью на сервере отредактирует до того, как мобилка в онлайн выйдет?
                                                        • 0

                                                          Во-первых, в этом сценарии все ресурсы "разделяемые", но все они созданы на этой мобилке, поэтому конкурентности быть не может. Во-вторых, в вашем сценарии (статья есть на сервере) у нее уже есть id, и, таким образом, к обсуждаемому примеру этот сценарий отношения не имеет. Ну и в-третьих, для решения проблемы конкуретного редактирования есть больше одного паттерна, начиная с оптимистической блокировки и заканчивая независимой накаткой каждого изменения.

                                                          • –1

                                                            Ну так и описали бы сценарий сразу поподробнее. В представленном случае да, оффлайн можно. С разделяемым доступом — проблематично.

                                                            • 0
                                                              Ну так и описали бы сценарий сразу поподробнее.

                                                              В сценарии сразу было описано, что все сущности создаются.


                                                              В представленном случае да, оффлайн можно. С разделяемым доступом — проблематично.

                                                              Проблематично что? Выдавать id новосоздаваемых ресурсов заранее? Да нет, это никак не зависит от разделяемости доступа. Делать редактирование? Ну да, проблематично, но это не зависит от ключей (и вообще от ORM, хотя некоторые упрощают оптимистичную блокировку).

                                                          • +1

                                                            А вы думаете что с инкрементными идентификаторами у вас не может быть конфликтов при синхронизации изменений?

                                                        • 0
                                                          Кто сказал что он правильный? Разработчики Doctrine? Почему нельзя внедрять через конструктор, а через методы можно? Чем сущность хуже/лучше других объектов?

                                                          Потому что конструкторы сущностей обычно используют по назначению для конструирования этих сущностей:


                                                          $user = new User($id, $email, $passwordHash);

                                                          а не извлекают уже заполненную сущность из DIC. Вместо этого хотите в конструктор инъектить абсолютно все сервисы, которые пригодятся сущности по жизни?


                                                          $user = new User($id, $email, $password, $passwordHasher, ..., ..., $confirmTokenizer, ..., $authTokenizer, ..., $ratingCalculator);

                                                          Если да, то… думаю, что Вы не используете конструкторы сущностей по назначению.


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

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


                                                          В статье не пропагандируется чистый ActiveRecord. В случае использования интерфейсов и композиции (вторая часть статьи) также можно не оглядываться на структуру БД и названия колонок.

                                                          В статье пропагандируется прямая работа с БД сквозь ActiveRecord в стиле Eloquent в Laravel. Это понятно и порой удобно. Но вокруг куча обид на "сильно сложный" Doctrine с призывами его не использовать. Это уже не так понятно.


                                                          А может быть наоборот, хорошо знает, пользовался и потому считает излишним, с кучей оверхеда. DataMapper меньше дает чем требует. SQL и РБД знаем — этого достаточно. Зачем Doctrine учить? Чтоб потом программистов дольше искать и вводить в курс дела?

                                                          Что там такого сложного учить? Прописать аннотации или YML для полей и связей и всё работает.


                                                          Как потом будете синхронизировать? Пример для примера. В реальности не делают такого.

                                                          Не знаю, в какой реальности Вы живёте, а я каждый день пользуюсь Evernote. Появился интернет и все заметки по UUID и версиям синхронизировались.


                                                          Основная масса веб-приложений пишется начиная с БД. БД надо подбирать даже тщательнее чем язык. Code First — красивая сказка, о которой хорошо статьи и книги писать.

                                                          Это так только у Вас. Не обобщайте. У нас же сначала читается ТЗ и пишется по Code First для Task Based UI. И порой даже с TDD.


                                                          Это много?

                                                          Ну так Вы же не всё вписали. В жизни же так:


                                                          class Order extends ActiveRecord
                                                          {
                                                              public function getStatuses(): array
                                                              {
                                                                  return array_map(function ($row) {
                                                                      return new Status(
                                                                          $row['value'],
                                                                          new \DateTimeImmutable($row['date'])
                                                                      );
                                                                  }, Json::decode($this->ar->statuses));
                                                              }
                                                          
                                                              public function addStatus($value, $date): array
                                                              {
                                                                  $statuses = $this->getStatuses();
                                                                  $statuses[] = new Status($value, $date);
                                                                  $this->ar->current_status = $value;
                                                                  $this->ar->statuses = Json::encode(array_map(function (Status $status) {
                                                                      return [
                                                                          'value' => $status->getValue(),
                                                                          'date' => $status->getDate(),
                                                                      ];
                                                                  }, $statuses)
                                                                  $this->ar->updateAttributes([
                                                                      'current_status' => $this->ar->current_status,
                                                                      'statuses' => $this->ar->statuses,
                                                                  ]);
                                                              }
                                                          }

                                                          пока остальные в Doctrine "мучаются":


                                                          class Order
                                                          {
                                                              public function getStatuses(): array
                                                              {
                                                                  return $this->statuses->toArray();
                                                              }
                                                          
                                                              public function addStatus($value, \DateTimeImmutable $date): array
                                                              {
                                                                  $this->statuses->add(new Status($value, $date));
                                                                  $this->currentStatus = $value;
                                                              }
                                                          }

                                                          прописав свой тип StatusesType.


                                                          Ни в коем случае. Как писать нормальную бизнес-логику в сущностях без полноценного конструктора с внедрением зависимостей? Пользоваться внедрением метода и всюду таскать зависимости для вызовов?

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


                                                          Про нарушение основного принципа ООП при использовании рефлексии для маппинга что-нибудь скажете?

                                                          Стоит задача сохранить объект в БД и восстановить его назад в том же состоянии. При работе с реляционными хранилищами вместо объектных приходится вручную извлекать сокрытые данные с помщью искусственных средств вроде рефлексии или привязки анонимных функций. Увы, объектные БД не популярны.


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


                                                          Помещение же объекта в БД или кеш и извлечение его обратно не модифицирует объект, а сохраняет его полностью нетронутым в том же валидном состоянии, в котором он находился перед сохранением. Следовательно, использование рефлексии для маппинга для сохранения в БД и сериализации для сохранения в кеш абсолютно безопасны для инварианта, так как по определению никак не модифицируют данные. Так что состав преступления отсутствует. Рефлексия — добро.

                                                          • 0
                                                            а не извлекают уже заполненную сущность из DIC. Вместо этого хотите в конструктор инъектить абсолютно все сервисы, которые пригодятся сущности по жизни?

                                                            Я про DIC вообще ничего не говорил. Он тут вообще непричем, давайте его сюда не примешивать. Я говорил про внедрение. Про "по назначению". Назначение определяется программистом. Объекту для работы нужна зависимость, ведь объекты это не только состояние, но и поведение в котором можно обращаться к другим объектам. Зависимость используется в трех методах. Ее хорошо бы внедрять через конструктор. Почему нельзя ее внедрить через конструктор, а через метод можно?


                                                            $user = new User($id, $email, $password, $passwordHasher, ..., ..., $confirmTokenizer, ..., $authTokenizer, ..., $ratingCalculator);

                                                            Если такое возникает, это не проблема внедрения зависимостей (внедрения конструктора). Класс делает слишком много. Ситуация известна как "сверхвнедрение конструктора". Это основной индикатор того, что у класса слишком много обязанностей. Нужно разбивать поведение по разным классам. Используя внедрение метода, мы лишаемся этого индикатора, скрываем проблему. а не решаем ее.


                                                            Не знаю, в какой реальности Вы живёте, а я каждый день пользуюсь Evernote. Появился интернет и все заметки по UUID и версиям синхронизировались.

                                                            UUID для идентификаторов может быть полезен. В статьях и книгах по DDD его рекомендуют использовать везде, просто потому что "это хорошо" и так удобнее для Doctrine ORM. Считаю эту позицию ошибочной.


                                                            пока остальные в Doctrine "мучаются": прописав свой тип StatusesType.

                                                            Вот именно. Связанный код оказывается разорван на две части. Анемичный код в сущности и жирный в персистентном слое, Плюс еще маппинги. Размазано по трем местам. Это неудобно и усложняем восприятие кода и вход в проект.


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

                                                            Это опять же говорит, что классы перегружены обязанностями. Проблема не в использовании конструктора.


                                                            Помещение же объекта в БД или кеш и извлечение его обратно не модифицирует объект, а сохраняет его полностью нетронутым в том же валидном состоянии, в котором он находился перед сохранением. Следовательно, использование рефлексии для маппинга для сохранения в БД и сериализации для сохранения в кеш абсолютно безопасны для инварианта, так как по определению никак не модифицируют данные. Так что состав преступления отсутствует. Рефлексия — добро.

                                                            Только объект больше не объект, а структура данных. Который больше ничего не скрывает. Инкапсуляция нарушена. Появляется тесная связь между кодом класса и маппером — через маппинги. Тесная связь это нехорошо, а DataMapper построен на этом.


                                                            База это внешний ресурс, к которому мы обращаемся через подключение. Соответственно, должны быть объекты, которые будут инкапсулировать обращения к базе за своим интерфейсом. DataMapper предлагает делать по другому — создать объекты, которые типо хранятся в памяти, а на самом деле в базе. Таким путем и достигается отделение кода от БД — через создание объектного хранища поверх БД (типо объекты всегда в памяти). В коде база перестает быть внешним ресурсом. Это неестественно. Вот это я считаю ненужной задачей, тем более что вся эта абстракция течет как дырявая крыша — любой массовый UPDATE сразу ее ломает. Плохая она.


                                                            Это так только у Вас. Не обобщайте. У нас же сначала читается ТЗ и пишется по Code First для Task Based UI. И порой даже с TDD.

                                                            Все равно такой код пишется с оглядкой на применяемую БД. Иначе потом долго придется переписывать куски на чистом SQL, чтобы соблюдалась консистентность и не тормозило. Кроме того, такому коду нужен маппер, который должен знать внутреннюю структуру персистентных объектов. А если домен не такой уж и сложный, как в большинстве интернет-магазинов, досках объявлений и прочих сайтах — зачем это все? Быстрее будет без маппинга, который нарушает инкапсуляцию и требует костылей в случае тормозов.


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

                                                            Ну что вы, какие могут быть обиды. Обсуждается непрактичность и неполноценность DataMapper. Критикуются диферамбы, в которых безальтернативно привозносят DataMapper. Вот и все. Хотите сказать, что у этого паттерна нет недостатков?

                                                            • 0
                                                              Ее хорошо бы внедрять через конструктор.

                                                              Почему вы так считаете? К примеру, у вас есть дверь. Что бы ее открыть или закрыть вам нужно вставить ключ (наша зависимость):


                                                              public function open(Key $key): void
                                                              {
                                                                  if (!$key->fitsIn($this->keyhole)) {
                                                                      throw new WrongKey();
                                                                  }
                                                              
                                                                  $this->state = self::OPENED;
                                                              }

                                                              Будете ли вы инджектить эту зависимость, без которой у объекта банально недостаточно всего что бы выполнить бизнес логику, в конструктор? Если нет — почему? Как вы определяете что должно инджектиться через конструктор а что нет?


                                                              Ситуация известна как "сверхвнедрение конструктора"

                                                              По сравнению с primitive obsession это не очень то полезный кодсмэл. Возможно если вы эти аргументы разобьете на VO то у вас уменьшится необходимость в сервисах?


                                                              Связанный код оказывается разорван на две части. Анемичный код в сущности и жирный в персистентном слое

                                                              1. почему анемичный код?
                                                              2. почему этот код должен быть связан? Персистентный слой будет обслуживать наши сущности, ну и в нашем случае "жирный" код будет кодом доктрины.
                                                              3. Как раз таки для того что бы "упростить" восприятие кода мы его и разделяем. Что бы когда человек разбирается с бизнес-концептами ему не пришлось бы отвлекаться на то, как это дело хранится в базе.

                                                              Только объект больше не объект, а структура данных.

                                                              С точки зрения объекта — он остается объектом. Он даже не знает что периодически из него достают весь стэйт через рефлексию и пихают в какую-то там базу. С его точки зрения он существует всегда в памяти. А с точки зрения памяти — объекты это структура данных.


                                                              Инкапсуляция нарушена. Появляется тесная связь между кодом класса и маппером — через маппинги.

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


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


                                                              В коде база перестает быть внешним ресурсом. Это неестественно.

                                                              В коде база является лишь хранилищем, которое закрыто интерфейсом репозитория (не тот который EntityRepository у доктрины, а именно вашим).


                                                              То есть в целом мы никуда от "внешнего" ресурса не ушли, мы лишь спрятали детали. Это позволяет нам выбирать хранилище под задачу а не подгонять задачу под хранилище.


                                                              тем более что вся эта абстракция течет как дырявая крыша — любой массовый UPDATE сразу ее ломает.

                                                              Опишите задачи где вам нужно делать массовый UPDATE и причем тут ORM?


                                                              Обсуждается непрактичность и неполноценность DataMapper.

                                                              Увы что бы это обсуждать нужно видеть как вы использовали DataMapper. Из того что вы описываете — вы либо всегда сталкивались с проектами где использовались анемичные сущности, либо где все было плохо с разделением ответственности.


                                                              У любого подхода есть свои плюсы и минусы. В случае с DM минусы это высокая сложность решения. Об этом можно судить по простой метрике — из production-ready решений в PHP есть только Doctrine. Остальные реализации DM полузаброшены и и не так распространены.


                                                              На тему сложности решений в духе Hibernate есть замечательный доклад Грэга Янга: 8 lines of code. Я думаю вам понравится. Там в целом та же идея. которую высказываете вы (и я согласен что DM это сложа, и мало кто его умеет готовить и т.д. и подходит это только для OTLP задач), но вы в замен предлагаете еще более страшные вещи.

                                                              • 0
                                                                Будете ли вы инджектить эту зависимость, без которой у объекта банально недостаточно всего что бы выполнить бизнес логику, в конструктор? Если нет — почему? Как вы определяете что должно инджектиться через конструктор а что нет?

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


                                                                1. Проверка на композицию. Также известна как проверка "has a" (содержит).
                                                                2. Текстологический анализ — берем описание юзкейса и ищем в нем существительные (кандидаты на объекты) и глаголы (возможные методы). Определяем смысловые отношения между ними.

                                                                Рассмотрим ваш пример с дверью. Проверка на "содержит". Получается: "Дверь" содержит "ключ". Очевидно, абсурд. Соответственно композиции и внедрения через конструктор не делаем.
                                                                Текстологический анализ. Описание может быть таким: Дверь должна открываться при помощи ключа. Чтобы открыть дверь нужен ключ. Очевидно, что для выполнения действия "открыть" с объектом "дверь" нужен объект "ключ". Отсюда можно видеть, что ключ должен быть аргументом метода.


                                                                Теперь пример, где нужно внедрение конструктора. Достаточно избитый. Пусть в расширенных требованиях есть такой текст — В системе существуют аккаунты пользователей. К каждому аккаунту можно добавлсять адреса и телефоны, при этом у одного из них может быть не более 5 адресов и 3 телефонов. Также должны существовать возможность изменить эти пределы через административный интерфейс.
                                                                Пожалуйста, есть инварианты, которые во всех примерах делаются с помощью внутренних констант класса. Надо эти константы сделать динамическими. Очевидно, должно существовать какое-то хранилище, реестр настроек.


                                                                Проверка "сождержит". Учетная запись содержит ограничения на количество информации. Звучит более правдоподобно.


                                                                Текстологический анализ. "Аккаунт" (каждый из них) "может быть" "не более пяти адресов". Обладателем ограничения является аккаунт. Соответственно у него должен быть доступ к информации об ограничениях. Реестр надо инжектить через конструктор. Клиентскому коду об ограничениях лучше не знать. В случае внедрения реестра через метод вместе с добавляемым адресов получается что-то вроде: "Эй аккаунт, добавь как вот этот телефон, но не забывай что у тебя их может быть не более трех". Аккаунт какой-то несамостоятельный получился. ничего не скрывает — уровень инкапсуляции низкий. Синглтоны не предлагать).


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

                                                                В общем-то да. Это и предлагаю. Объекты стейта можно интерфейсами обернуть, сделав зависимость клиентских классов от БД косвенной.


                                                                То есть в целом мы никуда от "внешнего" ресурса не ушли, мы лишь спрятали детали. Это позволяет нам выбирать хранилище под задачу а не подгонять задачу под хранилище.

                                                                Да, согласен, лучше прятать — но не таким сложным и неявным способом, как это делается с помощью DM.


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

                                                                Мэппер зависит не от сущности, т.е. ее интерфейса, а от деталей ее реализации, ее внутренней структуры. Что-то я у Фаулера в определении DataMapper ничего про обращение к внутренностям минуя интерфейс не припомню, кстати.


                                                                Опишите задачи где вам нужно делать массовый UPDATE и причем тут ORM?

                                                                Есть сайт глянцевого журнала. Публикации и реклама на сайте резмещаются в соответвии с графиком выхода номеров печатного издания. В 5 утра раз в неделю у публикации и баннеры, впервые добавленные для публикации (или после перерыва) должны быть опубликованы на сайте. До этого они набираются и корректируются. Причем пользователи, API и RSS читалки не должны видеть публикации по частям, а должны видеть их все и сразу с соответствующей рекламой. Публикации имеют разный приоритет — типа "тема номера", "персоналиии". Из поэтапная выгрузка может испортить картинку (Это бизнес-требование). Задержки до 10-15 минут при выгрузке допустимы. Основное требование — все или ничего. Задержки до 10-15 минут при выгрузке допустимы. Основное требование — все или ничего. Поэтому для выгрузки используется массовый UPDATE, который меняет атрибуты у тысяч записей сразу.


                                                                почему анемичный код?

                                                                Потому, что глядя на него. мы видим две строчки, которые по сути просто сеттеры, а предназначен этот код и делает этот код совсем другое. И причем не сам а с помощью мэппера, т.е. неявно. И чтобы он мог это делать ему еще и надо в этом анемичном и "независимом" коде всякие костыли подставлять в виде ArrayCollection.


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

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


                                                                За ссылку на доклад большое спасибо. Посмотрю. На остальное постараюсь ответить завтра.

                                                                • +1
                                                                   К каждому аккаунту можно добавлсять адреса и телефоны, при этом у одного из них может быть не более 5 адресов и 3 телефонов. Также должны существовать возможность изменить эти пределы через административный интерфейс.

                                                                  Странный кейс вы приводите. Зачем управлять пределами через админку? Вы CMS разрабатывает?
                                                                  Ну да ладно. Предположим надо.
                                                                  Тогда другой вопрос. Зачем вы завязываете сущность на целом реестре настроек?


                                                                  Проверка "сождержит". Учетная запись содержит ограничения на количество информации.

                                                                  Из этого утверждения не следует, что ограничения должны передаваться через реестр в конструктор сущности.


                                                                  Есть и другие способы решения задачи.


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