Laravel Eloquent has many through variation, remove duplicates - php

I've been trying to get a variation of the Laravel Eloquent 'has many through' relation working.
I have 3 models: Invoice, InvoiceLine and Order; I would like to retrieve a distinct set of all Orders of a given Invoice through its InvoiceLines.
Invoice has 1 or more InvoiceLines; InvoiceLine has exactly 1 Invoice; InvoiceLine has zero or 1 Order; Order has zero or more InvoiceLines
Below is a simplified version of the tables:
Invoice
id
InvoiceLine
invoice_id
orderid
Order
orderid
First I thought to use the default Has Many Through relationship but that won't work because my tables are different:
Then I thought to use the Many-To-Many (belongsToMany) relation:
class Invoice {
public function orders(): BelongsToMany {
return $this->belongsToMany(
Order::class,
'invoice_line',
'invoice_id',
'orderid'
);
}
}
The above code works for getting all orders, but the list is full of duplicates.
How can I setup this Invoice --> Order relation using Laravel 5.6 Eloquent ORM but have it so that the collection of Orders does not have any duplicates?
If this is not possible in a Eloquent ORM relationship definition, then how can I query these models in a different way so that I still have a relational object oriented collection of Invoices with its distinct set of related Orders?

Not sure if this can be done in Eloquent builder. Maybe something like this:
public function orders(): BelongsToMany {
return $this->belongsToMany(
Order::class,
'invoice_line',
'invoice_id',
'orderid'
)->distinct();
}
But you can also do:
// class Invoice
public function getRelatedOrders ()
{
$orderIdList = $this->invoiceLines->pluck('order_id')->unique();
return Order::whereIn('id', $orderIdList)->get();
{

Related

How to Join Eloquent and Query Builder in one query statement in laravel

I have 2 queries that needed to join 1st is eloquent and 2nd is query builder,
1st Query
$products = Product::all();
2nd Query
$inventory = DB::table('product_warehouse')
->where('product_id', $product_id)
->where('warehouse_id', $warehouse_id)
->first();
How to merge this 2 queries into elouquent way ?
From your usage of the query builder it seems like you have an intermediate table to store which product to which warehouse exist, but if it is a one to many relationship you should not have that table, instead in your products table you should have a warehouse_id which will reference the id on the warehouses table, as you said the relationship is one to many, not many to many.
So in your Warehouse model you can add:
public function products()
{
return $this->hasMany(Product::class);
}
And in your Product model:
public function warehouse()
{
return $this->belongsTo(Warehouse::class);
}
Based on your table name, you might need to set the $table in your warehouse model to match that:
protected $table = 'product_warehouse';
Then you have many ways to fetch it, one of which is :
Warehouse::find($warehouse_id)->products;
// or
Warehouse::with('products')->where('id', $warehouse_id)->get();
// to get the warehouse to which the product belongs to
Product::find($product_id)->warehouse;

Sum pivot table field on Eloquent many-to-many relationship

I have an orders table, an items table, and a pivot table called item_order which has two custom fields (price, quantity). The relationship between Order and Item is belongsToMany. I'm trying to return the count of all items with an id of 1, where the parent Order->status == 'received'. For the life of me, I can't figure out how to do this.
class Order extends Model
{
public function items()
{
return $this->belongsToMany(Item::class)->withPivot('price', 'quantity');
}
}
class Item extends Model
{
public function orders()
{
return $this->belongsToMany(Order::class)->withPivot('price', 'quantity');
}
}
Try this:
$total_quantity = Item::find(1) // getting the Item
->orders() // entering the relationship
->with('items') // eager loading related data
->where('status', '=', 'received') // constraining the relationship
->get() // getting the results: Collection of Order
->sum('pivot.quantity'); // sum the pivot field to return a single value.
The strategy here is to find the desired Item to then get the related Orders to this Item that has a 'received' status, to finally sum the pivot attribute in order to get a single value.
It should work.
Considering you know the id of the item, most performant way would be to query item_order table directly. I would create a pivot model for ItemOrder and define the belongsTo(Order::class) relationship and do this:
$sum = ItemOrder::where('item_id', $someItemId)
->whereHas('order', function ($q) {
return $q->where('status', 'received');
})
->sum('quantity');

Load laravel eloquent model withCount of related model

Given I have two eloquent models: Booking and Customer.
When I list all Bookings along with the respective Customer, I also want to show the amount of Bookings the respective customer has in total (count of this Booking + all other bookings).
Example output:
Booking1: Customer A (has 20 Bookings total)
Booking2: Customer B (has 10 Booking total)
Booking3: Customer C (VIP: has 100 Bookings total)
In order to avoid the n+1 problem (one additional query per booking while showing this), I'd like to eager load the bookingsCount for the Customer.
The relations are:
Booking: public function customer() { return $this->belongsTo(Customer::class) }
Customer: public function bookings() { return $this->hasMany(Booking::class) }
Example for querying the Bookings with eager loading
Working, but without eager loading of the bookingsCount:
Booking::whereNotCancelled()->with('customer')->get();
Not working:
Booking::whereNotCancelled()->with('customer')->withCount('customer.bookings')->get();
I learned, that you cannot use withCount on fields of related models, but you can create a hasManyThrough relation and call withCount on that relation, e.g. Booking::whereNotCancelled()->withCount('customerBookings'); (see accepted answer here).
However: This doesn't work. I guess, it's because a Booking belongsTo a Customer and a Customer hasMany Bookings.
Here's the hasManyThrough relation of class Booking
public function customerBookings()
{
// return the bookings of this booking's customer
return $this->hasManyThrough(Booking::class, Customer::class);
}
Here's the failing test for hasManyThrough
/**
* #test
*/
public function it_has_a_relationship_to_the_customers_bookings()
{
// Given we have a booking
$booking = factory(Booking::class)->create();
// And this booking's customer has other bookings
$other = factory(Booking::class,2)->create(['customer_id' => $booking->customer->id]);
// Then we expect the booking to query all bookings of the customer
$this->assertEquals(3, Booking::find($booking->id)->customerBookings()->count());
}
Reported error
no such column: customers.booking_id (SQL: select count(*) as aggregate from "bookings" inner join "customers" on "customers"."id" = "bookings"."customer_id" where "customers"."booking_id" = efe51792-2e9a-4ec0-ae9b-a52f33167b66)
No surprise. There is no such column customer.booking_id.
The Question
Is the intended behavior even possible in this case? If so, how would I eager load the booking's customer's total count of bookings?
Try this:
public function customer() {
return $this->belongsTo(Customer::class)->withCount('bookings');
}
Booking::whereNotCancelled()->with('customer')->get();

Join three table by laravel eloquent model which has only one relation with each other

How to join the three table in Laravel Eloquent model. My table structure is like. It is easy with raw(mysql with left join) query to get the desired result, but it is possible to get via Eloquent.
Customer table
id
title
name
Order table(has customer id)
id
customer_id
Hour table (has order id)
id
order_id
From Hour model I want to get the Customer Details and Order Details. Now I am able to get the Order details like below.
public function order()
{
return $this->belongsTo('App\Models\Order', 'order_id', 'id');
}
How to get the Customer Details?
I tried like below, but not get success
1.return $this->hasManyThrough('App\Models\Customer','App\Models\Order','customer_id','id');
2. return $this->hasManyThrough('App\Models\Customer','App\Models\Order','customer_id','order_id');
Edit 1:
class HourLogging extends Model
{
public function order()
{
return $this->belongsTo('App\Models\Order', 'order_id', 'id');
}
$timeoverview = HourLogging::select('hour_logging.*')->whereRaw("hour_logging.date BETWEEN '".$start_date."' AND '".$end_date."'")->orderBy('date','asc');
$timeoverview->with('order.customer');
return $timeoverview->get();
}
class Order extends Model {
public function customer() {
return $this->belongsTo('App\Models\Customer');
}
}
hasManyThrough works the opposite way to what you're trying to achieve. It would let you get Hour models from your Customer model easily.
In order to get what you need you need to define the relations that go the other way - from Hour to Order to Customer. Add the following relations to your models:
class Hour extends Model {
public function order() {
return $this->belongsTo('App\Models\Order');
}
}
class Order extends Model {
public function customer() {
return $this->belongsTo('App\Models\Customer');
}
}
With those relations in place you should be able to access Order data with $hour->order and Customer data with $hour->order->customer.
One thing to remember: Eloquent offers possibility to eagerly load related data to avoid additional queries. It makes sense to do it when you fetch multiple Hour records at once. You an load all Hour records with related Order and Customer data with the following:
$hours = Hour::with('order.customer')->get();

Laravel Eager Load and Group Multiple Joins on Pivot

I have a Pivot table thats used to join two other tables that have many relations per hotel_id. Is there a way I can eagerload the relationship that pulls the results for both tables in one relationship? The raw SQL query, works correctly but when using belongsToMany the order is off.
Amenities Pivot Table
id
hotel_id
distance_id
type_id
Distance Table
id
name
Type Table
id
name
RAW Query (This works fine)
SELECT * FROM amenities a
LEFT JOIN distance d ON a.distance_id = d.id
LEFT JOIN type t ON a.type_id = t.id WHERE a.hotel_id = ?
My "Hotels" Model is using belongsToMany like so
public function distance() {
return $this->belongsToMany('Distance', 'amenities', 'hotel_id', 'distance_id');
}
public function type() {
return $this->belongsToMany('Type', 'amenities', 'hotel_id', 'type_id');
}
This outputs the collection, but they are not grouped correctly. I need to loop these into select fields side by side as entered in the pivot table, so a user can select a "type" and the "distance", but the order is off when using the collection. The raw query above outputs correctly.
Hotels::where('id','=','200')->with('distance', 'type')->take(5)->get();
Ok Solved it. So apparently you can use orderBy on your pivot table. Incase anyone else has this issue this is what I did on both relationships.
public function distance() {
return $this->belongsToMany('Distance', 'amenities', 'hotel_id', 'distance_id')->withPivot('id')->orderBy('pivot_id','desc');
}
public function type() {
return $this->belongsToMany('Type', 'amenities', 'hotel_id', 'type_id')->withPivot('id')->orderBy('pivot_id','desc');
}
It's not really a great practice to include other query building steps in the relationship methods on your models. The relationship method should just define the relationship, nothing else. A cleaner method is to apply eager load constraints. (scroll down a bit) Consider the following.
Hotels::where('id', 200)->with(array(
'distance' => function ($query)
{
$query->withPivot('id')->orderBy('pivot_id','desc');
},
'type' => function ($query)
{
$query->withPivot('id')->orderBy('pivot_id','desc');
},
))->take(5)->get();
If you find that you are eagerly loading this relationship in this way often, consider using scopes to keep things DRY. The end result will allow you to do something like this.
Hotels::where('id', 200)->withOrderedDistance()->withOrderedType()->take(5)->get();
P.S. Your models should be singular. Hotel, not Hotels. The model represents a single record.
Solved by using ->withPivot('id')->orderBy('pivot_id','desc');
Posted answer in the question.

Categories