Как правильно работать с объектами выборки doctrine в Symfony?

Есть Symfony3, соответственно doctrine2 и несколько непонятных моментов.

Для теста был написан вот такой код:
$product = $this->getDoctrine()
            ->getRepository('AppBundle:Product')
            ->findById(1);

        $products = $this->getDoctrine()
            ->getRepository('AppBundle:Product')
            ->findAll();

        dump($product);

        foreach ($product->getImages() as $image) {
            dump($image);
        }


В качестве примера классический вариант с товаром.
<?php

namespace AppBundle\Entity;

use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\Mapping as ORM;

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

    /**
     * @var int
     *
     * @ORM\Column(name="status", type="integer")
     */
    private $status = 0;

    /**
     * @var int
     *
     * @ORM\Column(name="is_visible", type="integer")
     */
    private $is_visible = 1;

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

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

    /**
     * @ORM\OneToMany(targetEntity="Image", mappedBy="product")
     */
    protected $images;


    public function __construct()
    {
        $this->images = new ArrayCollection();
    }

    /**
     * Get id
     *
     * @return integer
     */
    public function getId()
    {
        return $this->id;
    }

    /**
     * Set status
     *
     * @param integer $status
     *
     * @return Product
     */
    public function setStatus($status)
    {
        $this->status = $status;

        return $this;
    }

    /**
     * Get status
     *
     * @return integer
     */
    public function getStatus()
    {
        return $this->status;
    }

    /**
     * Set isVisible
     *
     * @param integer $isVisible
     *
     * @return Product
     */
    public function setIsVisible($isVisible)
    {
        $this->is_visible = $isVisible;

        return $this;
    }

    /**
     * Get isVisible
     *
     * @return integer
     */
    public function getIsVisible()
    {
        return $this->is_visible;
    }

    /**
     * Set name
     *
     * @param string $name
     *
     * @return Product
     */
    public function setName($name)
    {
        $this->name = $name;

        return $this;
    }

    /**
     * Get name
     *
     * @return string
     */
    public function getName()
    {
        return $this->name;
    }

    /**
     * Set desc
     *
     * @param string $desc
     *
     * @return Product
     */
    public function setDesc($desc)
    {
        $this->desc = $desc;

        return $this;
    }

    /**
     * Get desc
     *
     * @return string
     */
    public function getDesc()
    {
        return $this->desc;
    }

    /**
     * Add image
     *
     * @param \AppBundle\Entity\Image $image
     *
     * @return Product
     */
    public function addImage(\AppBundle\Entity\Image $image)
    {
        $this->images[] = $image;

        return $this;
    }

    /**
     * Remove image
     *
     * @param \AppBundle\Entity\Image $image
     */
    public function removeImage(\AppBundle\Entity\Image $image)
    {
        $this->images->removeElement($image);
    }

    /**
     * Get images
     *
     * @return \Doctrine\Common\Collections\Collection
     */
    public function getImages()
    {
        return $this->images;
    }
}


<?php

namespace AppBundle\Entity;

use Doctrine\ORM\Mapping as ORM;

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

    /**
     * @var int
     *
     * @ORM\Column(name="product_id", type="integer")
     */
    private $product_id;

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

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

    /**
     * @ORM\ManyToOne(targetEntity="Product", inversedBy="images")
     * @ORM\JoinColumn(name="product_id", referencedColumnName="id")
     */
    protected $product;

    /**
     * Get id
     *
     * @return int
     */
    public function getId()
    {
        return $this->id;
    }

    /**
     * Set productId
     *
     * @param integer $productId
     *
     * @return Image
     */
    public function setProductId($productId)
    {
        $this->product_id = $productId;

        return $this;
    }

    /**
     * Get productId
     *
     * @return integer
     */
    public function getProductId()
    {
        return $this->product_id;
    }

    /**
     * Set previewUrl
     *
     * @param string $previewUrl
     *
     * @return Image
     */
    public function setPreviewUrl($previewUrl)
    {
        $this->preview_url = $previewUrl;

        return $this;
    }

    /**
     * Get previewUrl
     *
     * @return string
     */
    public function getPreviewUrl()
    {
        return $this->preview_url;
    }

    /**
     * Set reviewUrl
     *
     * @param string $reviewUrl
     *
     * @return Image
     */
    public function setReviewUrl($reviewUrl)
    {
        $this->review_url = $reviewUrl;

        return $this;
    }

    /**
     * Get reviewUrl
     *
     * @return string
     */
    public function getReviewUrl()
    {
        return $this->review_url;
    }
}


Все работает все хорошо. Но меня смущает 2 момента.

Во первых, это на столько здоровый объект реляции images, что отдебажить его можно только с помощью функции dump.
632f48bca94c421bb24f6f3729daacb0.png

Тоесть зачем такой жирный объект и как можно получить просто массив сущностей ?

Я зафоричил изображение, а каждое изображение имеет реляцию на товар, который в свою очередь имеет реляцию на изображения. Получается если у меня 1 товар у него 10 изображений, это будет очень много лишних данных в одном объекте, что очень сильно срет память.
35193003d0af40ff8a7b4a566517ffc8.png
Как этого можно избежать?
  • Вопрос задан
  • 3225 просмотров
Решения вопроса 3
Fesor
@Fesor
Full-stack developer (Symfony, Angular)
дополню ответ Юрий

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


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

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

/**
 * usage: $product->addImage($image);
 */
public function addImage(Image $image)
{
    $this->images->add($image);
}


А коллекция сама выполнит persist новой сущности. Таким образом количество репозиториев уменьшается до количества корней агрегатов сущностей. В вашем примере "корнем", то есть вершиной графа взаимоотношений объектов в контексте продуктов, является сам продукт. А потому репозиторий мы будем делать только для продуктов. Все остальное внутри оного разруливается либо при помощи коллекций.

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

К примеру "новички" в доктрине любят персисть сущность даже для обновления. Они путают `persist` и `save`. Так вот, если вы загрузили сущность из базы через доктрину, то сущность уже попадает в unit of work. И делать persist уже не нужно, этот метод только для того чтобы доктрина узнала о чем-то новом. А так она и так знает про эту сущность. В итоге вы можете просто что-то поменять и вызвать flush. То есть репозиторий - это тупо хранилище. Хранилище умеет хранить. Изменять то, что оно хранит оно не может.

Так же рекомендую на тему репозиториев почитать это:

www.whitewashing.de/2013/03/04/doctrine_repositori...

Ну и в целом.

https://www.youtube.com/watch?v=rzGeNYC3oz0 - доклад о том как готовить доктрину от авторов оной.

От себя лишь добавлю простые правила:

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

что очень сильно срет память.


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

Это логичное ограничение, дабы не возникало ситуаций что вы обновили что-то в одном экземпляре сущности и что-то в другом, и в базу попадут только часть изменений. За подробностями - читайте документацию к доктрине в отношении UnitOrWork и Identity Map.
Ответ написан
riky
@riky
Symfony / Laravel
1) ссылки с картинки на продукт не срут память, ибо объект продукта один и все картинки ссылаются на один объект. лишней памяти не тратится.

2) по поводу сложности. да оно кажется избыточным, но нужно чтобы было удобно потом работать с ними, добавлять новые связи, ленивая загрузка и тд.
просто сущности вы не получите, но если нужен только вывод то можно вместо getRepository / find сделать например createQuery / fetchArray. но это будет массив без вложенных сущностей, то есть картинок у продукта автоматически не будет.

3) вместо $em->getRepository('AppBundle:Product')->findById(1);
лучше $em->getRepository('AppBundle:Product')->find(1);
или даже $em->find('AppBundle:Product', 1);
Ответ написан
okwinza
@okwinza
PHP Developer
Если очень хочется именно массив сущностей:
public function getImages()
{
   return $this->images->toArray();
}

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

По остальному:
  1. Выворачивать ссылку на коллекцию в гетерах(делать return $this->images;) - неочень.
    Используйте return clone $this->images;
  2. setProductId() и подобные в вашем классе картинки бесполезен, у вас уже есть реляция.
  3. Может быть в тему в вашем случае - Extra Lazy Associations
Ответ написан
Пригласить эксперта
Ваш ответ на вопрос

Войдите, чтобы написать ответ

Войти через центр авторизации
Похожие вопросы