2

Problem definition

So given this part of Doctrine documentation (and many other articles about bidirectional association and persistence problems), I have a one-to-many association that is unidirectional. It is between a Club and it's Staff members. From domain logic, the Club should own the Staff. For example, if I used document storage, Staff would be a member of Club. In 99.9% of domain cases, I have a club and want a list of its staffers.

However, I am having trouble implementing this idea in Doctrine. Because in Doctrine the ONE-TO-MANY relationship has only an inverse side. So even conceptually the following Entity definition is wrong, but I do not know any better.

Entity code

<?php

class Club
{
    /**
     * @ORM\Id
     * @ORM\Column(type="uuid", unique=true)
     * @ORM\GeneratedValue(strategy="CUSTOM")
     * @ORM\CustomIdGenerator(class="Ramsey\Uuid\Doctrine\UuidGenerator")
     */
    public ?UuidInterface $id;

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

    /**
     * @ORM\OneToMany(targetEntity="Staff", mappedBy="club")
     */
    public Collection $staff;

    public function __construct(string $name)
    {
        $this->id       = Uuid::uuid4();
        $this->name     = $name;
        $this->staff    = new ArrayCollection();
    }

    public function addStaff(Staff $staff): void
    {
        $this->staff->add($staff);
    }

}

class Staff
{
    public const ROLE_OWNER = 'owner';

    /**
     * @ORM\Id
     * @ORM\Column(type="uuid", unique=true)
     * @ORM\GeneratedValue(strategy="CUSTOM")
     * @ORM\CustomIdGenerator(class="Ramsey\Uuid\Doctrine\UuidGenerator")
     */
    public ?UuidInterface $id;

    /**
     * @ORM\Column(type="string", name="user_id")
     */
    public string $userId;

    /**
     * @ORM\Column(type="string", name="role")
     */
    public string $role;

    public function __construct(string $userId, string $role)
    {
        $this->id     = Uuid::uuid4();
        $this->userId = $userId;
        $this->role   = $role;
    }
}

But here comes the real problem. I have ONE use case, where I want to find clubs for the appropriate staffer. It is a (ONE-)MANY-ONE problem, with the caveat that the first one is not another Doctrine entity, but a userId for the Staff entity.

So I tried several approaches to getting Club entities given userId:

Failed attempts

<?php
   // 1. Naive solution, just try to use the `ObjectRepository`
   // Will cause: You cannot search for the association field 'Club#staff', because it is the inverse side of an association. Find methods only work on owning side associations.
$staffers = $this->em->getRepository(Staff::class)->findBy(['userId' => $ownerId]);
$clubs = $this->em->getRepository(Club::class)->findBy(['staff' => $staffers]);

   // 2. Use `QueryBuilder` naively
   // Will cause: [Semantical Error] line 0, col 131 near 'staff = s.id': Error: Invalid PathExpression. StateFieldPathExpression or SingleValuedAssociationField expected
         $qb = $this->em->createQueryBuilder();
         $query = $qb->select('c')
         ->from(Club::class, 'c')
         ->join(Staff::class, 's', Join::WITH, 'c.staff = s.id')
         ->where('s.userId = :id')
         ->setParameter('id', $ownerId)
         ->getQuery();

    // 3. Use `QueryBuilder` with knowledge if actual DB columns
    // Will cause: [Semantical Error] line 0, col 138 near 'club_id WHERE': Error: Class Staff has no field or association named club_id
         $qb = $this->em->createQueryBuilder();
         $query = $qb->select('c')
         ->from(Club::class, 'c')
         ->join(Staff::class, 's', Join::WITH, 'c.id = s.club_id')
         ->where('s.userId = :id')
         ->setParameter('id', $ownerId)
         ->getQuery();

    // 4. Create query directly
    // Will cause: [Semantical Error] line 0, col 125 near 'staff = s.id': Error: Invalid PathExpression. StateFieldPathExpression or SingleValuedAssociationField expected
                $query = $this->em->createQuery('SELECT c FROM ' . Club::class . ' c JOIN ' . Staff::class . ' s WITH c.staff = s.id WHERE s.userId = :id')->setParameter('id', $ownerId);

WHat am I looking for?

  • a unidirectional association between Club and Staff, so that I don't have to be careful about persistence, performance, inconsistencies, etc. Just all the problems of bidirectional associations.
  • either:
    • a possible rework of the entities/associations
    • a way to retrieve Club entities, given staff.userId
Community
  • 1
  • 1
  • Well, if you absolutely must avoid bidirectional, how about a [native query](https://www.doctrine-project.org/projects/doctrine-orm/en/2.7/reference/native-sql.html)? – El_Vanja Mar 25 '20 at 00:39
  • @El_Vanja I am not experienced, but from looking at the link doesn't that create quite the dependency on the table structure? I am in the rapid development phase of the project, so the entities and the underlying table structure changes quite often. – Patrick Kusebauch Mar 25 '20 at 00:43
  • Sorry, I missed a part of your question and jumped ahead to write an answer (now deleted). For your case, why not define it as bidirectional? Even if it's one use case, I don't think you'd create a lot of overhead. I'd surely tend to keep the code simpler than writing lines and lines of dql or native queries. – Kevin Kopf Mar 25 '20 at 00:58
  • @AlexKarshin If I have 100 use cases in one way and 1 use case the other way, it does not make much sense to me. It is about a trade-off. Would you rather have one DQL/Native query or would you rather have a bi-directional association with everything it entails. Right now I would rather have the DQL. – Patrick Kusebauch Mar 25 '20 at 01:01

1 Answers1

0

A solution that is working, but I am unable to provide an explanation as to why this one works and others do not.

<?php
$this->em->createQueryBuilder()
            ->select('c')
            ->from(Club::class, 'c')
            ->innerJoin(Staff::class, 's')
            ->where('s.userId = :owner')
            ->setParameter('owner', $ownerId)
            ->getQuery()
            ->getResult();