Happyr/Doctrine-Specification

Globally unique alias

KDederichs opened this issue · 5 comments

Hey,

I've got a question regarding aliases, is there a possibility to get globally unique aliases?
I'm trying to prevent that multiple specs use the same alias which probably would cause some sort of interference.
I tried generating an alias with uniqid but that seems to be called multiple times somehow and so the alias on the join and where condition is different.

@KDederichs I have a similar issue… did you ever figure out a good solution?

I'm trying to prevent that multiple specs use the same alias which probably would cause some sort of interference.

@KDederichs Why are you trying to solve this problem through global unique aliases? What prevents you from manually manipulating aliases and ensuring their uniqueness on their own?

In general nothing, but it would be extra work if you have a big project with a lot of nested Specs that multiple people work on.
Or am I missing something that prevents that in general?

I am currently working on a large project and there is a big nesting of specifications and we do not have any particular problems.

For example we have a Questionnaire with some relations:

  • Questionnaire
    • Contestant
      • Contest
      • User

And for example, i want to get a slice of published Questionnaires.

$questionnaires = $this->rep->match(Spec::andX(
    new PublishedQuestionnaires('c', 'ct', 'u'),
    new Slice($slice_size, $slice_number),
    Spec::orderBy('join_at', 'DESC', 'ct'),
));
PublishedQuestionnaires
class PublishedQuestionnaires extends BaseSpecification
{
    private $contest_alias = '';

    private $contestant_alias = '';

    private $user_alias = '';

    public function __construct(
        string $contest_alias = 'c',
        string $contestant_alias = 'ct',
        string $user_alias = 'u',
        ?string $dql_alias = null
    ) {
        $this->contest_alias = $contest_alias;
        $this->contestant_alias = $contestant_alias;
        $this->user_alias = $user_alias;

        parent::__construct($dql_alias);
    }

    public function getSpec()
    {
        return Spec::andX(
            new AddEntityToResult($this->user_alias),
            new AddEntityToResult($this->contestant_alias),
            Spec::innerJoin('contestant', $this->contestant_alias),
            new ContestantPublished($this->contest_alias, $this->user_alias, $this->contestant_alias),
        );
    }
}
ContestantPublished
class ContestantPublished extends BaseSpecification
{
    private $contest_alias = '';

    private $user_alias = '';

    public function __construct(string $contest_alias = 'c', string $user_alias = 'u', ?string $dql_alias = null)
    {
        $this->contest_alias = $contest_alias;
        $this->user_alias = $user_alias;
        parent::__construct($dql_alias);
    }

    protected function getSpec()
    {
        return Spec::andX(
            new JoinedContestant($this->contest_alias, $this->user_alias),
            new ContestantApproved($this->contest_alias),
        );
    }
}
JoinedContestant
class JoinedContestant extends BaseSpecification
{
    private $contest_alias = '';

    private $user_alias = '';

    public function __construct(string $contest_alias = 'c', string $user_alias = 'u', ?string $dql_alias = null)
    {
        $this->contest_alias = $contest_alias;
        $this->user_alias = $user_alias;
        parent::__construct($dql_alias);
    }

    protected function getSpec()
    {
        return Spec::andX(
            Spec::innerJoin('user', $this->user_alias),
            new UserPublished($this->user_alias),
            Spec::innerJoin('contest', $this->contest_alias),
            new ContestPublished($this->contest_alias),
        );
    }
}

So, as you can see, i have no problems using aliases and nested specifications even with a large nesting level.

If you use global unique aliases, then you will encounter the problem of not being able to reuse the alias in other specifications and will have to do JOIN each time, which will complicate the generated queries and decrease performance.

Example usage a global unique aliases

$contestant_alias = uniqid();

$questionnaires = $this->rep->match(Spec::andX(
    new PublishedQuestionnaires(),
    new Slice($slice_size, $slice_number),
    // we need JOIN for apply order
    Spec::innerJoin('contestant', $contestant_alias),
    Spec::orderBy('join_at', 'DESC', $contestant_alias),
));
PublishedQuestionnaires
class PublishedQuestionnaires extends BaseSpecification
{
    public function getSpec()
    {
        $contestant_alias = uniqid();
        $user_alias = uniqid();

        return Spec::andX(
            // we already have this JOIN, but we can't access it here. So we repeat it
            Spec::innerJoin('contestant', $contestant_alias),
            new AddEntityToResult($this->contestant_alias),
            Spec::innerJoin('user', $user_alias),
            new AddEntityToResult($this->user_alias),
            new ContestantPublished($contestant_alias),
        );
    }
}
ContestantPublished
class ContestantPublished extends BaseSpecification
{
    protected function getSpec()
    {
        return Spec::andX(
            new JoinedContestant(),
            new ContestantApproved(),
        );
    }
}
JoinedContestant
class JoinedContestant extends BaseSpecification
{
    protected function getSpec()
    {
        $user_alias = uniqid();
        $contest_alias = uniqid();

        return Spec::andX(
            // we need the user alias for apply the user specification, but we cannot access here
            // the previously made join. So we repeat it again
            Spec::innerJoin('user', $user_alias),
            new UserPublished($user_alias),
            Spec::innerJoin('contest', $contest_alias),
            new ContestPublished($contest_alias),
        );
    }
}

As you can see, the specifications have become simpler. No need to forward aliases in arguments. But the same joins are repeated several times and we cannot reuse them because they are defined at different levels of the specification tree.

Perhaps this problem could be solved through the conditions of the join, but as you may have noticed, the join alias is still needed at different levels and in this example it will not solve the problem.

Currently it is not possible to make a global unique aliases. Theoretically, we can solve this problem by specifying the full path to the field relative to the current alias and make the join under the hood with the assignment of a unique alias.

Spec::orderBy('%s.contestant.join_at', 'DESC')

But this requires changing the API of the project and still will not allow the use of aliases outside the specification mechanism.

$last_joined_user_name = $rep
    ->getQueryBuilder(Spec::andX(
        new PublishedQuestionnaires(),
        Spec::orderBy('%s.contestant.join_at', 'DESC'),
        Spec::limit(1),
    ), 'q')
    // not work
    // ->select('q.contestant.user.name')
    // we must use the JOIN again
    ->innerJoin('q.contestant', 'ct')
    ->innerJoin('ct.user', 'u')
    ->select('u.name')
    ->getQuery()
    ->getSingleScalarResult()
;

Currently it is not possible to make a global unique aliases. Theoretically, we can solve this problem by specifying the full path to the field relative to the current alias and make the join under the hood with the assignment of a unique alias.

A similar solution is implemented in PR #273 and available in release 2.0.
Now you do not need to manage all the DQL aliases at all levels of the specifications. Now it is enough to indicate the context of execution of a specific specification.

$questionnaires = $this->rep->match(Spec::andX(
    new PublishedQuestionnaires(),
    new Slice($slice_size, $slice_number),
    Spec::orderBy('contestant.join_at', 'DESC'),
));
PublishedQuestionnaires
final class PublishedQuestionnaires extends BaseSpecification
{
    protected function getSpec()
    {
        return Spec::andX(
            Spec::addSelect(
                Spec::selectEntity('contestant.user'),
                Spec::selectEntity('contestant'),
            ),
            new ContestantPublished('contestant'),
        );
    }
}
ContestantPublished
final class ContestantPublished extends BaseSpecification
{
    protected function getSpec()
    {
        return Spec::andX(
            new JoinedContestant(),
            new ContestantApproved(),
        );
    }
}
JoinedContestant
final class JoinedContestant extends BaseSpecification
{
    protected function getSpec()
    {
        return Spec::andX(
            new UserPublished('user'),
            new ContestPublished('contest'),
        );
    }
}
ContestPublished
final class ContestPublished extends BaseSpecification
{
    protected function getSpec()
    {
        return Spec::eq('enabled', true);
    }
}

As you can see, the code has become much simpler and more concise.