Laravel - Union + Paginate at the same time? - php

Brief:
I am trying to union 2 tables recipes and posts then add ->paginate(5) to the queries.
But for some reason I get this error:
Cardinality violation: 1222 The used SELECT statements have a
different number of columns (SQL: (select count(*) as aggregate from
posts
Code:
$recipes = DB::table("recipes")->select("id", "title", "user_id", "description", "created_at")
->where("user_id", "=", $id);
$items = DB::table("posts")->select("id", "title", "user_id", "content", "created_at")
->where("user_id", "=", $id)
->union($recipes)
->paginate(5)->get();
Am i doing something wrong?
Without ->paginate(5) the query works fine.

You're right, pagination cause problem. Right now, you can create a view and query the view instead of the actual tables, or create your Paginator manually:
$page = Input::get('page', 1);
$paginate = 5;
$recipes = DB::table("recipes")->select("id", "title", "user_id", "description", "created_at")
->where("user_id", "=", $id);
$items = DB::table("posts")->select("id", "title", "user_id", "content", "created_at")
->where("user_id", "=", $id)
->union($recipes)
->get();
$slice = array_slice($items->toArray(), $paginate * ($page - 1), $paginate);
$result = Paginator::make($slice, count($items), $paginate);
return View::make('yourView',compact('result'));

I faced this kind of issue already. I found a thread also not about pagination but about unions.
Please see this link : Sorting UNION queries with Laravel 4.1
#Mohamed Azher has shared a nice trick and it works on my issue.
$query = $query1->union($query2);
$querySql = $query->toSql();
$query = DB::table(DB::raw("($querySql order by foo desc) as a"))->mergeBindings($query);
This creates an sql like below:
select * from (
(select a as foo from foo)
union
(select b as foo from bar)
) as a order by foo desc;
And you can already utilize Laravel's paginate same as usual like $query->paginate(5). (but you have to fork it a bit to fit to your problem)

Reiterating jdme's answer with a more elegant method from Illuminate\Database\Query\Builder.
$recipes = DB::table("recipes") ..
$items = DB::table("posts")->union($recipes) ..
$query = DB::query()
->fromSub($items, "some_query_name");
// Let's paginate!
$query->paginate(5);
I hope this helps!

The accepted answer works great for Query Builder.
But here's my approach for Laravel Eloquent Builder.
Assume that we're referring to same Model
$q1 = Model::createByMe(); // some condition
$q2 = Model::createByMyFriend(); // another condition
$q2->union($q1);
$querySql = $q2->toSql();
$query = Model::from(DB::raw("($querySql) as a"))->select('a.*')->addBinding($q2->getBindings());
$paginated_data = $query->paginate();
I'm using Laravel 5.6

order by
$page = Input::get('page', 1);
$paginate = 5;
$recipes = DB::table("recipes")->select("id", "title", "user_id", "description", "created_at")
->where("user_id", "=", $id);
$items = DB::table("posts")->select("id", "title", "user_id", "content", "created_at")
->where("user_id", "=", $id)
->union($recipes)
->orderBy('created_at','desc')
->get();
$slice = array_slice($items, $paginate * ($page - 1), $paginate);
$result = Paginator::make($slice, count($items), $paginate);
return View::make('yourView',compact('result'))->with( 'result', $result );
View page :
#foreach($result as $data)
{{ $data->your_column_name;}}
#endforeach
{{$result->links();}} //for pagination
its help to more peoples.. because nobody cant understand show data in
view page union with pagination and orderby .. thank u

I know this answer is too late. But I want to share my problems and my solution.
My problems:
Join with many tables at the same time
UNION
Paginate (Must use, because I have to use a common theme to show pagination. If I made own custom for pagination, it will not match to current. And in the future, a common theme may be changed.)
Big data: view took 4 seconds, page load took 4 seconds => total is 8 seconds. (But if I set condition inside that view, it was least than 1 second for total.)
Query
※This is the sample.
MariaDB, about 146,000 records.
SELECT A.a_id
, A.a_name
, B.organization_id
, B.organization_name
FROM customers A
LEFT JOIN organizations B ON (A.organization_id = B.organization_id)
UNION ALL
SELECT A.a_id
, A.a_name
, B.organization_id
, B.organization_name
FROM employees A
LEFT JOIN organizations B ON (A.organization_id = B.organization_id)
Solution
Reference from www.tech-corgi.com (やり方2), I updated my PHP code to filter inside my query, and then call paginate normally.
I must add a condition (filter) before getting large records. In this example is organization_id.
$query = "
SELECT A.a_id
, A.a_name
, B.organization_id
, B.organization_name
FROM customers A
LEFT JOIN organizations B ON (A.organization_id = B.organization_id)
WHERE 1 = 1
AND B.organization_id = {ORGANIZATION_ID}
UNION ALL
SELECT A.a_id
, A.a_name
, B.organization_id
, B.organization_name
FROM employees A
LEFT JOIN organizations B ON (A.organization_id = B.organization_id)
WHERE 1 = 1
AND B.organization_id = {ORGANIZATION_ID}
";
$organization_id = request()->organization_id;
$query = str_replace("{ORGANIZATION_ID}", $organization_id, $query);
But it still cannot be used in paginate(). There is a trick to solve this problem. See below.
Final code
Trick: put query inside (). For example: (SELECT * FROM TABLE_A).
Reason: paginage() will generate and run Count query SELECT count(*) FROM (SELECT * FROM TABLE_A), if we did not put inside brackets, Count query would not be a correct query.
$query = "
( SELECT A.a_id
, A.a_name
, B.organization_id
, B.organization_name
FROM customers A
LEFT JOIN organizations B ON (A.organization_id = B.organization_id)
WHERE 1 = 1
AND B.organization_id = {ORGANIZATION_ID}
UNION ALL
SELECT A.a_id
, A.a_name
, B.organization_id
, B.organization_name
FROM employees A
LEFT JOIN organizations B ON (A.organization_id = B.organization_id)
WHERE 1 = 1
AND B.organization_id = {ORGANIZATION_ID}
) AS VIEW_RESULT
";
$organization_id = request()->organization_id;
$query = str_replace("{ORGANIZATION_ID}", $organization_id, $query);
$resultSet = DB::table(DB::raw($query))->paginate(20);
Now I can use it normally:
SELECT, JOIN, UNION
paginate
High performance: Filter data before getting
Hope it help!!!

Getting the total count for pagination is the problem here. This is the error I got when used $builder->paginate()
"SQLSTATE[21000]: Cardinality violation: 1222 The used SELECT statements have a different number of columns (SQL: (select count(*) as aggregate from `institute_category_places` where `status` = approved and (`category_id` in (3) or `name` LIKE %dancing class% or `description` LIKE %dancing class% or `address_line1` LIKE %dancing class% or `address_line2` LIKE %dancing class% or `city` LIKE %dancing class% or `province` LIKE %dancing class% or `country` LIKE %dancing class%) and `institute_category_places`.`deleted_at` is null) union (select * from `institute_category_places` where `status` = approved and (`category_id` in (3, 4) or `name` LIKE %dancing% or `description` LIKE %dancing% or `address_line1` LIKE %dancing% or `address_line2` LIKE %dancing% or `city` LIKE %dancing% or `province` LIKE %dancing% or `country` LIKE %dancing% or `name` LIKE %class% or `description` LIKE %class% or `address_line1` LIKE %class% or `address_line2` LIKE %class% or `city` LIKE %class% or `province` LIKE %class% or `country` LIKE %class%) and `institute_category_places`.`deleted_at` is null))"
If you want to paginate without total count you can use
$builder->limit($per_page)->offset($per_page * ($page - 1))->get();
to get only set of rows in the page.
Getting all the rows and counting total is memory inefficient. So I used following approach to get total count.
$bindings = $query_builder->getBindings();
$sql = $query_builder->toSql();
foreach ($bindings as $binding) {
$value = is_numeric($binding) ? $binding : "'" . $binding . "'";
$sql = preg_replace('/\?/', $value, $sql, 1);
}
$sql = str_replace('\\', '\\\\', $sql);
$total = DB::select(DB::raw("select count(*) as total_count from ($sql) as count_table"));
Then we have to paginate the result manually.
$page = Input::get('page', 1);
$per_page = 15;
$search_results = $query_builder->limit($per_page)->offset($per_page * ($page - 1))->get();
$result = new LengthAwarePaginator($search_results, $total[0]->total_count, $per_page, $page, ['path' => $request->url()]);
If you can use raw sql queries, it is much more CPU and memory efficient.

Using Eloquent
I adapted jdme's answer in order to use it with Eloquent. I created a class extending the default Eloquent Builder and overiding the union method to fix the issue with paginate.
Create app\Builder\BuilderWithFixes.php:
<?php
namespace App\Builder;
use Illuminate\Database\Eloquent\Builder;
class BuilderWithFixes extends Builder
{
/**
* Add a union statement to the query.
*
* #param \Illuminate\Database\Query\Builder|\Closure $query
* #param bool $all
* #return \Illuminate\Database\Query\Builder|static
*/
public function union($query, $all = false)
{
$query = parent::union($query, $all);
$querySql = $query->toSql();
return $this->model->from(\DB::raw("($querySql) as ".$this->model->table))->select($this->model->table.'.*')->addBinding($this->getBindings());
}
}
In you Model (for example app\Post.php), include the method newEloquentBuilder below to replace the default Eloquent Builder with \App\Builder\BuilderWithFixes:
<?php
namespace App;
use Eloquent as Model;
class Post extends Model
{
// your model stuffs...
public function newEloquentBuilder($query)
{
return new \App\Builder\BuilderWithFixes($query);
}
}
Now you can use union + paginate at the same time within your model (in this case Post) normally, like:
$recipes = Recipe::select("id", "title", "user_id", "description", "created_at")
->where("user_id", "=", $id);
$items = Post::select("id", "title", "user_id", "content", "created_at")
->where("user_id", "=", $id)
->union($recipes)
->paginate(5);

I had this same problem, and unfortunately I couldn't get the page links with {{ $result->links() }}, but I found another way to write the pagination part and the page links appears
Custom data pagination with Laravel 5
//Create a new Laravel collection from the array data
$collection = new Collection($searchResults);
//Define how many items we want to be visible in each page
$perPage = 5;
//Slice the collection to get the items to display in current page
$currentPageSearchResults = $collection->slice($currentPage * $perPage, $perPage)->all();
//Create our paginator and pass it to the view
$paginatedSearchResults= new LengthAwarePaginator($currentPageSearchResults, count($collection), $perPage);
return view('search', ['results' => $paginatedSearchResults]);

for paginate collection do this:
add this to boot function in \app\Providers\AppServiceProvider
/**
* Paginate a standard Laravel Collection.
*
* #param int $perPage
* #param int $total
* #param int $page
* #param string $pageName
* #return array
*/
Collection::macro('paginate', function($perPage, $total = null, $page = null, $pageName = 'page') {
$page = $page ?: LengthAwarePaginator::resolveCurrentPage($pageName);
return new LengthAwarePaginator(
$this->forPage($page, $perPage),
$total ?: $this->count(),
$perPage,
$page,
[
'path' => LengthAwarePaginator::resolveCurrentPath(),
'pageName' => $pageName,
]
);
});
From hereafter for all collection you can paginate like your code
$items = DB::table("posts")->select("id", "title", "user_id", "content", "created_at")
->where("user_id", "=", $id)
->union($recipes)
->paginate(5)

For those who may still look for the answer, I have tried union and paginate together and got right result under laravel 5.7.20. This will be better than merging collections then paginate which will not work on big amount of data.
Some demo code (in my case, I will deal with multiple databases with same table name):
$dbs=["db_name1","db_name2"];
$query=DB::table("$dbs[0].table_name");
for($i=1;$i<count($log_dbs);$i++){
$query=DB::table("$dbs[$i].table_name")->union($query);
}
$query=$query->orderBy('id','desc')->paginate(50);
I haven't tried on other higher version of laravel. But at least it could work now!
More information
My previous version of laravel is 5.7.9 which will report the Cardinality violation error. So the laravel team solved this issue in some version of 5.7.x.

$page = Input::get('page', 1);
$paginate = 5;
$recipes = DB::table("recipes")->select("id", "title", "user_id", "description", "created_at")
->where("user_id", "=", $id);
$items = DB::table("posts")->select("id", "title", "user_id", "content", "created_at") ->where("user_id", "=", $id)->union($recipes)->get()->toArray();
$slice = array_slice($items, $paginate * ($page - 1), $paginate);
$result = new Paginator($slice , $paginate);

Related

Not able to find correct data with whereHas and having count

I have users, conversations, conversation_user and messages table:
Before making a new conversation with $data array of user id's, I am trying to find existing one:
$data = [2,3]; // without auth()->id() -> 1 => total of 3 users per conversation
auth()->user()->conversations()->has('messages')->whereHas('users', function ($query) use ($data) {
$query->whereIn('user_id', $data);
})->whereHas('users', function ($query) use ($data) {
$query->groupBy('conversation_id', 'conversation_user.id')
->havingRaw('count(conversation_id) = ' . count($data) + 1); // total count is 3
})->first()
Now first whereHas returns even if I have conversation between auth()->id() and ID-2, because user_id 2 is in (2,3).. So it would retrieve the wrong conversation, where I need to count for users per conversation as well.
The second whereHas is for counting however if I use $query->groupBy('conversation_id') I get mysql SQL_MODE error for grouping, meaning I need to add $query->groupBy('conversation_id', 'conversation_user.id') as well, but with all that I get no record from database even if there are some.
What am I missing here?
[Updated with generated sql]
select * from `conversations`
inner join `conversation_user` on `conversations`.`id` = `conversation_user`.`conversation_id`
where `conversation_user`.`user_id` = 1 and exists (
select * from `conversation_messages`
where `conversations`.`id` = `conversation_messages`.`conversation_id`
and `conversation_messages`.`deleted_at` is null
) and exists (
select * from `users`
inner join `conversation_user` on `users`.`id` = `conversation_user`.`user_id`
where `conversations`.`id` = `conversation_user`.`conversation_id`
and `user_id` in (2, 3) and `users`.`deleted_at` is null
) and exists (
select * from `users`
inner join `conversation_user` on `users`.`id` = `conversation_user`.`user_id`
where `conversations`.`id` = `conversation_user`.`conversation_id`
and `users`.`deleted_at` is null
group by `conversation_id`, `conversation_user`.`id`
having count(conversation_id) = 3
) and `conversations`.`deleted_at` is null
[Update with table structures]
users -> id, name, email
conversations -> id, slug, subject
conversation_user -> id, user_id, conversation_id
messages -> id, conversation_id, user_id, body
[Another update]
Seems like this works also, in case someone need:
auth()->user()->conversations()->has('messages')->whereHas('users', function ($query) use ($data) {
$query->whereIn('user_id', $data);
})->whereDoesntHave('users', function ($query) use ($data) {
$query->whereNotIn('user_id', $data);
})->first()
I think this is the SQL you want -
SELECT c.*
FROM conversation_user cu
JOIN conversations c
ON cu.conversation_id = c.id
WHERE cu.user_id IN (1, 2, 3)
AND NOT EXISTS (
SELECT *
FROM conversation_user
WHERE conversation_id = cu.conversation_id
AND user_id NOT IN (1, 2, 3)
)
GROUP BY cu.conversation_id
HAVING COUNT(DISTINCT cu.user_id) = 3
Not sure if this is correct as I am not a Laravel user -
$data = [1, 2, 3];
$conv = DB::table('conversation_user cu')
->select('c.*')
->join('conversations c', 'cu.conversation_id', '=', 'c.id')
->whereIn('cu.user_id', $data)
->whereNotExists(function($query) use ($data) {
$query->select(DB::raw(1))
->from('conversation_user')
->whereColumn('conversation_id', 'cu.conversation_id')
->whereNotIn('user_id', $data);
})
->groupBy('cu.conversation_id')
->havingRaw('COUNT(DISTINCT cu.user_id) = ?', count($data))
->get();

Laravel - query to join tables in Many to many relation

I have the below tables
posts
- id
- name
categories
- id
- name
category_post
- post_id
- category_id
Post.php
public function Category()
{
return $this->belongsToMany(Category::class);
}
Category.php
public function posts()
{
return $this->belongsToMany(Post::class);
}
A post may contain many categories.
I want to query all posts related to the categories of any given post in the least number of database queries.
For example, If a post belongs to three categories, I want to get all the posts related to that three categories. I could achieve this in 4 DB queries as below.
$post = Post::find(1);
$categoryIds = $post->category()->pluck('id');
$postIds = DB::table('category_post')
->whereIn('category_id', $categoryIds)
->pluck('post_id')
->unique();
$relatedPosts = DB::table('posts')
->whereIn('id', $postIds)
->get();
Any help would be highly appreciated to reduce DB queries or refactor the code in laravel way.
Thanks
Your given example can be written like:
$postWithRelatedPosts = Post::whereHas('category.post', function ($q) {
$q->where('id', 1);
})->get();
This is a single query but has 2 sub-queries within it. It generates something like:
select *
from `posts`
where exists (
select *
from `categories` inner join `category_post` on `categories`.`id` = `category_post`.`category_id`
where `posts`.`id` = `category_post`.`post_id` and exists (
select *
from `posts` inner join `category_post` on `posts`.`id` = `category_post`.`post_id`
where `categories`.`id` = `category_post`.`category_id` and `id` = ?
)
)

Laravel table joins

I'm trying to do some table joins and having some trouble.i need to display
the customer’s complete name and title, item description, quantity ordered, for each item ordered after April 2000. i made a SQL script that works but i need to use Laravel ORM.
SELECT `first_name`,`last_name`,o.order_id,`quantity`,`description`,`date_placed`
FROM `customer` c
JOIN `order` o
ON c.`customer_id` = o. `customer_id`
JOIN `orderline` ol
ON o.`order_id` = ol. `order_id`
JOIN `item` i
ON ol.`item_id` = i. `item_id`
WHERE `date_placed` > '2000-4-00';
I created 2 models for the tables "Customer", "Order"
here is my Customer model
public function orders(){
return $this->hasMany('App\Order','customer_id');
}
here is my order model
public function orderline(){
return $this->hasMany('App\Orderline','order_id');
}
right now i am able to get some of my data but i dont feel like this is a good way to go about
$customer = Customer::all();
foreach($customer as $input){
$item = Customer::find($input->customer_id)->orders;
$name = $input->title . ' ' . $input->first_name . ' ' . $input->last_name;
$datePlaced = null;
$orderID = null;
foreach($item as $value){
$orderID = $value->order_id;
$datePlaced = $value->date_placed;
$order = Order::find($value->order_id)->orderline;
}
if anyone could point me in the right direction that would be great.
It looks like you want to get all Customers with their Orders and OrderLines?
Customer::with(['order' => function ($query) {
$query->where('date_placed', '>=', '2000-04-01')->with('orderline');
}])->get();
If you want to limit the columns on the relationships, you can...
Customer::with(['order' => function ($query) {
$query->select(/* columns */)->where('date_placed', '>=', '2000-04-01')
->with(['orderline' => function ($query) {
$query->select(/* columns here */);
}]);
}])->get();
Just make sure if you specify the columns in the relationships, that you're selecting all of the foreign keys or related columns for each relationship.

How to count and group by in yii2

I would like to generate following query using yii2:
SELECT COUNT(*) AS cnt FROM lead WHERE approved = 1 GROUP BY promoter_location_id, lead_type_id
I have tried:
$leadsCount = Lead::find()
->where('approved = 1')
->groupBy(['promoter_location_id', 'lead_type_id'])
->count();
Which generates this query:
SELECT COUNT(*) FROM (SELECT * FROM `lead` WHERE approved = 1 GROUP BY `promoter_location_id`, `lead_type_id`) `c`
In yii 1.x I would've done the following:
$criteria = new CDbCriteria();
$criteria->select = 'COUNT(*) AS cnt';
$criteria->group = array('promoter_location_id', 'lead_type_id');
Thanks!
Solution:
$leadsCount = Lead::find()
->select(['COUNT(*) AS cnt'])
->where('approved = 1')
->groupBy(['promoter_location_id', 'lead_type_id'])
->all();
and add public $cnt to the model, in my case Lead.
As Kshitiz also stated, you could also just use yii\db\Query::createCommand().
You can get the count by using count() in the select Query
$leadCount = Lead::find()
->where(['approved'=>'1'])
->groupBy(['promoter_location_id', 'lead_type_id'])
->count();
Reference Link for various functions of select query
If you are just interested in the count, use yii\db\Query as mentioned by others. Won't require any changes to your model:
$leadsCount = (new yii\db\Query())
->from('lead')
->where('approved = 1')
->groupBy(['promoter_location_id', 'lead_type_id'])
->count();
Here's a link to the Yii2 API documentation
Without adding the $cnt property to model
$leadsCount = Lead::find()
->select(['promoter_location_id', 'lead_type_id','COUNT(*) AS cnt'])
->where('approved = 1')
->groupBy(['promoter_location_id', 'lead_type_id'])
->createCommand()->queryAll();
Just a note, in case it helps anyone, that a getter used as a property is countable (whereas if called as a function it will return 1). In this example, I have a Category class with Listings joined by listing_to_category. To get Active, Approved Listings for the Category, I return an ActiveQuery, thus:
/**
* #return \yii\db\ActiveQuery
*/
public function getListingsApprovedActive() {
return $this->hasMany(Listing::className(), ['listing_id' => 'listing_id'])
->viaTable('listing_to_category', ['category_id' => 'category_id'])
->andWhere(['active' => 1])->andWhere(['approved' => 1]);
}
Calling count on the property of the Category will return the record count:
count($oCat->listingsApprovedActive)
Calling count on the function will return 1:
count($oCat->getListingsApprovedActive())

How do I build a UNION query with ORDER BY and GROUP BY in Kohana's query builder?

I'm trying to build a UNION query using Kohana's query builder. Everything works fine until I add a GROUP BY or ORDER BY clause.
Here is the code I'm using (simplified):
$query1 = DB::select('p.name')
->from(array('person', 'p'))
->where('p.organization', 'LIKE', 'foo%')
->limit(10);
$names = DB::select('sh.name')
->union($query1, FALSE)
->from(array('stakeholder', 'sh'))
->where('sh.organization', 'LIKE', 'foo%')
->group_by('name')
->order_by('name')
->limit(10)
->execute()
->as_array();
Instead of adding the GROUP BY and ORDER BY at the end of the entire query, it's adding it immediately after the second query.
This is the SQL this generates:
SELECT sh.name FROM stakeholder AS sh WHERE sh.organization LIKE 'foo%'
GROUP BY name ORDER BY name LIMIT 10
UNION
SELECT p.name from person AS p WHERE p.organization LIKE 'foo%' LIMIT 10;
What I want is:
SELECT sh.name FROM stakeholder AS sh WHERE sh.organization LIKE 'foo%'
UNION
SELECT p.name from person AS p WHERE p.organization LIKE 'foo%'
GROUP BY name ORDER BY name LIMIT 10;
The clauses here are applied from the first query set up in the union() method, so just reverse where you're putting them:
$query1 = DB::select('p.name')
->from(array('person', 'p'))
->where('p.organization', 'LIKE', 'foo%')
->group_by('name')
->order_by('name')
->limit(10);
$names = DB::select('sh.name')
->union($query1, FALSE)
->from(array('stakeholder', 'sh'))
->where('sh.organization', 'LIKE', 'foo%')
->execute()
->as_array();
You can also remove that superfluous ->limit(10) from $names since it will be ignored and superseded by the one in $query1.
You can also extend Kohana_ORM using ORM's db_pending:
class ORM extends Kohana_ORM {
public function union($table, $all = TRUE)
{
// Add pending database call which is executed after query type is determined
$this->_db_pending[] = array(
'name' => 'union',
'args' => array($table, $all),
);
return $this;
}
}
Usage:
ORM::factory('MyModel')
->union(DB::select(DB::expr("'RP' id, 'Pasantías' name, 'Pasantías' short_name, 'R' parent_id, null data")))
->union(DB::select(DB::expr("'RC' id, 'Capacitación' name, 'Capacitación' short_name, 'R' parent_id, null data")))
->join(['catalogo', 'p'])->on('catalogo.parent_id', '=', 'p.id')
->where('p.parent_id', 'is', NULL)
->where('catalogo.id', 'not in', ['RV', 'RPA', 'RPT']);
That answer from 2011 isn't working in Kohana 3.3.
But I found this module: https://github.com/Invision70/kohana-orm-union

Categories