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: