Skip to content

Deprecate getting query parts from QueryBuilder #6179

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Oct 10, 2023

Conversation

morozov
Copy link
Member

@morozov morozov commented Oct 10, 2023

Q A
Type deprecation

The methods were removed in #3836.

@morozov morozov added this to the 3.8.0 milestone Oct 10, 2023
@morozov morozov force-pushed the deprecate-query-parts branch from d9b2ab4 to bc57ca0 Compare October 10, 2023 09:20
@morozov morozov requested a review from derrabus October 10, 2023 09:51
@morozov
Copy link
Member Author

morozov commented Oct 10, 2023

The DB2 builds are failing due to a 500: Internal Server Error from an IBM server. This is a known regularly happening issue.

@morozov morozov force-pushed the deprecate-query-parts branch from bc57ca0 to 2ec9b03 Compare October 10, 2023 10:01
@janklan
Copy link

janklan commented May 2, 2024

I have a use case where one of the class methods accepts a QueryBuilder instance that's meant to supply a query producing a list of IDs used in an article.tag_id IN ( subquery ) condition.

My application has various situation where the list of "tags in scope" is generated using vastly different logic, and I always end up running a query that fetches the same kind of data, but with varying result set, depending on what the result of the subquery is.

I wanted to ensure a correct Query Builder is passed by checking whether the SQL parts contain precisely one SELECT statement with a single property of a particular name. The name enforcement is a convention detail I can live without, but I do need the method to be supplied with a query producing a single-column result.

A realistic pseudo code of the use case might look like this:

class ArticleRepository
{
    public function __construct(private readonly EntityManagerInterface $entityManager) {}

    // This method allows the developer to pass a custom query builder object that should generate a sub query, whatever the logic may be - ArticleRepository is not (should not) be concerned with what the subquery internal logic is.
    public function getArticlesWithTagsInScopeQueryBuilder(QueryBuilder $tagScopeQueryBuilder): QueryBuilder
    {
        $baseQueryBuilder = $this->getBaseQueryBuilder();

        // Here is the key step that's now deprecated: ensuring the query won't fail. Again, an example to prove my point. The logic may be more complex than counting the number of select statements. 
        if (1 !== count($tagScopeQueryBuilder->getQueryPart('select'))) {
            throw new \InvalidArgumentException('The Tag Query Builder must be usable in a subquery, so it must yield a single column of Tags or Tag IDs.');
        }

        return $baseQueryBuilder
            ->andWhere(
                $baseQueryBuilder->expr()->in(
                    x: 'article.tag_id',
                    y: $tagScopeQueryBuilder->getSQL() // <------------- more likely to work, because if the subquery was not useful like this, we'd be able to reject it. 
                )
            );
    }

    private function getBaseQueryBuilder(): QueryBuilder
    {
        // IRL, this would be a fairly complex query fetching some additional entities etc.
        return $this->entityManager->createQueryBuilder()
            ->select('article')
            ->from(Article::class, 'article');
    }
}

and the usage might be, from anywhere else in the code:

$customQueryBuilder = $entityManager->getRepository(Tag::class)->createQueryBuilder()->select('tag.id');

$articleRepository->getArticlesWithTagsInScopeQueryBuilder(
    $customQueryBuilder
);

I found this PR through the deprecation notice in QueryBuilder::getQueryPart(), and I'm wondering whether there is any other way to check the contents of the query builder other than parsing the resulting SQL?


Update: greg0ire suggested I add a better piece of code to explain my intentions, so I did just that.

@mdumoulin
Copy link
Contributor

Hi there ! Despite I understand the intention behind getQueryPart methods removal, I am not sure it serves the purpose of a query builder. If this object is about being able to build a query, we should be able to manipulate it in every possible way and get the SQL for it.
I think the getter removal is oriented by the fact that the builder also contains execute*() methods, but is it really the main goal of the builder ?


Here is a relevant use case I think :

Let's say I get every people with lastname starting by A:

function getPeopleQuery(): Builder
{
	$builder = $connection->createQueryBuilder();
	$builder
    	->select([
    	'p.firstname',
    	'p.lastname',
    	'p.email'
    	])
    	->from('people', 'p')
    	->where('p.lastname LIKE :lastname')
    	->setParameter('lastname', 'A%')
    	->orderBy('p.lastname', 'ASC');
    
	return $builder;
}

If I want to get the number of result of this query, I need to change the query in something like this :

$builder = $connection->createQueryBuilder();
$builder
	->select('COUNT(*)')
	->from('people', 'p')
	->where('p.lastname LIKE :lastname')
	->setParameter('lastname', 'D%');

If the 2nd part is not in the same place than the first one, I am no longer able to do it by modifying the first query :

$selectQuery = getPeopleQuery();
$countQuery = countFromQuery($builder);

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants