orm icon indicating copy to clipboard operation
orm copied to clipboard

[POSSIBLE BUG] setFetchMode not overriding fetch annotation

Open yesdevnull opened this issue 6 years ago • 5 comments
trafficstars

Bug Report

Q A
BC Break no
Version 2.6.4

Summary

I have the following entities:

  • IntervalGroup w/ oneToMany relation to IntervalType (fetch: EXTRA_LAZY)
  • IntervalType w/ oneToMany relation to Interval (fetch: EXTRA_LAZY)
  • Interval w/ oneToMany relation to Task (fetch: EAGER)

On the IntervalGroup repository I'm doing a DQL Query Builder query to load all IntervalGroups, and left join IntervalGroup.intervalTypes and intervalTypes.interval.

By default, the Task relation on Interval will be eager-loaded during hydration, however I don't want Tasks in this case. To counter that I'm using setFetchMode() on the Query object like so:

$query->setFetchMode(Interval::class, 'tasks', ClassMetadataInfo::FETCH_LAZY)

Which, as I understand, should prevent the ORM from fetching and hydrating those relations in that query.

Current behavior

Currently, the query hint for the fetch mode is not overriding the fetch mode on the annotation, so Doctrine loads the Interval.tasks relation.

createEntity in the UnitOfWork class is using the fetch value from the annotation, instead of the fetchMode query hint:

// Lines 2859 - 2862 of UnitOfWork.php
if ($assoc['fetch'] == ClassMetadata::FETCH_EAGER) {
    $this->loadCollection($pColl);
    $pColl->takeSnapshot();
}

(as seen here)

When I dump($assoc) I can see for the targetEntity = "Task" that $assoc['fetch'] equals 3 (ClassMetadata::FETCH_EAGER) when I specified in setFetchMode() that it should be 2 (ClassMetadata::FETCH_LAZY).

How to reproduce

class IntervalGroup
{
    /**
     * @ORM\OneToMany(targetEntity="IntervalType", mappedBy="intervalGroup", fetch="EXTRA_LAZY")
     */
    private $intervalTypes;

    public function __construct()
    {
        $this->intervalTypes = new ArrayCollection();
    }
}
class IntervalType
{
    /**
     * @ORM\ManyToOne(targetEntity="IntervalGroup", inversedBy="intervalTypes", fetch="EAGER")
     * @ORM\JoinColumn(name="interval_group_id", referencedColumnName="id", nullable=true)
     */
    private $intervalGroup;

    /**
     * @ORM\OneToMany(targetEntity="Interval", mappedBy="intervalType", fetch="EXTRA_LAZY")
     */
    private $intervals;

    public function __construct()
    {
        $this->intervals = new ArrayCollection();
    }
}
class Interval
{
    /**
     * @ORM\ManyToOne(targetEntity="IntervalType", inversedBy="intervals")
     * @ORM\JoinColumn(name="interval_type_id", referencedColumnName="id", nullable=false)
     */
    private $intervalType;

    /**
     * @ORM\ManyToMany(targetEntity="Task", fetch="EAGER")
     * @ORM\JoinTable(name="interval_mapping",
     *      joinColumns={@JoinColumn(name="interval_id", referencedColumnName="id")},
     *      inverseJoinColumns={@JoinColumn(name="task_id", referencedColumnName="task_id", unique=true)}
     * )
     */
    private $tasks;

    public function __construct()
    {
        $this->tasks = new ArrayCollection();
    }
}
class IntervalGroupRepository extends EntityRepository
{
    public function findAllAscendingByName()
    {
        $qb = $this->createQueryBuilder('ig');

        $qb
            ->select(['ig', 'it', 'i'])
            ->leftJoin('ig.intervalTypes', 'it')
            ->leftJoin('it.intervals', 'i');

        $query = $qb->getQuery();

        $query
            ->setFetchMode(Interval::class, 'tasks', ClassMetadataInfo::FETCH_LAZY);

        return $query->getResult();
    }
}

Expected behavior

The fetch mode override (as set in setFetchMode()) should be respected during hydration. UnitOfWork::createEntity's $assoc['fetch'] should check the $hints array to see if it is overridden, or use the default annotation value if not.

I believe overriding a fetch annotation should be possible to do, though perhaps I'm doing it the wrong way (in which case I'm happy to be pointed in the right direction!).

yesdevnull avatar Oct 09 '19 05:10 yesdevnull

The documentation mentions the behaviour of oneToMany relation when the fetch mode is set to to eager, but not how it behaves when set to lazy.

https://www.doctrine-project.org/projects/doctrine-orm/en/2.6/reference/dql-doctrine-query-language.html#temporarily-change-fetch-mode-in-dql

Thank you for reporting this.

SenseException avatar Oct 14 '19 20:10 SenseException

Thanks for the link, @SenseException, I can't believe I missed that part of the documentation :sweat:

I've been able to (partially) get around this by using JMSSerializer groups to avoid unnecessary resolution and hydration of relationships and properties.

yesdevnull avatar Oct 15 '19 00:10 yesdevnull

Hi Has there been any progress on this issue? I would like to use this feature to set a lazy association to extra lazy hydration.

up

xepozz avatar Oct 31 '22 22:10 xepozz

The bug still exists even in ORM 3. The hints array with fetchMode set by method $query->setFetchMode() is not read at all. I just debugged it. The code in https://github.com/doctrine/orm/blob/3.1.0/src/Query/SqlWalker.php#L909 is using fetchMode value from ClassMetadata definition, ignoring hints value.

FetchMode from ClassMetadata must be overriden from hints array here: https://github.com/doctrine/orm/blob/3.1.0/src/Query/Parser.php#L1613

Housik avatar Mar 19 '24 22:03 Housik