Laravel eloquent 5.3 Eager/Lazy loading related models with multiple limit - php

I'm leveraging Laravel's eloquent by Lazy/Eager Loading and have come across an interesting issue.
Description:
An account has many groups, each with a priority number
All groups Contain Items in them
The issue:
I need to manually set a limit to the number of returned items of each group
The difference here is that the limit number changes based on each groups priority level
So, for example, the eloquent "select" statement would get all three groups but limit the number of items returned in group 1 to just 3 items, group 2 to just 8 items, group 3 to 17 items.
What I've tried to do:
Code in Controller to get records
return Account::with([
'group_list.item'
])->where('group_id', $my->group_id)
->orderBy('group_priority_num', 'ASC')
->take(3) <----Gets three groups
->get(['group_priority_num','group_title','group_id']);
Group Model
public function group_list() {
// Example
$this->number = [4 , 7, 15];
return $this->group()->limit($this->number)->groupBy('user_group_id_fk');
}
This only returns 1 record or none at all, so I'm currently confused about how to approach this correctly. Can someone lend me some guidance on this?
Any assistance is greatly appreciated, Thank you in advance.
Cheers!

Eager loading is implemented via a second query using WHERE foreign_key_column IN (list of primary model IDs). With such a query it is impossible to set a limit on per model basis unless there's some other condition that the results can be further filtered by.
You have 3 options:
Let Eloquent eager load everything and filter the results in PHP according to your needs.
Perform the eager loading your self by fetching only the primary model followed by separate queries for each model's relationships with their respective limits.
Add a flag column to your groups table that would let you mark the rows that should be returned for each group.

Related

Laravel Eager Loading, only when ids are available

I have a table like this:
payments
-id
-client_id
-vendor_id
-product_id
-item_id
-..._id
Using Laravel's eager loading, I can do this ->load('client', 'vendor' ... ) and load the relationships for all the returned rows of payments. But I've noticed that for example when there are no values in payments for vendor_id the eager loading query for the "vendor" relationship still happens, as below:
select * from `vendors` where `vendors`.`id` in ('')
The in ('') I understand should be filled with the values for vendor_id in the payments table, but since there were none the above query should not happen, right?! Is that a bug in Laravel?
The problem is that i got like 20 [x]_id columns in this payments table, and in each row only a couple (varies for each row) [x]_id columns have values, and I don't want to make 20 extra eager-loading queries, just the ones needed based on the actual existence of these ids!
When you are doing lazy eager loading, laravel does not know if records are present or not. If not present laravel returns empty collection.
If you want to filter out at the start itself to show only payments having vendors and clients etc, you can do :
$payments : Payment::whereHas('vendors')->whereHas('clients')->get();
Above will give you only payments which contain at-least one vendor and 1 client.
Also, you are using load() which is lazy-eager loading. It is designed so that you can get the primary collection first and later when and if you need, then only get the relation collection.
Check for with() if you do not want to lazy-eager load.
Also, if you want to avoid eager loading, you can go for join but again then you wont be using defined relations.

Laravel Has Many Trough with multiple levels

I have a very specific query that I don't know how to get in Eloquent
I have the following tables
Orders, OrderInvoice,OrderPayment
So each Order has many OrderInvoices and each OrderInvoice has many OrderPayments
Then I have the table turns which has many payments
So what I want is to get all the orders related to a specific turn
I know how to get all the invoices:
$this->belongsToMany('OrderInvoice','orders_payments','turn_id','invoice_id');
But I need the next level and get the Orders,
How can I achieve that in eloquent?
Thank you so much!
EDIT: Tables structure
Orders
id
OrderInvoice
id
order_id
OrderPayment
id
invoice_id
turn_id
Turns
id
Most straightforward:
// load related collections
$turn->load('invoices.orders');
// then
$turn->invoices; // collection of invoices, for each you can do this:
$turn->invoices->first()->orders; // collection of orders for an invoice
If you want to get single collection of orders for given turn, then you need this trick, which is the easiest way (no joins etc), but not the best in terms of performance for sure:
// store orders in a separate variable
$turn->load(['invoices.orders' => function ($q) use (&$orders) {
$orders = $q->get()->unique();
}]);
// then
$orders; // single collection of all the orders related to the given turn
// also still accessible as usually:
$orders->invoices;
$orders->invoices->first()->orders;

User roster, joining a table but loosing data

I need some help with a query (using Laravel framework). I'm building a user roster with a few columns that incude the users ratings. Basically, it selects all the active users who have initials and joins the user ratings table to select the ratings for the respective users. The where_in is to select only specific ratings. My issue is on the roster, it only selects one rating, rather than all of them (if the user has more than one rating). I've also tried without the group_by, but then the users are duplicated on the table depending on the number of ratings they have (example: if the user has 2 ratings, their row is displayed twice on the roster).
Query:
$users = DB::table('users')
->where('users.initials', '!=', '')
->left_join('user_ratings', 'user_ratings.uid', '=', 'users.uid')
->where_in('users_ratings.rid', array(6,17,21,20))
->group_by('users.uid')
->order_by('users.name')
->get();
Tables:
Users
=======
uid name
1 John
2 Jeff
3 Cathy
Ratings
======
rid uid
1 1
2 1
2 2
3 1
4 3
The problem is when you do a left_join, the result you are going to get is multiple rows. So without the group_by clause it will return all the results you want in rows, not columns, and with the group_by it will just return the first rating (which is the expected behavior).
I would suggest you just use Eloquent ORM and set up the models and their relationships (it's a lot easier and cleaner). I'm guessing user_rating is a many-to-many pivot table? In which case you would have two models User and Rating and their relationship will be has_many_and_belongs_to. Also, the naming conventions in laravel have the pivot table in alphabetical order, so it will be called "rating_user". Look here for how to set up relationships: http://laravel.com/docs/database/eloquent#relationships
Once the models with their relationships are setup, what I would do is
$users = User::where('initials', '!=', '')->order_by('name')->get();
Then,
foreach($users as $user) {
echo "Ratings for " .$user->name . ": ";
$ratings = $user->ratings()->pivot()->where_in('rid', array(6,17,21,20))->get();
foreach($ratings as $rating) {
however you want to display the ratings here...
}
}
This may not be the most efficient way, but it should get the job done given my assumptions are true.
A quick look at the documentation reveals that the Fluent Query Builder can only do what MySQL can do, and MySQL cannot return an array to the client.
Take a look at this question for your alternatives.
If you want Laravel to fetch the ratings for you, you need to build a model.

Laravel eloquent counting a relation

I'm new to laravel and eloquent and I'm not sure if this is even possible. but I have 2 tables with a one to many relationship. One is "locations" and one is "users". One location can have many users.
So if I wanted to get all locations with all users I would just do this:
Location::with("users")->get();
But I also want to know how many users each location has, I tried doing this
Location::with("users")->count("users")->get();
But that didn't work.
The n+1 issue that was mentioned doesn't occur if you use eager loading.
$locations = Location::with('users')->get();
$locations->users()->count();
This should result in three queries, no matter how many users or locations you have.
count query against the location model
select * from locations
select * from users where in
The confusion arises from the fact that this also works:
$locations = Location::all();
$locations->users()->count();
But in this case, it does query once for each location.
See the docs for more info:
http://laravel.com/docs/eloquent#eager-loading
You should be using just withCount but I guess it wasn't available back in 2012.
So here's how it should look like:
Location::with("users")->withCount("users")->get();
And you will have an users_count attribute available on you object.
Read the docs for more details: https://laravel.com/docs/5.5/eloquent-relationships#querying-relations (scroll down a bit and you'll see Counting Related Models)
You need to call the count method on each Location record to get users count per location, here is an example:
foreach(Location::get() as $location) // or foreach(Location::with("users")->get() as $location)
{
echo $location->users()->count();
}
This should solve your problem and give you the number of users per each location. You can add a check in the loop to ignore locations with users' count = 0

MongoDB Indexing vs Array Implementation for our specific application

Here is the issue. We are working with MongoDB-PHP.
In our application, we have many user groups where users can make posts. Presently we are maintaining the post ids these groups in the document of that group in array format. So that, when we need to grab first 10 posts we can grab them from the array using slice operation.
Eg: Case 1
collection posts: //this collection stores all the posts of various groups
{
{"_id":"1","post_text":"....",...}
{"_id":"2","post_text":"....",...}
} `
collection groups: //this collection contains documents for each group
{
{
"_id":"1"
"name":"Group ABC",
"post_ids":{"1","2"...."100"}
//1,2..100 represents MongoIDs of corresponding posts of this group
//so i can slice first 10 posts of this group when someone visits this page
}
}
`
In contrast to storing these post ids in document of the group, if we use indexing on group id and store that in posts collection.
Eg: Case 2
collection posts
{
{"_id":"1","group_id":"1","post_text":"....",...}
{"_id":"2","group_id":"2","post_text":"....",...}
}
Also note that in Case 1 we do not have to apply any sorting operations as array elements are pushed in order while in Case 2 we will have to apply sort(by timestamp criteria) after the find operation, which would read all documents from memory and then apply sorting on them.
Whose performance would be better taking into consideration that indexes would be stored in RAM ?
Please let me know if the issue is not clear from this question.
Doing one query (case #2) would be faster than doing two queries. Also, making documents bigger (e.g., appending new posts to post_ids in #1) is a fairly slow operation.

Categories