Laravel Eloquent Limit in Relation that has Sub Relation - mysql

I have hasmany relation from category to contents, and I want limitation 4 content for each category.
I would like to limit the result of relation contents that has sub relation to languages
My Code
Category::with(['contents.languages' => function($query){
$query->limit(4);
}])
->get();
But I see in log the limit works on languages relation, not contents, that I wanted is limit on contents

take() and limit() functions will not work with eager loading if you retrieve parent model more than one using get().
So you have to do another way,
$categories = Category::with('contents')->get();
After retrieving $categories, you can do foreach loop like below,
$contents = [];
foreach($categories as $category){
$category->limitedContents = $category->contents()->with('languages')->limit(4);
}
And by doing this you will get 4 contents per category in all categories with limitedContents.
Note: Here I used name as 'limitedContents' because you have already defined contents relationship.

This question is basically something akin to Get top n records for each group of grouped results
As far as I can see there's not much choice but to perform N+1 queries. You can achieve this by doing:
$categories = Category::get();
$categories->each(function ($category) {
$category->load([ 'contents' => function ($q) {
return $q->limit(4);
}, 'contents.languages']);
});
Can we do better? I doubt it doubtful, though I am open to ideas. While we can optimise this to send a less queries to the database, the database internally will still need to compute the N+1 queries.

This also works
foreach(Category::with('contents')->get() as $category)
{
foreach($category->contents->take(4) as $content)
{
$languages = $content->with('languages')->get();
foreach($languages as $language)
{
//your code
}
}
}

You should try this:
Category::with(['contents.languages'])->limit(4)->get();

You could set additional method in Category model and make a call with that one:
public function contentsTake4()
{
return $this->hasMany('App\Content')->limit(4);
}
Then you would call it in controller as :
Category::with('contentsTake4.products')->get();

Try this
Category::with([
'contents' => function($q) { $q->limit(4); },
'contents.languages'
])->get();

Related

How to get all first records (ordered by a column) of a nested one to many relationship in Laravel?

I've some nested one to many relationship as follows:
Enquiry hasMany Item
Item hasMany Component
Component hasMany Process
Imagine I have the Enquiry Model loaded already. Now I would like to access the first Process of each Component of each Item ordered by a column in Process called order
This picture might show it better (Sorry for using ERD like this)
What I've got so far is something like this:
$enquiry->items->load(['components' => function($query) {
$query->with(['processes' => function($query) {
$query->orderBy('order')->limit(1)
}]);
}])->get();
but it only gives me the top process, not the top Process of each Component of each Item belonging to the given Enquiry.
Any help would be appreciated. And ofcourse any help regarding a better title would be much appreciated as well. Thank you.
Can't you define a relationship on your Component model like this:
class Component {
public function firstProcess() {
return $this->hasMany(Process::class)->orderBy('order')->take(1);
}
}
And then do something like this:
$enquiry->load('items.components.firstProcess');
Then you should be able to access that property on each of your components:
foreach ($enquiry->items as $item) {
foreach($item->components as $component) {
print_r($component->firstProcess);
}
}
I didnt test this code.
Thanks to #PaulSpiegel Link it directed me to the right way. This is how I did it with subqueries in Laravel using QueryBuilder.
$subQuery = DB::table('processes as icp')
->selectRaw('min(icp.order), icp.*')
->groupBy('icp.component_id');
$firstProcesses = DB::table('enquiries as enq')
->where('enq.id', $enquiry->id)
->join('items', 'items.enquiry_id', 'enq.id')
->join('components as ic', 'ic.item_id', 'items.id')
->joinSub($subQuery, 'icp', function ($join) {
$join->on('ic.id', 'icp.component_id');
})->get();

Laravel - Eloquent - Filter based on latest HasMany relation

I have this two models, Leads and Status.
class Lead extends Model
{
public function statuses() {
return $this->hasMany('App\LeadStatus', 'lead_id', 'id')
->orderBy('created_at', 'DESC');
}
public function activeStatus() {
return $this->hasOne('App\LeadStatus', 'lead_id', 'id')
->latest();
}
}
class LeadStatus extends Model
{
protected $fillable = ['status', 'lead_id'];
}
This works fine, now I'm trying to get all Leads based on the 'status' of the last LeadStatus.
I've tried a few combinations with no success.
if ($search['status']) {
$builder = $builder
->whereHas('statuses', function($q) use ($search){
$q = $q->latest()->limit(1);
$q->where('status', $search['status']);
});
}
if ($search['status']) {
$builder = $builder
->whereHas('status', function($q) use ($search){
$q = $q->latest()->Where('status', $search['status']);
});
}
Has anybody done this with Eloquent? Do I need to write some raw SQL queries?
EDIT 1: I'll try to explain again :D
In my database, the status of a lead is not a 1 to 1 relation. That is because I want to have a historic list of all the statuses which a Lead has had.
That means that when a Lead is created, the first LeadStatus is created with the status of 'new' and the current date.
If a salesman comes in, he can change the status of the lead, but this DOES NOT update the previous LeadStatus, instead it creates a new related LeadStatus with the current date and status of 'open'.
This way I can see that a Lead was created on 05/05/2018 and that it changed to the status 'open' on 07/05/2018.
Now I'm trying to write a query using eloquent, which only takes in count the LATEST status related to a Lead.
In the previous example, if I filter by Lead with status 'new', this Lead should not appear as it has a status of 'open' by now.
Hope this helps
Try this:
Lead::select('leads.*')
->join('lead_statuses', 'leads.id', 'lead_statuses.lead_id')
->where('lead_statuses.status', $search['status'])
->where('created_at', function($query) {
$query->selectRaw('max(created_at)')
->from('lead_statuses')
->whereColumn('lead_id', 'leads.id');
})->get();
A solution using the primary key (by Borjante):
$builder->where('lead_statuses.id', function($query) {
$query->select('id')
->from('lead_statuses')
->whereColumn('lead_id', 'leads.id')
->orderBy('created_at', 'desc')
->limit(1);
});
I had this same problem and posted my solution here but I think it's worth re-posting as it improves on the re-usability. It's the same idea as the accepted answer but avoids using joins, which can cause issues if you want to eager load relations or use it in a scope.
The first step involves adding a macro to the query Builder in the AppServiceProvider.
use Illuminate\Database\Query\Builder;
Builder::macro('whereLatestRelation', function ($table, $parentRelatedColumn)
{
return $this->where($table . '.id', function ($sub) use ($table, $parentRelatedColumn) {
$sub->select('id')
->from($table . ' AS other')
->whereColumn('other.' . $parentRelatedColumn, $table . '.' . $parentRelatedColumn)
->latest()
->take(1);
});
});
This basically makes the sub-query part of the accepted answer more generic, allowing you to specify the join table and the column they join on. It also uses the latest() function to avoid referencing the created_at column directly. It assumes the other column is an 'id' column, so it can be improved further. To use this you'd then be able to do:
$status = $search['status'];
Lead::whereHas('statuses', function ($q) use ($status) {
$q->where('status', $userId)
->whereLatestRelation((new LeadStatus)->getTable(), 'lead_id');
});
It's the same logic as the accepted answer, but a bit easier to re-use. It will, however, be a little slower, but that should be worth the re-usability.
If I understand it correctly you need / want to get all Leads with a specific status.
So you probably should do something like this:
// In your Modal
public function getLeadById($statusId)
{
return Lead::where('status', $statusId)->get();
// you could of course extend this and do something like this:
// return Lead::where('status', $statusId)->limit()....->get();
}
Basically I am doing a where and returning every lead with a specific id.
You can then use this function in your controller like this:
Lead::getLeadById(1)

how to extend/specify Yii2's relations?

I'm wondering about performance and thinking about how to narrow down my SQL queries. Therefore I have the following question:
Let's say we have following relations:
public function getOrders() {
return $this->hasMany(Orders::className(), ['fk_product_id' => 'id']);
}
public function getOrdersByDate() {
return $this->hasMany(Orders::className(), ['fk_product_id' => 'id'])->orderBy('date');
}
So the question is, is there a way to connect these two relations without having to make extra SQL query when I call for $model->ordersByDate? I know I could go through the first relation with foreach() and sort it to get the result of 2nd relation, but that doesn't seem very wise.
You can use ->with() to get all the information at once
Model::find()->with('orders')->with('ordersByDate')->all()
and then reference them with $model->orders
Or you can get the orders once with getOrders and sort/find in the array later.
Unless you could be more specific in what kind of queries you're running, and you want a solution you can adapt to several different issues, you just have to read up :)
http://www.yiiframework.com/doc-2.0/yii-helpers-basearrayhelper.html
check the BaseArrayHelper::index()
http://www.w3schools.com/php/php_arrays_sort.asp
http://php.net/manual/en/function.array-search.php
http://php.net/manual/en/function.ksort.php
$fruits = array("d"=>"lemon", "a"=>"orange", "b"=>"banana", "c"=>"apple");
ksort($fruits);
foreach ($fruits as $key => $val) {
echo "$key = $val\n";
}

Yii2 Model search query

How can I add where condition to my Articles model so that slug(From category model) is equal to $slug?
And this is a function that Gii generated:
public function getCategory()
{
return $this->hasOne(Categories::className(), ['id' => 'category_id']);
}
Here's my code:
public function specificItems($slug)
{
$query = Articles::find()->with('category');
$countQuery = clone $query;
$pages = new Pagination(['totalCount' => $countQuery->count(),'pageSize' => 12]);
$articles = $query->offset($pages->offset)
->limit($pages->limit)
->all();
return ['articles' => $articles,'pages' => $pages];
}
Your SQL query should contain columns from both article and category table. For that you need to use joinWith().
$result = Articles::find()
->joinWith('category')
->andWhere(['category.slug' => $slug])
->all();
Where 'category' is then name of your category table.
However, in your code you deviate from certain best practices. I would recommend the following:
Have both table name and model class in singular (Article and article). A relation can be in plural, like getCategories if an article has multiple categories.
Avoid functions that return result sets. Better return ActiveQuery class. If you have a query object, all you need to get the actual models is ->all(). However, you can further manipulate this object, add more conditions, change result format (->asArray()) and other useful stuff. Returning array of results does not allow that.
Consider extending ActiveQuery class into ArticleQuery and implementing conditions there. You'll then be able to do things like Article::find()->joinWith('category')->byCategorySlug('foo')->all().

Yii JSON with relations

How I can return object with all relations(ans sub objects relations?).
Now I use EJsonBehavior but it returns only first level relations, not sub related objects.
My source code:
$order = Order::model()->findByPk($_GET['id']);
echo $order->toJSON();
Yii::app()->end();
The eager loading approach retrieves the related AR instances together with the main AR instance(s). This is accomplished by using the with() method together with one of the find or findAll methods in AR. For example,
$posts=Post::model()->with('author')->findAll();
The above code will return an array of Post instances. Unlike the lazy approach, the author property in each Post instance is already populated with the related User instance before we access the property. Instead of executing a join query for each post, the eager loading approach brings back all posts together with their authors in a single join query!
We can specify multiple relationship names in the with() method and the eager loading approach will bring them back all in one shot. For example, the following code will bring back posts together with their authors and categories:
$posts=Post::model()->with('author','categories')->findAll();
We can also do nested eager loading. Instead of a list of relationship names, we pass in a hierarchical representation of relationship names to the with() method, like the following,
$posts=Post::model()->with(
'author.profile',
'author.posts',
'categories')->findAll();
The above example will bring back all posts together with their author and categories. It will also bring back each author's profile and posts.
Eager loading may also be executed by specifying the CDbCriteria::with property, like the following:
$criteria=new CDbCriteria;
$criteria->with=array(
'author.profile',
'author.posts',
'categories',
);
$posts=Post::model()->findAll($criteria);
or
$posts=Post::model()->findAll(array(
'with'=>array(
'author.profile',
'author.posts',
'categories',
)
);
I found the solution for that. you can use $row->attributes to create data
$magazines = Magazines::model()->with('articles')->findAll();
$arr = array();
$i = 0;
foreach($magazines as $mag)
{
$arr[$i] = $mag->attributes;
$arr[$i]['articles']=array();
$j=0;
foreach($mag->articles as $article){
$arr[$i]['articles'][$j]=$article->attributes;
$j++;
}
$i++;
}
print CJSON::encode(array(
'code' => 1001,
'magazines' => $arr,
));
This is the best piece of code I found after a long time search to meet this requirement.
This will work like Charm.
protected function renderJson($o) {
//header('Content-type: application/json');
// if it's an array, call getAttributesDeep for each record
if (is_array($o)) {
$data = array();
foreach ($o as $record) {
array_push($data, $this->getAttributes($record));
}
echo CJSON::encode($data);
} else {
// otherwise just do it on the passed-in object
echo CJSON::encode($this->getAttributes($o));
}
// this just prevents any other Yii code from being output
foreach (Yii::app()->log->routes as $route) {
if ($route instanceof CWebLogRoute) {
$route->enabled = false; // disable any weblogroutes
}
}
Yii::app()->end();
}
protected function getAttributes($o) {
// get the attributes and relations
$data = $o->attributes;
$relations = $o->relations();
foreach (array_keys($relations) as $r) {
// for each relation, if it has the data and it isn't nul/
if ($o->hasRelated($r) && $o->getRelated($r) != null) {
// add this to the attributes structure, recursively calling
// this function to get any of the child's relations
$data[$r] = $this->getAttributes($o->getRelated($r));
}
}
return $data;
}