Загрузка связанных сущностей объекта

Cx2

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

сервис1->маппер1->сущность1

НО, сущность1 может содержать в себе сущность2, 3, 4 и т.д.
Как правильно загружать зависимые сущности?

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

Какие еще стандартные подходы используются для таких целей.

Если можно, приведите пример кода.
 

Вурдалак

Продвинутый новичок
Если ты говоришь про lazy loading, то можно посмотреть как это реализовано, например, в Doctrine.
Там каждая возвращаемая из mapper'а сущность представляет из себя на самом деле сложный объект (прокси), который наследуется от сущности и в неявном виде содержит все нужные зависимости.

Технически, в той же Doctrine mapper один: так называемый entity manager, он универсальный и работает с любыми сущностями, которые ему известны, поэтому как таковой проблемы «какой mapper в какой нужно заинжектить» там нет.
 

Cx2

Новичок
Да, lazy loding, связанные сущности должны подгружаться только там где это нужно. А какая Doctrine - 1 или 2? Насколько я знаю Doctrine это ORM, а я изначально про мапперы думал... Можно какой-нибудь примитивный пример или более примитивный проект? Я изначально рассчитывал, что сущность сама изменения не записывает, модификациями занимается сервис через маппер, но может я и заблуждаюсь, изучу все варианты.
 

fixxxer

К.О.
Партнер клуба
А какая Doctrine - 1 или 2?
2, разумеется, первую-то чего обсуждать? Можно еще php3 обсудить с таким же успехом.

Doctrine это ORM, а я изначально про мапперы думал
А что, по-твоему, означает буква "M" в аббревиатуре "ORM"? :)
Doctrine 2 - это ORM, реализующая паттерн Data Mapper. (Вот в первой был Active Record, но даже в AR есть маппер - просто он "внутри" сущности, объединен с ней).

Если ты собрался писать свой DataMapper, лучше забудь про это, и бери Doctrine 2, то, что ты хочешь, она умеет. Собственный DM - это гораздо более сложная задача, чем тебе кажется.
 
Последнее редактирование:
  • Like
Реакции: Cx2

Cx2

Новичок
Запутался. Перепутал ORM и Active Record. Привык к PDO, но в данном моем случае Doctrine похоже удобнее будет, были сомнения в быстродействии Doctrine, прочитаю документацию, попробую его в деле. Пока не понятно, как избежать дублирования кода, если в разных местах получать через entity manager одну и ту же сущность для разных нужд.
 

Cx2

Новичок
Попробовал доктрину, нет это точно не для меня! Очень тяжелая, например создание одной сущности (id, name) из таблицы с одной записью занимает времени в 2 раза больше чем весь фреймворк Slim + шаблонизатор Twig выдают hello world из шаблона. На мой взгляд, если не нужны ее особенности, то она больше проблем создаст, чем удобств.
Мне нужно что-то проще, возможно даже велосипед :)
Скажите вот такой код не противоречит хорошим практикам пхп программирования?
Смущает, что маппер сам себя инжектит в load'e()
Мне такого функционала в аккурат.

Код:
class Mapper
{
    ...
    public function load($entity_class, $id) {
        $stmt = $this->pdo->query('SELECT ...');
        $stmt->setFetchMode(PDO::FETCH_CLASS, $entity_class, [$this]);
        return $stmt->fetch();
    }
    ...
}

class Entity
{
    protected $mapper = null;
    public function __construct(Mapper $mapper) {
        $this->mapper = $mapper;
    }
}

class Post extends Entity
{
    ...
    getAuthor() {
        return $this->mapper->load('User', $this->author_id);
    }
    ...
}

$mapper = new Mapper(new Pdo('...'));
$post = $mapper->load('Post', 1);
$author = $post->getAuthor();
 
Последнее редактирование:

fixxxer

К.О.
Партнер клуба
Так вариация на тему Active Record получится. :)

С Data Mapper сущность должна быть простым PHP-классом, который ничего не знает и не хочет знать о проблемах персистенции. Решается это через Reflection. Если не хочется с ним связываться, в качестве компромисса можно рассмотреть вариант со специальными методами типа setAttributes(array):void и exportAttributes():array. Но банальная серализация-гидрация одной сущности без связей - это только малая часть работы. Тебе надо будет еще и связи все обрабатывать (в обе стороны), и для lazy load проксики реализовать... Я ж говорю, это непростая задача.

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

WMix

герр M:)ller
Партнер клуба
да, и правда, зачем тебе ссылка на mapper в Entity?

@Cx2, $entity->getAuthor() попробуй иначе представить $mapper->getPost( $entity )->getAuthor()
в смысле что не entity отвечает за доставку а mapper
Решается это через Reflection.
он заполняет иначе через $stmt->setFetchMode(PDO::FETCH_CLASS, $entity_class, [$this]);
 

fixxxer

К.О.
Партнер клуба
он заполняет иначе через $stmt->setFetchMode(PDO::FETCH_CLASS, $entity_class, [$this]);
Это работает, только пока маппинг тупой как дрова, как только появится преобразования в маппере, появится Reflection (вообще, PDO fetch_class внутри для создания инстанса делает примерно то же, что ReflectionClass::newInstanceWithoutConstructor). А персистить приватные свойства без reflection, сохраняя "чистоту" сущностей, не получится. Конечно, можно пойти на компромисс и обязать сущности реализовывать какой-нибудь метод getAttributes(), как это было в ранних версиях analogue.
 

Cx2

Новичок
@fixxxer, analogue orm тоже получается с AR уклоном? В документации есть такой пример:
Код:
class User extends Entity
{
    public function __construct($email, Role $role)
    {
        $this->email = $email;
        $this->role = $role;
        $this->permissions = new EntityCollection;
    }
    public function addPermission(Permission $permission)
    {
        $this->permissions->push($permission);
    }
    /**
     * Return all User's permissions
     *
     * @return Collection
     */
    public function getPermissions()
    {
        $rolePermissions = $this->role->permissions;
        return $this->permissions->merge($rolePermissions);
    }
}
работа с ним кажется вполне понятной, проще чем доктрина.
Не хватает счетчика (select count(*) ...) и хотелось бы с PDO и без магии.

@WMix, не очень понял как это работает:
Код:
$mapper->getPost( $entity )->getAuthor()
В sql что-то типа Joina должно добавиться?
 

fixxxer

К.О.
Партнер клуба
analogue orm тоже получается с AR уклоном?
Не, там мапперы отдельно, в сущности просто есть методы getAttributes() и setAttributes().
Причем это старая версия, документация там не очень своевременно обновляется, в новой можно вообще работать с plain php objects, примерно так:
PHP:
class User { private $id; private $email; ... }
class UserMap extends EntityMap
{
    protected $arrayName = null; // use reflection
    protected $properties = ['id', 'email'];
}
Не хватает счетчика (select count(*) ...) и хотелось бы с PDO и без магии.
Analogue - это штука для write models, для read models можно хоть напрямую через PDO.

Впрочем, если очень хочется, конечно, можно написать и что-то вроде
PHP:
return $mapper->count()
- там Laravel-овский query builder внутри, все, что он умеет, умеет и $mapper->query().
 
Последнее редактирование:

Cx2

Новичок
А можно ли в analogue сделать загрузку коллекции связанных сущностей, если в БД они хранятся так:

Код:
products_table
(id, name, stores_set)
(1, 'планшет', '1,2')
(2, 'ПК', '1,3')

stores_table
(id, name)
(1, 'магазин 1')
(2, 'магазин 2')
загрузить нужно магазины
$product = $mapper->getProduct(1);
$stories = $product->getStories();
 
Последнее редактирование:

fixxxer

К.О.
Партнер клуба
Эээ, в varchar через запятую? Ох.

Лучше хранить в нормализованом виде, через pivot table - такое умеет из коробки.
 

Cx2

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

Andkorol

Новичок
Ага, в varchar через запятую. В некоторых случаях сводная таблица имеет тенденцию к постоянному разрастанию, это создает дополнительную нагрузку на диск, а зачем она нужна, если в этих некоторых случаях можно без нее. И к тому же в самом MySQL есть операторы для работы с такими данными. У меня есть потребность в реализации таких связей, когда идентификаторы через запятую.
Феерично... :D
 

Yoskaldyr

"Спамер"
Партнер клуба
Не, реально уже нужен список таких чудных цитат
 

fixxxer

К.О.
Партнер клуба
. И к тому же в самом MySQL есть операторы для работы с такими данными
создает дополнительную нагрузку на диск
Вот именно что работая с такими операторами ты создаешь дополнительную нагрузку и на диск, и на CPU: если для pivot table обычно достаточно обращения к индексу вообще не трогая данные, то с where или join on по comma-separated через операторы ты гарантированно получишь fullscan, а, возможно, еще и using temporary впридачу. Какой-то (вряд ли измеримый) профит можно получить, только если таблицы очень маленькие, но тогда вообще нагрузке на диск беспокоиться нечего - все целиком закэшируется в памяти.
 
Последнее редактирование:
  • Like
Реакции: Cx2

Cx2

Новичок
Век живи, век учись, а я думал - это нормальная практика, когда через запятую :)
Буду иметь в виду, что так больше делать не стоит и никому больше не скажу, что когда-то так делал :)
Я правильно понял, pivot table - это сводная таблица, элементы строки которой содержат ключи связанных объектов?

Вернемся к ORM. Я смотрел на гитхабе, кто как что делает, наткнулся https://www.opulencephp.com/docs/1.1/orm-data-mappers
Это фреймворк и там сразу все есть + хорошее быстродействие (это для меня ключевой фактор).
Но в документации для Relationships очень мало инфы, много примеров в виде классов, но нет пример как с ними работать.
Там есть такой пример (всего один):
PHP:
use MyApp\WordPress\Orm\AuthorRepo;
use MyApp\WordPress\Post;

class PostDataMapper extends SqlDataMapper
{
   private $authorRepo;

   public function __construct(
       IConnection $readConnection,
       IConnection $writeConnection,
       AuthorRepo $authorRepo
   ) {
       parent::__construct($readConnection, $writeConnection);

       $this->authorRepo = $authorRepo;
   }

   protected function loadEntity(array $hash)
   {
      // Grab the author
        $author = $this->authorRepo->getById($hash['author_id']);

       return new Post(
           (int)$hash['id'],
           $hash['title'],
           $author
       );
   }
}
В loadEntity в принципе можно как угодно подготовить данные перед созданием объекта. Но этот метод - protected, он должен запускаться во всяких getById() и ему подобных геттерах? Кто такие $readConnection и $writeConnection?
 
Последнее редактирование:

WMix

герр M:)ller
Партнер клуба
не очень понял как это работает:
маппер помимо прочего имеет все связки (Realtion) с другими табличами
PHP:
class Mapper{
  protected $table;
  protected $identity;
  protected $entityClass;
  protected $relations = [];
  // ...
}
Realtion помимо прочего (там тип 1:1 / n:1/ n:m, правило связки on a=b имя в данном случае "post") содержит маппер на посты
https://github.com/analogueorm/analogue/tree/5.5/src/Relationships вот как в аналоге

все чего тебе не хватает самого ключа, а он у тебя в entity.
$mapper->getPost( $entity )->getAuthor()
это выдуманная строка, в ней только подсказка что это задачи маппера
 
Сверху