Search code examples
doctrine-mongodb

How do I obtain all documents referenced by a single document?


If I have a document Shop that has many Activities defined as ReferenceMany, is there a way I can directly query for the list of Activities for a Shop without hydrating a Shop instance?

For example:

{
    "_id": fd390j09afj09dfj,
    "activities": [
        ...
    ]
}

All I want is to be able to say "get me the array activities where _id is fd390j09afj09dfj, and hydrate them as Activity instances.


Here's the first solution I came up with:

    /**
     * Gets all activities configured for a shop.
     *
     * @param string $shopId
     * @return \BikeShed\Domain\Activity[]|\Doctrine\Common\Collections\ArrayCollection
     */
    public function findByShopId($shopId) {

        /** @var \BikeShed\Domain\Repository\Shop $shopRepository */
        $shopRepository = $this->dm->getRepository('BikeShed\Domain\Shop');

        $shop = $shopRepository->findOneById($shopId);

        return $shop->getActivities();

    }

It's simply fetching the Shop and then getting all the Activities via the defined relation.


Here's a working example of how you would implement jmikola's last suggestion:

    /**
     * @param string $shopId
     * @return ActivityModel[]
     */
    public function findByShopId($shopId) {

        $partialShopData = $this->dm->getRepository('BikeShed\Domain\Shop')->createQueryBuilder()
            ->hydrate(false)
            ->field('activities')
            ->getQuery()
            ->getSingleResult()
        ;

        $activityIds = [];
        if(!empty($partialShopData['activities']))
            foreach($partialShopData['activities'] as $activity)
                if(!empty($activity['$id']))
                    $activityIds[] = $activity['$id'];

        return $this->createQueryBuilder()
            ->field('id')
            ->in($activityIds)
            ->getQuery()
            ->toArray()
        ;

    }

Solution

  • You cannot directly query the Shop collection or (or ODM repository) and receive Activity instances; however, you can use the Query Builder API to specify a projection with select('activities'). The executed query will still return Shop instances, but the activities field should be the only thing hydrated (as a PersistentCollection of Activity instances). In this case, you shouldn't modify any of the non-hydrated Shop fields, as ODM will detect any non-null value as a change.

    It should be trivial to add a convenience method on ShopRepository that issues the above query with its select() and returns the collection (or an array) of Activity documents instead of the Shop. Keeping the Shop inaccessible should also protect you from inadvertently modifying other non-hydrated fields within it.

    The down-side with this method is that the Activities will be proxy objects and lazily loaded. You can mitigate this with reference priming. With priming, you'll end up doing two queries (one for the Shop and one for all referenced Activity documents).


    Regarding your follow-up question about putting this method on the Activity repository, you do have another option. Firstly, I agree that ActivityRepository::findByShopId() is preferable to calling a method on ShopRepository that returns Activity objects.

    Each repository has a reference to the document manager, which you can use to access other repositories via the getRepository() method. An ActivityRepository::findByShopId() could do the following:

    • Access the Shop repository through the document manager
    • Query for the Shop by its ID, projecting only the activities field and disabling hydration completely
    • Collect the identifiers from the activities array. Depending on whether the Activity references are simple or not, the elements in that array may be the raw _id values or DBRef objects.
    • Execute a query for all Activity objects (easy, since we're already in that repository) where the ID is $in the array of identifiers