Doctrine 2 - Outer join query - sql

In the context of the SonataAdminBundle / SonataUserBundle, I'm using the query builder to add static filters to the "list" query :
With this query, I get only users in the group "Juge", the query works well :
$query
->leftJoin( $query->getRootAlias().'.groups', 'g')
->andWhere( 'g.name = :group_name' )
->setParameter('group_name', 'Juge');
In an other Admin class, i want to do the oposite of this query : get the users who ARE NOT in the "Juge" group. How can I perform this? There is not outerJoin function in doctrine 2 right?

I think you want to do
$query
->leftJoin( $query->getRootAlias().'.groups', 'g',
Expr\Join::WITH, 'g.name = :group_name')
->where('g.name IS NULL')
->setParameter('group_name', 'Juge');
where Expr is Doctrine\ORM\Query\Expr.

I used the following code and it works for me, its a kind of Outer Join.
$qb = $this->getEntityManager()->createQueryBuilder()
->select('u')
->from('UserBundle:User', 'u')
->leftJoin('u.group g WITH g.id = :groupId', false)
->where('g IS NULL')
->groupBy('u.id')
->setParameter('groupId', 12)
return $qb->getQuery()->getResult();

Related

exists query does not return relevant result

I have a Laravel application with this Eloquent query:
$products = Product::where('name', 'LIKE', "%{$value}%")
->whereHas('categories', function($q) {
$q->where( 'slug', 'tonery-cartridge' );
})->with('manufacturer')
->with('sm_image')
->orderBy('created_at','DESC')
->take(10)
->get();
This code generates the sql command like:
select * from `products` where `name` LIKE '%can%'
and exists (
select * from `categories` inner join `category_product`
on `categories`.`id` = `category_product`.`category_id`
where `products`.`id` = `category_product`.`product_id`
and `slug` = 'tonery-cartridge'
)
order by `created_at` desc limit 10
I am sure there are products which name contains "can" string and which belongs to the category with slug "tonery-cartridge". Why this query returns an empty result? If I try to make inner join sql manually it works well as on the screenshot below:
I think that your queries are not equivalent. The SQL output from laravel does not join tables in it's FROM clause, but in your manually constructed SQL statement you do a lot of inner joins in your FROM clause and on the resulting table you perform your operations, which is not true for the former.
Try the following
DB::table('products')
->join('category_product', 'category_product.product_id', '=', 'products.id')
->join('categories', 'category_product.category_id', '=', 'categories.id')
->whereRaw('products.name LIKE %can% AND categories.slug = "tonery-cartridge"')
->select('products.name', 'categories.slug')
->orderBy('created_at','DESC')
->take(10)
->get()
If you want to avoid using whereRaw, you can try the following.
DB::table('products')
->join('category_product', 'category_product.product_id', '=', 'products.id')
->join('categories', 'category_product.category_id', '=', 'categories.id')
->where([
['products.name, 'LIKE', '%' . $value . '%'],
['categories.slug', '=', 'tonery-cartridge']])
->select('products.name', 'categories.slug')
->orderBy('created_at','DESC')
->take(10)
->get()
HTH
This could be one of the solutions
$category = Category::where( 'slug', 'tonery-cartridge' )->first();
$products = $category->products()
->where('name', 'LIKE', "%{$value}%")
->with('manufacturer')
->with('sm_image')
->latest()
->take(10)
->get();

Yii2: how to use subquery with ActiveRecord?

I want to create a find function to use then in a GridView widget My I don't know how to use a subquery.
This is the simple version of the PostgreSQL query that I have:
SELECT color
FROM cars
LEFT JOIN (
SELECT name
FROM companies
)
Here is what I a trying and doesn't work:
$query = Cars::find()
->select([
'color' => '
SELECT name // Problem here.
FROM companies // Problem here.
',
]);
Your expected query is not what you are trying to do. You can add a custom subquery as
$query = Cars::find()
->select([
'(SELECT name FROM companies) AS xxx'
]);
But if you want to add a field from joined table to the grid, then you should have a relation defined and then you can easily add such a column to your grid. See https://www.yiiframework.com/doc/guide/2.0/en/db-active-record#relational-data
You can use instance of ActiveQuery in leftJoin() method like this:
$subQuery = Companies::find()
->select('name', 'id'); //I've added ID to show how to write on condition
$query = Cars::find()
->select('color')
->leftJoin(
['alias' => $subQuery],
'alias.id = cars.company_id'
);
See documentation for more details about leftJoin() and join() methods.
$query = Cars::find()
->select(['cars.color','companies.name'])
->leftJoin('companies', 'cars.company_id = companies.id');

QueryBuilder - IN expression

I've created a quiz and I record in DB if people answered right to all question and the time they take to finish the quiz.
I'm trying to create a querybuilder to retrieve the guy who answered correct to the maximum of questions with the minimum of time.
My table looks like this :
So, the request (which works) I did in SQL in the DB is :
SELECT
id
FROM
public.user_quizz
WHERE
quizz_id = 4
AND
number_correct_answers IN (SELECT max(number_correct_answers) FROM user_quizz WHERE quizz_id = 4)
AND
answered_in IN (SELECT min(answered_in) FROM user_quizz WHERE quizz_id = 4);
Of course, I don't know if it's the best (and the most optimal) request we could do in this case, but it works.
Now, I'm trying to translate this query into querybuilder.
I'm blocked on the IN expression. I don't know how I could do the SELECT here.
$qb = $this->createQueryBuilder('u');
$query = $qb->select('u')
->andWhere(
$qb->expr()->eq('u.quizz', ':quizzId'),
$qb->expr()->in(
'u.numberCorrectAnswers',
)
)
->setParameter('quizzId', $quizz->getId())
->getQuery()
;
Thanks for your help.
$qbSelectMax = $this->createQueryBuilder('uc') // user copy, to prevent alias collisions
$qbSelectMax
->select($qb->expr()->max('uc.numberCorrectAnswers'))
->where($qb->expr()->eq('uc.quizz', ':quizzId'));
$qb = $this->createQueryBuilder('u')
$query = $qb->select('u')
->andWhere(
$qb->expr()->eq('u.quizz', ':quizzId'),
$qb->expr()->in(
'u.numberCorrectAnswers',
$qbSelectMax->getDQL()
)
)
->setParameter('quizzId', $quizz->getId())
->getQuery();
You can create sub DQL query to select max numberCorrectAnswers first and then pass DQL right into in parameter
ORDER BY could be used to sort by number of answers and less time taken:
$qb = $this->createQueryBuilder('u')
->orderBy('u.numberCorrectAnswers', 'DESC')
->addOrderBy('u.answeredIn', 'ASC');

How to select from subquery using Laravel Query Builder?

I'd like to get value by the following SQL using Eloquent ORM.
- SQL
SELECT COUNT(*) FROM
(SELECT * FROM abc GROUP BY col1) AS a;
Then I considered the following.
- Code
$sql = Abc::from('abc AS a')->groupBy('col1')->toSql();
$num = Abc::from(\DB::raw($sql))->count();
print $num;
I'm looking for a better solution.
Please tell me simplest solution.
In addition to #delmadord's answer and your comments:
Currently there is no method to create subquery in FROM clause, so you need to manually use raw statement, then, if necessary, you will merge all the bindings:
$sub = Abc::where(..)->groupBy(..); // Eloquent Builder instance
$count = DB::table( DB::raw("({$sub->toSql()}) as sub") )
->mergeBindings($sub->getQuery()) // you need to get underlying Query Builder
->count();
Mind that you need to merge bindings in correct order. If you have other bound clauses, you must put them after mergeBindings:
$count = DB::table( DB::raw("({$sub->toSql()}) as sub") )
// ->where(..) wrong
->mergeBindings($sub->getQuery()) // you need to get underlying Query Builder
// ->where(..) correct
->count();
Laravel v5.6.12 (2018-03-14) added fromSub() and fromRaw() methods to query builder (#23476).
The accepted answer is correct but can be simplified into:
DB::query()->fromSub(function ($query) {
$query->from('abc')->groupBy('col1');
}, 'a')->count();
The above snippet produces the following SQL:
select count(*) as aggregate from (select * from `abc` group by `col1`) as `a`
The solution of #JarekTkaczyk it is exactly what I was looking for. The only thing I miss is how to do it when you are using
DB::table() queries. In this case, this is how I do it:
$other = DB::table( DB::raw("({$sub->toSql()}) as sub") )->select(
'something',
DB::raw('sum( qty ) as qty'),
'foo',
'bar'
);
$other->mergeBindings( $sub );
$other->groupBy('something');
$other->groupBy('foo');
$other->groupBy('bar');
print $other->toSql();
$other->get();
Special atention how to make the mergeBindings without using the getQuery() method
From laravel 5.5 there is a dedicated method for subqueries and you can use it like this:
Abc::selectSub(function($q) {
$q->select('*')->groupBy('col1');
}, 'a')->count('a.*');
or
Abc::selectSub(Abc::select('*')->groupBy('col1'), 'a')->count('a.*');
There are many readable ways to do these kinds of queries at the moment (Laravel 8).
// option 1: DB::table(Closure, alias) for subquery
$count = DB::table(function ($sub) {
$sub->from('abc')
->groupBy('col1');
}, 'a')
->count();
// option 2: DB::table(Builder, alias) for subquery
$sub = DB::table('abc')->groupBy('col1');
$count = DB::table($sub, 'a')->count();
// option 3: DB::query()->from(Closure, alias)
$count = DB::query()
->from(function ($sub) {
$sub->from('abc')
->groupBy('col1')
}, 'a')
->count();
// option 4: DB::query()->from(Builder, alias)
$sub = DB::table('abc')->groupBy('col1');
$count = DB::query()->from($sub, 'a')->count();
For such small subqueries, you could even try fitting them in a single line with PHP 7.4's short closures but this approach can be harder to mantain.
$count = DB::table(fn($sub) => $sub->from('abc')->groupBy('col1'), 'a')->count();
Note that I'm using count() instead of explicitly writing the count(*) statement and using get() or first() for the results (which you can easily do by replacing count() with selectRaw(count(*))->first()).
The reason for this is simple: It returns the number instead of an object with an awkwardly named property (count(*) unless you used an alias in the query)
Which looks better?
// using count() in the builder
echo $count;
// using selectRaw('count(*)')->first() in the builder
echo $count->{'count(*)'};
Correct way described in this answer: https://stackoverflow.com/a/52772444/2519714
Most popular answer at current moment is not totally correct.
This way https://stackoverflow.com/a/24838367/2519714 is not correct in some cases like: sub select has where bindings, then joining table to sub select, then other wheres added to all query. For example query:
select * from (select * from t1 where col1 = ?) join t2 on col1 = col2 and col3 = ? where t2.col4 = ?
To make this query you will write code like:
$subQuery = DB::query()->from('t1')->where('t1.col1', 'val1');
$query = DB::query()->from(DB::raw('('. $subQuery->toSql() . ') AS subquery'))
->mergeBindings($subQuery->getBindings());
$query->join('t2', function(JoinClause $join) {
$join->on('subquery.col1', 't2.col2');
$join->where('t2.col3', 'val3');
})->where('t2.col4', 'val4');
During executing this query, his method $query->getBindings() will return bindings in incorrect order like ['val3', 'val1', 'val4'] in this case instead correct ['val1', 'val3', 'val4'] for raw sql described above.
One more time correct way to do this:
$subQuery = DB::query()->from('t1')->where('t1.col1', 'val1');
$query = DB::query()->fromSub($subQuery, 'subquery');
$query->join('t2', function(JoinClause $join) {
$join->on('subquery.col1', 't2.col2');
$join->where('t2.col3', 'val3');
})->where('t2.col4', 'val4');
Also bindings will be automatically and correctly merged to new query.
I like doing something like this:
Message::select('*')
->from(DB::raw("( SELECT * FROM `messages`
WHERE `to_id` = ".Auth::id()." AND `isseen` = 0
GROUP BY `from_id` asc) as `sub`"))
->count();
It's not very elegant, but it's simple.
This works fine
$q1 = DB::table('tableA')->groupBy('col');
$data = DB::table(DB::raw("({$q1->toSql()}) as sub"))->mergeBindings($q1)->get();
I could not made your code to do the desired query, the AS is an alias only for the table abc, not for the derived table.
Laravel Query Builder does not implicitly support derived table aliases, DB::raw is most likely needed for this.
The most straight solution I could came up with is almost identical to yours, however produces the query as you asked for:
$sql = Abc::groupBy('col1')->toSql();
$count = DB::table(DB::raw("($sql) AS a"))->count();
The produced query is
select count(*) as aggregate from (select * from `abc` group by `col1`) AS a;
->selectRaw('your subquery as somefield')
Deriving off mpskovvang's answer, here is what it would look like using eloquent model. (I tried updating mpskovvang answer to include this, but there's too many edit requests for it.)
$qry = Abc::where('col2', 'value')->groupBy('col1')->selectRaw('1');
$num = Abc::from($qry, 'q1')->count();
print $num;
Produces...
SELECT COUNT(*) as aggregate FROM (SELECT 1 FROM Abc WHERE col2='value' GROUP BY col1) as q1

Doctrine Search by count on join Table

I have a problem with my nemesis: SQL/DQL
I have an entity: Style and an entity Vote. A Style can have a lot of Votes (as you can imagine) and one Vote is linked to exactly one Style.
I now want to search for Styles that have less Votes than a Specific Amount (it's user input, but let's say it is 5). This is the code I use with the querybuilder
$qb = $this->createQueryBuilder('s');
$qb
->select("s")
->join('s.votes', 'v')
->addSelect("COUNT(v.id) as voteCount")
->where("voteCount < ?1")
->orderBy('voteCount', "DESC")
->setMaxResults(3)
->setFirstResult(0)
->groupBy('s.id')
->setParameter(1, 5);
$query = $qb->getQuery();
$result = $query->getResult();
Once I try to execute the query, it basically says, that i voteCount is not known in the where clause. Here is the exact Error Message
An exception occurred while executing 'SELECT s0_.active AS active0, s0_.views AS views1, s0_.description AS description2, s0_.name AS name3, s0_.id AS id4, s0_.updated AS updated5, s0_.created AS created6, COUNT(v1_.id) AS sclr7, s0_.battle_id AS battle_id8, s0_.user_id AS user_id9, s0_.voucher_id AS voucher_id10 FROM Style s0_ INNER JOIN Vote v1_ ON s0_.id = v1_.style_id WHERE sclr7 < ? GROUP BY s0_.id ORDER BY sclr7 DESC LIMIT 3 OFFSET 0' with params [1]:
SQLSTATE[42S22]: Column not found: 1054 Unknown column 'sclr7' in 'where clause'
What did I do wrong? How can I search vor the voteCount?
Conditions for aggregate functions such as sum and count should be put in a having clause.
$qb = $this->createQueryBuilder('s');
$qb->select("s");
$qb->join('s.votes', 'v');
$qb->addSelect("COUNT(v.id) as voteCount");
$qb->orderBy('voteCount', "DESC");
$qb->setMaxResults(3);
$qb->setFirstResult(0);
$qb->groupBy('s.id');
$qb->having("count(v.id) < ?1"); //changed from where
$qb->setParameter(1, 5);
$query = $qb->getQuery();
$result = $query->getResult();
You cannot reference a column alias in the WHERE clause.
Change
$qb->where("voteCount < ?1");
To
$qb->where("COUNT(v.id) < ?1");