Laravel hasOne Eloquent Relationship // Multiple queries instead of one? - laravel

I'm testing the speed of my application and I have multiple hasOne relationships.
For example, an Order has one status ( order is pending , order is shipped etc)
Order Model
<?php
namespace App\Models;
use App\Models\OrderStatus;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Order extends Model
{
protected $primaryKey = 'order_id';
public function status() {
return $this->hasOne( OrderStatus::class , 'id' , 'order_status_id' );
}
}
OrderStatus Model
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class OrderStatus extends Model
{
protected $primaryKey = 'id';
protected $table = 'orders_status';
}
OrderController Both examples make two queries to the database
public function show ( $order_id ) {
Order::with('status')->where('order_id' , $order_id)->firstOrFail(),
}
public function otherShowExample ( Order $order ) {
$order->load('status');
return view( 'order.show' , [
'order' => $order,
'order_status' => $order->status
] );
}
With Join is just one query to the database
public function showOneQuery ( $order_id ) {
$order = Order::where('order_id' , $order_id)
->select('orders.*' , 'orders_status.orders_status_name' )
->join('orders_status' , 'orders_status.id' , '=' , 'orders.order_status_id')
->firstOrFail();
}
DB::listen for the OrderController#show and OrderController#otherShowExample
select * from `orders` where `order_id` = ? limit 1
select * from `orders_status` where `orders_status`.`id` = ? and `orders_status`.`id` is not null limit 1
In the OrderController#show, when trying to show to the user only one record, with the OrderStatus relationship, using the facade DB::listen I can see that are two queries made.
The question is : Is this the normal behavior of the hasOne relationship ? Is not better using the join() ? I'm doing something wrong ?

Yes. It's normal behavior of HasOne relationship (and any other).
Methods like with and load realize eager loading.
So, first of all, Eloquent will get an item or a collection of items from database. Then it will load relationships.

Yes, It's normal. In Laravel relationship, First laravel fetch a main model object and after that for each relationship it will fire individual query to load them. It's no matter with which type of relation we used and number of records we are processing. When we use a join with Model, it will fire a single query but a result set will be considered as a Order model instance, we can't identify a relationship data in that Order instance and if we try to check $order->status then it will fire a query for that.
In relationship, query will be simple one so it's easy to do indexing in a table but in a join query sometime it's difficult to do indexing to optimize a query and in that case, firing more number of queries will work faster then individual query. For multiple queries laravel will not initialize connection with DB as laravel initialize one time per request so it will not add that amount of extra time to initialize a connection.

Related

Laravel eloquent for four tables

I'm new to Laravel. I am developing a project. and in this project I have 4 tables related to each other
-Users
-Orders
-OrderParcels
-Situations
When listing the parcels of an order, I want to get the information of that order only once, the user information of that order once again, and list the parcels as a table under it. so far everything ok. but I also want to display the status of the parcels listed in the table as names. I couldn't add the 4th table to the query. do you have a suggestion? I'm putting pictures that explain the structure below.
My current working code is
$orderParcels = Orders::whereId($id)
->with('parcels')
->with('users:id,name')
->first();
and my 'orders' model has method
public function parcels(){
return $this->hasMany(OrderParcels::class);
}
public function users(){
return $this->hasOne(User::class,'id','affixer_id');
}
Note[edit]: I already know how to connect like this
$orderParcels = DB::table('order_parcels as op')
->leftjoin('orders as o','op.orders_id','o.id')
->leftjoin('users as u','o.affixer_id','u.id')
->leftjoin('situations as s','op.status','s.id')
->select('op.*','o.*','u.name','s.situations_name')
->where('op.orders_id',$id)->get();
but this is not working for me, for each parcels record it returns me orders and user info. I want once orders info and once user info.
Laravel provides an elegant way to manage relations between models. In your situation, the first step is to create all relations described in your schema :
1. Model Order
class User extends Model {
public function parcels()
{
return $this->hasMany(OrderParcels::class);
}
public function users()
{
return $this->hasOne(User::class,'id','affixer_id');
}
}
2. Model Parcel
class Parcel extends Model {
public function situations()
{
return $this->hasOne(Situation::class, ...);
}
}
Then, you can retrieve all desired informations simply like this :
// Retrieve all users of an order
$users = $order->users; // You get a Collection of User instances
// Retrieve all parcels of an order
$parcels = $order->parcels; // You get a Collection of User instances
// Retrieve the situation for a parcel
$situations = $parcel->situations // You get Situation instance
How it works ?
When you add a relation on your model, you can retrieve the result of this relation by using the property with the same name of the method. Laravel will automatically provide you those properties ! (e.g: parcels() method in your Order Model will generate $order->parcels property.
To finish, in this situation where you have nested relations (as describe in your schema), you should use with() method of your model to eager load all the nested relation of order model like this :
$orders = Orders::with(['users', 'parcels', 'parcels.situations'])->find($id)
I encourage you to read those stubs of Laravel documentation :
Define model relations
Eager loading
Laravel Collection
Good luck !
Use join to make a perfect relations between tables.
$output = Orders::join('users', 'users.id', '=', 'orders.user_id')
->join('order_parcels', 'order_parcels.id', '=', 'orders.parcel_id')
->join('situations', 'situation.id', '=', 'order_parcels.situation_id')
->select([
'orders.id AS order_id',
'users.id AS user_id',
'order.parcels.id AS parcel_id',
'and so on'
])
->where('some row', '=', 'some row or variable')->get();

Join Tables not working unless hard code ID

I forgot I have $weekends id already passed so I did this public function
show(Weekend $weekend)
{
$id = $weekend->id;
$teams = DB::table('weekends')
->join('weekend_team_members', 'weekend_team_members.weekends_id', '=', 'weekends.id')
->select('weekend_team_members.firstname','weekend_team_members.lastname','weekend_team_members.position')
->where('weekend_team_members.weekends_id', $id)
->get();
return view('pages.weekend')->with(['weekend' => $weekend,'teams'=> $teams]);
}
This code only works if I change ->where('weekend_team_members.weekends_id', '=','weekends.id') to
->where('weekend_team_members.weekends_id', '=',2) otherwise its an empty array. I am sure its probably something simple but I can't figure it out.
Here is my join statement
$teams = DB::table('weekends')
->join('weekend_team_members', 'weekend_team_members.weekends_id', '=', 'weekends.id')
->select('weekend_team_members.firstname','weekend_team_members.lastname','weekend_team_members.position')
->where('weekend_team_members.weekends_id', '=','weekends.id')
->get();
Weekend Model
<?php
namespace App\Models\Webmaster;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Weekend extends Model
{
use HasFactory;
protected $fillable = ['title','verse','songtitle','songvideo','image'];
public function weekendTeamMembers() {
return $this->hasMany(App\Models\Webmaster\WeekendTeamMember::class);
}
}
WeekendTeamMember Model
<?php
namespace App\Models\Webmaster;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class WeekendTeamMember extends Model
{
use HasFactory;
protected $fillable = ['firstname','lastname','position'];
public function weekend() {
return $this->belongsTo(App\Models\Webmaster\Weekend::class);
}
}
You're looking for the whereColumn() method: https://laravel.com/docs/8.x/queries#additional-where-clauses (scroll down a bit). Right now, you're searching against the literal string weekends.id:
SELECT * FROM `weekends` JOIN ... WHERE `weekend_team_members`.`weekends_id` = 'weekends.id'
The whereColumn() will ensure you're doing the correct SQL:
SELECT * FROM `weekends` JOIN ... WHERE `weekend_team_members`.`weekends_id` = `weekends`.`id`
Notice the difference between usage of single quotes and backticks:
= 'weekends.id'
= `weekends`.`id`
One is a string, one is a DB column, and will produce different results.
So, your final query would be:
$teams = DB::table('weekends')
->join('weekend_team_members', 'weekend_team_members.weekends_id', '=', 'weekends.id')
->select('weekend_team_members.firstname','weekend_team_members.lastname','weekend_team_members.position')
->whereColumn('weekend_team_members.weekends_id', 'weekends.id')
->get();
(Note: the = is implied, it's optional, but ->whereColumn('weekend_team_members.weekends_id', '=', 'weekends.id') is valid too)
First, weekend_team_members.weekends_id isn't going to work natively without defining a column. Laravel expects it to be weekend_id (singular).
Second, your where() clause doesn't accept a column name as an argument and, even if it did, it wouldn't do anything. The "where" you're describing there is already accomplished by your ->join() method (that's what an inner join is).
Third, what is the purpose of the join anyhow? You are only selecting columns from the weekend_team_members table.
If the intention is to filter out weekend_team_members that don't have a weekend_id, then you'd only need to do a ->whereNotNull('weekend_id'). If you wanted to select some columns from the weekends table, you'd be able to do that already from the join you've created.
If you were looking for team members belonging to a specific weekend, then you will need to pass it an ID (otherwise how would the query know which weekend you're looking for?).

Retrieve all columns from table A and some from table B using laravel eloquent

I am trying to retrieve the thumb image path by joining the images table to the listing table. As such, I have the following query in my controller.
$listings = Listing::select('listings.*, images.path as image_path')
->where('listings.ownerid', '=', $ownerid)
->leftJoin('images', 'listings.thumbId', '=', 'images.id')->get();
After testing out the function, the query fails since laravel interprets the query as
select `listings`.`*, images`.`path` as `image_path` from `listings` left join `images` on `listings`.`thumbId` = `images`.`id` where `listings`.`ownerid` = 1)
Notice the asterisk (*) is joined with the ", images" word making it '*, images'.
The query works fine without laravel's odd typo. How does one fix this issue?
You need to do one change in your query. You are passing raw select fields so you need to use selectRaw() instead of select(). Like
$listings = Listing::selectRaw('listings.*, images.path as image_path')
->where('listings.ownerid', '=', $ownerid)
->leftJoin('images', 'listings.thumbId', '=', 'images.id')->get();
check by try above query.
I suggest you to use Laravel Eloquent Relationships feature. Since your code above is more like Query Builder rather than Eloquent. Let's see the example bellow:
You will have 2 Models, 1 for each table (listings, images):
App\Listing Model:
<?php
...
use App\Image;
class Listing extends Eloquent {
...
protected $table = 'listings';
// define Eloquent Relationship of Listing model to Image model
public function image() {
return $this->belongsTo(Image::class, 'thumbId');
}
...
}
App\Image Model:
<?php
...
use App\Listing;
class Image extends Eloquent {
...
protected $table = 'images';
...
// define Eloquent Relationship of Image model to Listing model
public function listings() {
return $this->hasMany(Listing::class, 'thumbId');
}
}
So how to get the data?
// get all listing data
$listings = Listing::all();
// loop through the data
foreach ($listing as $listing) {
dump($listing->id);
// because we have define the relationship, we can access the related data of image
dump($listing->image->path);
// call $this->image will return related Image model
dump($listing->image);
}
You can see Laravel official documentation for more example and explanation.
Hope it helps.

Complicated Eloquent relationship using `Model::query`

I have a complicated relationship I'm trying to establish between two models.
The goal is to use $supplier->supply_orders to access the orders where the user supplies an item.
This throws: LogicException: Relationship method must return an object of type Illuminate\Database\Eloquent\Relations\Relation.
With the code I've got I can use $supplier->supply_orders()->get(), however, when I try to use it as a relationship it throws. Since this is a relationship I should be able to wrap it in a relationship, but how would I go about doing that?
Supplier Model:
class Supplier extends Model {
public function supply_orders() {
return Order::query()
->select('order.*')
->join('item_order', 'order.id', '=', 'item_order.order_id')
->join('item', 'item_order.item_id', '=', 'item.id')
->where('item.supplier_id', '=', $this->id);
}
}
~~~ A whole lot of back info that I don't think you need but might ~~~
sql tables:
supplier
- id
items:
- id
- supplier_id
item_order:
- id
- order_id
- item_id
orders:
- id
The other Eloquent Models:
class Item extends Model {
public function orders() {
return $this->belongsToMany('Order');
}
}
class Order extends Model {}
Example of how this should work:
$supplier = factory(Supplier::class)->create();
$item = factory(Item::class)->create([
'supplier_id' => $supplier->id,
]);
$order = factory(Order::class)->create();
$order->items()->attach($item);
$orders = $supplier->supply_orders // Throws LogicException
This throws: LogicException: Relationship method must return an object of type Illuminate\Database\Eloquent\Relations\Relation
Sounds like a hasManyThrough with a many to many relationship. Laravel has no inbuilt support for this but you can always go ahead and write your own relationship like this: https://laravel.io/forum/03-04-2014-hasmanythrough-with-many-to-many
If you dont want relationships you can always do something like:
Order::whereHas('items.supplier', function($query) use($supplier) {
$query->where('id', $supplier->id);
});
For this to work, you need to have a relationship function items in your Order model and a relationship function supplier in your item model
I believe the reason it throws a relationship error is that you haven't created an Eloquent relation for
$supplier->supply_orders.
Instead, Laravel looks at your supply_orders() as a method in the class, and thus can't figure out which table to use as the pivot. To get the base relationship to work within Eloquent, you'd need to create a new pivot table for the relationship between suppliers and orders something like:
suppliers
-id
orders
-id
order_supplier
-id
-order_id
-supplier_id
From here, Laravel will accept a simple many to many relationship between the two (this would not cause a failure):
Supplier Class:
/**
* Get all orders associated with this supplier via order_supplier table
*
* #return \Illuminate\Database\Eloquent\Relations\BelongsToMany
*/
public function orders(){
return $this->belongsToMany("\App\Order");
}
Now that the relationship is solid both between the suppliers and orders, as well as the orders and items, you can eager load the relationship in all directions. Where it gets complicated for your particular need with the current DB setup is that you have a 3rd parameter from the items table that is not a direct pivot. Without having to re-structure the DB, I think the easiest would be to load your suppliers and the relationships like normal:
$suppliers = Supplier::with('orders', function($query) {
$query->with('items');
});
From here you've got all the relationships loaded and can draw down the ones with the right item->ids in a follow-up to the $suppliers collection. There are quite a few ways to skin the cat (even including all in one query) now that you have the Eloquent relationship... but I tend to keep it a little more simple by breaking it into a few readable bits.
Hope this helps.

How to use Laravel Eloquent with complex queries

I am trying to setup Eloquent for a new API that we're working on. I am using relations in a model.
Some relations are complex and aren't really suitable for a quick chained Query Builder statement. For example, I am trying to return metrics and some of those metrics are complex. Such as counting the total clicks generated by a user (it's not just a simple COUNT(*)). Here is the code that I have now:
<?php
namespace App\Models\Eloquent;
use Illuminate\Support\Facades\DB;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Model;
class Affiliate extends Model
{
protected $table = 'user';
public function profile()
{
return $this->belongsTo('App\Models\Eloquent\Profile', 'id');
}
public static function clicks()
{
$sql = "SELECT
user_id,
COUNT(*) / SUM(dummy_metric) AS total_clicks
FROM clicks
WHERE something = 'true'
AND another_thing > 100 # dummy filter for example only
GROUP BY user_id";
$rows = DB::select(DB::raw($sql));
// (PSUEDO CODE) THIS IS WHAT I'D LIKE TO DO IDEALY
return $this->belongsTo($rows, user_id);
}
Possible? I'd like to be able to write our own queries without relying on Query Builder all of the time, but I still want to be able to join the relation to Eloquent.
Assuming you have Clicks model defined, find below the eloquent version of your query.
Click::select(DB:raw("user_id, COUNT(*) / SUM(dummy_metric) AS total_clicks"))
->where(/*condition*/)->groupBy("user_id")->get();
Note:
1) Where method accepts an array of conditions, so you can add more than one condition in same method. Reference
Update
I think thius should be your clicks() method:
public function clicks(){
return $this->hasMany('App\Models\Eloquent\Click',/*relevant ids*/);
}
And, now where you want a count of clicks(in controller for example), you can use following query:
$user = User::find("user_id")->with('clicks')->get();
$clicks = $user->clicks()->count();
To make it more efficient, refer to this article on Tweaking Eloquent relations
Update 2:
You can use Accessor function to retrieve total count
Add following 2 methods in User model.(change clicksCount string to anything you need )
public function clicksCount(){
return $this->hasOne('App\Models\Eloquent\Click')
->select(DB:raw("user_id, COUNT(*) count"))
->groupBy("user_id");
}
public function getClicksCountAttribute(){
return $this->clicksCount->count();
}
Now, you can use $user->clicksCount; to get the count directly.
If you are using Laravel 5.4, you can use withCount() method to easily retrieve the count. Counting Related Models

Resources