Load laravel eloquent model withCount of related model - php

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();

Related

withExists() or withCount() for nested relationship (Many To Many and One To Many) in Laravel Eloquent

I have the following relationships in models:
Product.php
public function skus()
{
return $this->belongsToMany(Sku::class);
}
Sku.php
public function prices()
{
return $this->hasMany(Price::class);
}
I need to get an attribute indicating whether a product has at least one price or not (in the extreme case, just the number of prices).
Product::withExists('sku.prices') or Product::withCount('sku.prices')
I know about this repository https://github.com/staudenmeir/belongs-to-through, but I prefer to use complex query once
UPDATE: I have already written a sql query for this purpose, but I don't know how to do it in Laravel:
SELECT
*,
EXISTS (SELECT
*
FROM prices
INNER JOIN skus
ON prices.sku_id = skus.id
INNER JOIN product_sku
ON skus.id = product_sku.sku_id
WHERE products.id = product_sku.product_id
) AS prices_exists
FROM products
Here you can get at least one record
$skuPrice = Sku::with('prices')
->has('prices', '>=', 1)
->withCount('prices')
->get();

Laravel Eloquent has many through variation, remove duplicates

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();
{

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;

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();

Counting related rows in a child table

I have been trying to do some queries and getting a count on related tables using eloquent.
Tables:
requests
contact (belongs to requests)
history (belongs to contact)
As such X number of requests each have Y number of contacts which in term each have Z number of histories
Using sql I can do something like this to get all the counts.
SELECT
id,
(
SELECT count(contact.id)
FROM contact
WHERE contact.requests_id = requests.id
) AS n_contact,
(
SELECT count(history.id)
FROM contact INNER JOIN history ON (history.contact_id = contact.id)
WHERE contact.requests_id = requests.id
) AS n_history
FROM requests;
But I am a bit lost when using eloquent to build queries. If for instance I was selecting all contacts for a given request at what point would I join/count the history? Or do I need to add in some accessor's into the relevant Models for these 3 tables?
public function getAllContacts($id) {
return Requests::where('requests.id', '=', $id)
->join('requests', 'contact.requests_id', '=', 'requests.id')
->select('contact.*', 'requests.name');
->get();
}
Thanks in advance for any help.
You can use helper relation for this, if you'd like to use Eloquent instead of manual joins:
// Request model
public function contactsCount()
{
return $this->hasOne('Contact')->selectRaw('request_id, count(*) as aggregate')->groupBy('request_id');
}
public function getContactsCountAttribute()
{
if ( ! array_key_exists('contactsCount', $this->relations)) $this->load('contactsCount');
return $this->getRelation('contactsCount')->aggregate;
}
The same would go for Contact model towards History model.
For counting far relation (Request -> History) you can use hasManyThrough relation with a little adjustment.
This way you can eager load those aggregates for multiple models without n+1 issue, nice and easy:
$requests = Request::with('contactsCount', 'contacts.historyCount')->get();
// 1 query for reuqests, 1 query for contacts count and 2 queries for contacts.historyCount
// example output
$requests->first()->contactsCount; // 17
$requests->first()->contacts->first()->historyCount; // 5
/* Make Relation in the Request Model */
public function contacts()
{
return $this->hasMany('App\Model\Contact', 'request_id', 'id');
}
/* use withCount() to get the total numner of contacts */
public function getAllContacts($id) {
return Requests::with('contacts')
->withCount('contacts')
->find($id);
}

Categories