Doctrine добавляет дополнительные запросы во время гидратации, вызывая n + 1 проблему с «нормальными» отношениями один к одному и с саморегулированием. - PullRequest
1 голос
/ 08 мая 2020

News связаны друг с другом с использованием подхода «один ко многим» со ссылками на себя (одна новость является родительской и может иметь много потомков). Более того, каждый News имеет нормальные (не ссылающиеся на себя) отношения один к одному с Event и Gallery. Когда я запускаю простой DQL:

SELECT n FROM App\Entity\News n WHERE n.parent = :id

, а затем обновляю результаты методом getResults со значением по умолчанию HYDRATION_OBJECT, дополнительные запросы выполняются где-то внутри метода getResults.

SELECT t0.* FROM event t0 WHERE t0.news_id = 2 AND ((t0.deleted_at IS NULL));
SELECT t0.* FROM gallery t0 WHERE t0.news_id = 2 AND ((t0.deleted_at IS NULL));
SELECT t0.* FROM event t0 WHERE t0.news_id = 1 AND ((t0.deleted_at IS NULL));
SELECT t0.* FROM gallery t0 WHERE t0.news_id = 1 AND ((t0.deleted_at IS NULL));

Где news_id = 1 и news_id = 2 - дочерние элементы новостей, выбранных первым запросом.

News также имеет не ссылающиеся на себя отношения «один ко многим» (я их здесь проигнорировал), но гидратация не вызывает дополнительных запросов о них. Проблема возникает только тогда, когда в операторе where задействовано отношение parent.

Как воспроизвести

// news Entity
/**
 * @ORM\Entity(repositoryClass="App\Repository\NewsRepository")
 * @ORM\Table(uniqueConstraints={@UniqueConstraint(name="news_slug_deleted", columns={"slug","deleted_at"})})
 */
class News {
    use SoftDeleteableEntity;

    /**
     * @ORM\Id()
     * @ORM\GeneratedValue()
     * @ORM\Column(type="integer")
     */
    private $id;
    /**
     * @ORM\Column(type="string", length=255)
     */
    private $title;

    /**
     * @Gedmo\Slug(fields={"title"})
     * @ORM\Column(length=128)
     */
    private $slug;
/**
     * @ORM\OneToOne(targetEntity="App\Entity\Gallery", mappedBy="news", cascade={"persist", "remove"}, orphanRemoval=true)
     *
     * @var Gallery
     */
    private $gallery;

    /**
     * @ORM\OneToOne(targetEntity="App\Entity\Event", mappedBy="news", cascade={"persist", "remove"}, orphanRemoval=true)
     *
     * @var Event
     */
    private $event;
    /**
     * One News has Many News.
     * @ORM\OneToMany(targetEntity="News", mappedBy="parent")
     */
    private $children;

    /**
     * Many News have One News.
     * @ORM\ManyToOne(targetEntity="News", inversedBy="children")
     * @ORM\JoinColumn(name="parent_id", referencedColumnName="id", nullable=true)
     */
    private $parent;
}
/**
 * @ORM\Entity(repositoryClass="App\Repository\EventRepository")
 * @Gedmo\SoftDeleteable()
 */
class Event
{
    use SoftDeleteableEntity;

    /**
     * @ORM\Id()
     * @ORM\GeneratedValue()
     * @ORM\Column(type="integer")
     */
    private $id;
    /**
     * @ORM\OneToOne(targetEntity="App\Entity\News", inversedBy="event", cascade={"persist"})
     * @ORM\JoinColumn(nullable=false)
     */
    private $news;

Gallery сущность похожа на Event, поэтому я проигнорировал ее здесь.

// News controller

public function index(NewsRepository $newsRepository, $slug)
{
        $news = $newsRepository->findOneBy(['slug' => $slug]);
        $newsRepository->getConnectedNews($news->getId());
}
// news repository

class NewsRepository extends ServiceEntityRepository
{
    public function __construct(ManagerRegistry $registry)
    {
        parent::__construct($registry, News::class);
    }
    public function getConnectedNews($newsId) {
        $query = $this->createQueryBuilder('n')->andWhere('n.parent = :id')->setParameter('id', $newsId);
        return $query->getQuery()->getResult(AbstractQuery::HYDRATE_OBJECT);
    }
}

Гидратация news, у которого есть 20 дочерних элементов, заканчивается: 20 * 2 + 1 (n * r + 1) запросов, где:

  • (n) 20 - это количество детей
  • (r) 2 - это количество отношений один к одному
  • 1 - это базовый c запрос

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

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

1 Ответ

1 голос
/ 08 мая 2020

У вас есть пара обратных отношений OneToOne в этом объекте.

Обратные отношения OneToOne не могут быть загружены лениво с помощью Doctrine и могут легко стать проблемой производительности.

Если вы действительно необходимо, чтобы эти отношения отображались на стороне инверсии (и не только на стороне-владельце), убедитесь, что вы сделали соответствующие соединения явно, или отметьте эти связи как FETCH=EAGER, чтобы Doctrine выполняет соединения за вас.

Например, запрос, который позволил бы избежать ужасной проблемы «n + 1», будет выглядеть так:

SELECT n, g, e
    FROM App\Entity\News n
    LEFT JOIN n.gallery g
    LEFT JOIN n.event e
WHERE n.parent = :id
...