Laravel Query Builder with polymorphic joins not working on SQLite - mysql

I have a big Laravel Query Builder query and here is the minimal version of it
$query = DB::table('trainings')
->select(
'trainings.id as training_id as training_id',
'taggables.id as taggables_id',
'taggables.tag_id as taggables_tag',
'tags.id as tags_id',
DB::raw('GROUP_CONCAT(tags.category) as tags_category'),
DB::raw('GROUP_CONCAT(tags.value) as tags_value')
)
->join('taggables', function($join) {
$join->on('taggables.taggable_id', 'trainings.id')
->where('taggables.taggable_type', 'App\\Training')
;
})
->leftjoin('tags','tags.id','=','taggables.tag_id')
->groupBy('trainings.id')
-get()
;
which generates this sql:
select trainings.id as training_id,
taggables.id as taggables_id,
taggables.tag_id as taggables_tag,
tags.id as tags_id,
GROUP_CONCAT(tags.category) as tags_category,
GROUP_CONCAT(tags.value) as tags_value
from trainings
inner join taggables on taggables.id = trainings.id and taggables.taggable_type = "App\\Training"
left join tags on tags.id = taggables.tag_id
group by trainings.id
and the results is:
When i'm running the same code on phpunit test (with different data + sqlite) the results looks like this:
array:1 [
0 => {#2579
+"training_id": "2"
+"taggables_id": "1"
+"taggables_tag": "{"value":1,"category":1,"id":1}"
+"tags_id": null
+"tags_category": null
+"tags_value": null
}
]
For some reason "taggables_tag" returns JSON/object of the related tags table data and also the tags_ -fields are empty (probably because the last join is not working).
Any ideas how to fix this?
UPDATE
The problem is the join, not group_concat:
$query = DB::table('trainings')
->join('taggables', function($join) {
$join->on('taggables.taggable_id', 'trainings.id')
->where('taggables.taggable_type', 'App\\Training')
;
})
->get()
Returns:
+"tag_id": "{"value":1,"category":1,"organisation_id":1,"updated_at":"2020-01-20 10:29:56","created_at":"2020-01-20 10:29:56","id":1}"
+"taggable_id": "2"
+"taggable_type": "App\Training"

I was able to solve this by:
// running tests (SQLite) returns json of the related mode
if (App::environment() === 'testing') {
$query->addSelect('taggables.tag_id->value as taggables_tag_id');
} else {
$query->addSelect('taggables.tag_id as taggables_tag_id');
}
and changing the last leftjoin to ->leftjoin('tags','tags.id','=','taggables_tag_id')

Related

Laravel Many to Many How to form query in eloquent or in query builder

From the following query im getting the expected result:
SELECT *
FROM rooms r
JOIN amenities_room am
ON r.id = am.room_id
JOIN amenities a
ON am.amenities_id = a.id
AND a.id IN (2,3)
GROUP BY r.id
HAVING COUNT(*)=2;
How can i for the query in laravel way in (Eloquent or in Query Builder)
Note:
The following tables are involved:
rooms
id
number
name
amenities
id
name
amenities_room
room_id
amenities_id
$rooms = Room::with('amenities')
->withCount('amenities', function($query){
$query->whereIn('id', [2, 3]);
})
->where('amenities_count', 2)
->get()
Docs for counting related models: https://laravel.com/docs/5.5/eloquent-relationships#counting-related-models
You got this error mb_strpos() expects parameter 1 to be string, object given because your eloquent is expecting a string but you have passed an object here in withCount() try this
$rooms = Room::with('amenities')
->withCount('amenities')->where(function($query){
$query->whereIn('id', [2, 3]);
})
->where('amenities_count', 2)
->get()

Laravel 5.2 - Odd Results When using Left Join on Subquery

I have the following query:
$products = Product::leftJoin(DB::Raw('(SELECT imageable_id, MIN(created_at) as min_created_at
FROM images WHERE imageable_type = "App\\\Product"
GROUP BY imageable_id) AS subquery'), function($join) {
$join->on('subquery.imageable_id', '=', 'products.id');
})
->leftJoin('images', function ($join) {
$join->on('images.imageable_id', '=', 'subquery.imageable_id')
->where('images.created_at', '=', 'subquery.min_created_at');})
->select('products.*', 'images.file_path')
->paginate(5);
When I die and dump the query log, the above gets translated as follows:
"query" => """
select `products`.*, `images`.`file_path` from `products`
left join (SELECT imageable_id, MIN(created_at) as min_created_at
FROM images WHERE imageable_type = "App\\Product"
GROUP BY imageable_id) AS subquery on `subquery`.`imageable_id` = `products`.`id`
left join `images` on `images`.`imageable_id` = `subquery`.`imageable_id` and `images`.`created_at` = ?
limit 5 offset 0
"""
"bindings" => array:1 [
0 => "subquery.min_created_at"
]
Which looks correct, though I'm unsure why a binding has been added for subquery.min_created_at
Now when I execute the above the query in laravel images.file_path is always null when clearly I know there are related images. When I test the above query by pasting it and running directly in MySQL command line I get the expected results i.e. for products which have images, file_path for image is not null. The only difference when I run in MySQL command line is that I'm not doing any binding for subquery.min_created_at - I simply replaced the ? with subquery.min_created_at
Any ideas why the query is behaving this way. If I remove the second left join it works correctly but then I can't select the first created image to load e.g doing the following give me the file_path:
$products = Product::leftJoin(DB::Raw('(SELECT imageable_id, file_path
FROM images WHERE imageable_type = "App\\\Product"
GROUP BY imageable_id) AS subquery'), function($join) {
$join->on('subquery.imageable_id', '=', 'products.id');
})
->select('products.*', 'subquery.file_path')
->paginate(5);
Ideally I want to get my original query working in - any help appreciated.
You are using ->where() for your second join condition:
->where('images.created_at', '=', 'subquery.min_created_at')
This generates
and `images`.`created_at` = ?
which the binding is for.
Instead you should use ->on();
$join->on('images.imageable_id', '=', 'subquery.imageable_id')
->on('images.created_at', '=', 'subquery.min_created_at');

how to optimize mysql query in phalcon

i used this query:
$brands = TblBrand::find(array("id In (Select p.brand_id From EShop\\Models\\TblProduct as p Where p.id In (Select cp.product_id From EShop\\Models\\TblProductCategory as cp Where cp.group_id_1='$id'))", "order" => "title_fa asc"));
if($brands != null and count($brands) > 0)
{
foreach($brands as $brand)
{
$brandInProductCategory[$id][] = array
(
"id" => $brand->getId(),
"title_fa" => $brand->getTitleFa(),
"title_en" => $brand->getTitleEn()
);
}
}
TblBrand => 110 records
TblProduct => 2000 records
TblProductCategory => 2500 records
when i used this code, my site donot show and loading page very long time ...
but when i remove this code, my site show.
how to solve this problem?
The issue is your query. You are using the IN statement in a nested format, and that is always going to be slower than anything else. MySQL will need to first evaluate what is in the IN statement, return that and then do it all over again for the next level of records.
Try simplifying your query. Something like this:
SELECT *
FROM Brands
INNER JOIN Products ON Brand.id = Products.brand_id
INNER JOIN ProductCategory ON ProductCategory.product_id = Products.id
WHERE ProductCategory.group_id_1 = $id
To achieve the above, you can either use the Query Builder and get the results that way
https://docs.phalconphp.com/en/latest/api/Phalcon_Mvc_Model_Query_Builder.html
or if you have set up relationships in your models between brands, products and product categories, you can use that.
https://docs.phalconphp.com/en/latest/reference/model-relationships.html
example:
$Brands = Brands::query()
->innerJoin("Products", "Products.brand_id = Brand.id")
->innerJoin("ProductCategory", "ProductCategory.product_id = Products.id")
->where("ProductCategory.group_id_1 = :group_id:")
->bind(["group_id" => $id])
->cache(["key" => __METHOD__.$id] // if defined modelCache as DI service
->execute();
$brandInProductCategory[$id] = [];
foreach($Brands AS $Brand) {
array_push($brandInProductCategory[$id], [
"id" => $Brand->getId(),
"title_fa" => $Brand->getTitleFa(),
"title_en" => $Brand->getTitleEn()
]);
}

How to simplify this 2 MySQL Queries

This must be an easy one, but I can't remember how I did it before.
I'm trying to make another query within the query where record IDs are match:
/*
tables:
models:
model_id
model_name
models_years:
model_year_id
model_id
year
*/
$models = DBW::run('SELECT * FROM models', [], true);
$models_years = DBW::run('SELECT * FROM models_years', [], true);
$output = [];
foreach ($models as $model)
{
$years = [];
foreach ($models_years as $model_year)
{
if ($model_year['model_id'] == $model['model_id'])
{
$years[] = $model_year['year'];
}
}
$output[] = [
'model_name' => $model['model_name'],
'years' => $years
];
}
var_dump( $output );
I use PDO (settings: ATTR_DEFAULT_FETCH_MODE = FETCH_ASSOC), and "DBW::run" function returns $stmt->fetchAll().
this is just an example of what I'm trying to do or improve, I know it's possible to do all of that in a single SQL, I've done it before and can't remember! :(
SELECT m.model_name, y.`year`
FROM models AS m
LEFT JOIN models_years As y ON m.model_id = y.model_id
OR from your original question, the following might be what you need:
SELECT m.model_name, group_concat(y.`year`) AS `year`
FROM models AS m
LEFT JOIN models_years As y ON m.model_id = y.model_id
GROUP BY 1
Why?
1 - Join table to reduce SQL calls
2 - GROUP the result (BY 1 == BY m.model_name, just a lazy shorthand here)
3 - group_concat(...) will by default produce: year1,year2,year3,... and then you can use PHP explode(',', ...) to change to array if you need
Join the tables, and use GROUP_CONCAT to combine all the years for each model. Then use explode() to split that into an array.
$output = DBW::run('select m.model_name, GROUP_CONCAT(y.year) AS years
FROM models AS m
LEFT JOIN models_years AS y ON m.model_id = y.model_id
GROUP BY m.model_id', [], true);
foreach ($output as &$row) {
$row['years'] = explode(',', $row['years']);
}
SELECT model_name, year FROM models, models_years WHERE
models.model_id=models_years.model_id
Or, if you prefer the JOIN syntax,
SELECT model_name, year FROM models JOIN models_years ON
models.model_id=models_years.model_id
another way to do this is with the sub select.
select * from models where model_id in (select model_id from models_years)

mysql join ON and AND to laravel eloquent

I've been able to get the query result I need using the following raw sql:
select `person`.`id`, `full_name`, count(actions.user_id) as total
from `persons`
left join `actions`
on `actions`.`person_id` = `persons`.`id`
and `actions`.`user_id` = $user
where `type` = 'mp'
group by `persons`.`id`
But I haven't been able to get it working in eloquent yet.
Based on some similar answers, I'd tried functions within ->where() or leftJoin(), but the count of each person's actions isn't yet being filtered by $user. As it stands:
$query = Person::leftJoin('actions', function($q) use ($user)
{
$q->on('actions.person_id', 'persons.id')
->where('actions.user_id', $user);
})
->groupBy('persons.id')
->where('type', 'foo')
//->where('actions.user_id', '=', $user)
->get(['persons.id', 'full_name', DB::raw('count(actions.id) as total')]);
I'm at least heading in roughly the right direction, right...?
If it's relevant, the Persons.php model has two actions relationships:
public function actions()
{
return $this->hasMany('Action');
}
public function actionsUser($id)
{
return $this->hasMany('Action')->where('user_id', $id);
}
So, for reference, I solved it like so:
$query = Person::leftJoin('actions', function($q) use ($user)
{
$q->on('actions.person_id', '=', 'persons.id')
->where('actions.user_id', '=', "$user");
})
->groupBy('persons.id')
->where('type', 'foo')
->get(['persons.id', 'full_name', DB::raw('count(actions.id) as total')]);
The ->where() clause within leftJoin, oddly, needs the speech marks for the variable to be passed through the sql query correctly (likewise, '2' doesn't seem to work while "2" does).
I found that the where doesn't always work on the leftJoin clause
If in the future you get any trouble with it, I'd suggest you using this:
$query = Person::leftJoin('actions', function($q) use ($user)
{
$q->on('actions.person_id', '=', 'persons.id')
->on('actions.user_id', '=', "$user");
})
->groupBy('persons.id')
->where('type', 'foo')
->get(['persons.id', 'full_name', DB::raw('count(actions.id) as total')]);
Hope it helps someone.
When laravel eloquent just start getting complex like this
For more flexibility and readability I'll just use plain sql statement then hydrate the results.
$sql = "
SELECT `person`.`id`,
`full_name`,
count(actions.user_id) AS total
FROM `persons`
LEFT JOIN `actions`
ON `actions`.`person_id` = `persons`.`id`
AND `actions`.`user_id` = $user
WHERE `type` = 'mp'
GROUP by `persons`.`id`
";
$query = Person::hydrate(
DB::select( $sql )
);