How to fetch two related objects in Laravel (Eloquent) with one SQL query - laravel

I am trying to get two related objects in Laravel using eager loading as per documentation.
https://laravel.com/docs/5.4/eloquent-relationships#eager-loading
My models are:
class Lead extends Model {
public function session() {
return $this->hasOne('App\LeadSession');
}
}
class LeadSession extends Model {
public function lead() {
return $this->belongsTo('App\Lead');
}
}
I want to get both objects with one SQL query. Basically I want to execute:
select * from lead_sessions as s
inner join lead as l
on l.id = s.lead_id
where s.token = '$token';
and then be able to access both the LeadSession and Lead objects. Here is the php code I am trying:
$lead = Lead::with(['session' => function ($q) use ($token) {
$q->where('token','=',$token);
}])->firstOrFail();
print($lead->session->id);
I have also tried:
$lead = Lead::whereHas('session', function($q) use ($token) {
$q->where('token','=',$token);
})->firstOrFail();
print($lead->session->id);
and
$session = LeadSession::with('lead')->where('token',$token)->firstOrFail();
print($session->lead->id);
In all three cases I get two queries executed, one for the leads table, and another for the lead_sessions table.
Is such a thing possible in Eloquent? In my view it should be a standard ORM operation, but for some reason I am struggling a whole day with it.
I don't want to use the Query Builder because I want to use the Eloquent objects and their functions afterwards.
I am coming from Python and Django and I want to replicate the behavior of select_related function in Django.

Try this and see if it makes more than one query
$session = LeadSession::join('leads', 'leads.id', '=', 'lead_sessions.lead_id')
->where('token',$token)
->firstOrFail();
I hope it only runs a single query. I didnt test this. Not sure if you have to add a select() to pick the columns. But yeah, try this first.
Updates
Just adding how to use both session and lead data. Try a select and specify the data you need. The reason being that if both tables have similar columns like 'id', one of them will be overwritten. So you have to alias your select like
$session = LeadSession::join('leads', 'leads.id', '=', 'lead_sessions.lead_id')
->where('token',$token)
->select(
'lead_sessions.*',
'leads.id as lead_id',
'leads.name',
'leads.more_stuff'
)
->firstOrFail();
Now all this data belongs to $session variable. For testing you were doing
print($lead->session->id);
//becomes
print($session->lead_id); //we aliased this in the query

Related

How to express Larvel subqueries to use within a whereIn

I'm sorry for asking somthing so simple, but I can't get the docs (https://laravel.com/docs/9.x/queries#subquery-where-clauses)
I'm writting something like a social network functionality, so I have my messages table and my users table, there's another pivot table where I store the users the user follows, and they work pretty well.
I want to represent the following SQL in Laravel's ORM
SELECT * FROM mensajes
WHERE user_id=1
OR user_id IN (SELECT seguido_id FROM seguidos WHERE user_id=1)
The idea is I'll get the user's posts, and also the posts from the users that the user follows.
My following solution works, but I feel it's quite dirty, and should be solved with a Subquery
// this relation returns the users the user is following, ans works correctly
$seguidos = auth()->user()->seguidos;
// I store in an array the ids of the followed users
$seg = [];
foreach ($seguidos as $s) {
array_push($seg, $s->id);
}
array_push($seg, auth()->user()->id);
// Then I retrieve all the messages from the users ids (including self user)
$this->mensajes = Mensaje::whereIn('user_id', $seg)
->orderBy('created_at', 'desc')
->get();
I'd like to change everything to use subqueries, but I don't get it
$this->mensajes = Mensaje::where('user_id', auth()->user()->id)
->orWhereIn('user_id', function($query) {
// ... what goes here?
// $query = auth()->user()->seguidos->select('id');
// ???? This doesn't work, of course
}
->orderBy('created_at', 'desc')
->get();
You can simply construct the raw query as you have done with the SQL.
->orWhereIn('user_id', function($query) {
$query->select('seguido_id')
->from('seguidos')
->where('seguidos.user_id', auth()->user()->id);
});
But normally sub queries have a relationship between the primary SQL query with the sub query, you don't have this here and instead of doing one query with a sub query, you can quite simply write it as two queries. If you are not calling this multiple times in a single request, one vs two queries is insignificant performance optimization in my opinion.
Mensaje::where('user_id', auth()->user()->id)
->orWhereIn(
'user_id',
Seguidos::where('user_id', auth()->user()->id)->pluck('seguido_id'),
);

How to do a surgery on an Eloquent Query on-the-fly

is there any way to interrupt and change the components and scopes of an eloquent query?
in order to add multitenancy to an existing project, I've added a global scope to my models, filtering results by tenant_id. and it works fine.
the problem is I have found more than 500 hardcoded 'where conditions ' and 'create statements' all over the place. such as these:
$notification_type= NotificationTypes::where('id', '2')->get();
or
$tickets = Tickets::create( $title, $body, $sender, '1'); // 1 as statusID
etc.
It's a problem because I'm using the single DB approach for multitenancy and these IDs must be relative to the tenant. for Example in the first query above, I don't want the 'NotificationTypes' with Id of '2', But I want the 'second' NotificationType with that tenant_id (id of this column could be 4 or 7 or else).
I can figure out a way to properly calculate the exact amount of these Relative IDs. but is there any way to find the prior scope and conditions of an eloquent object and change them?
I'm looking for a way to add multitenancy to the project without changing much of the existing code. like a module or plugin.
You can use scope in Laravel
public function scopeActive($query)
{
$query->where('active', 1);
}
Then in your controller just call
$notification_type = NotificationTypes::active()->get();
More ref on doc: https://laravel.com/docs/9.x/eloquent#query-scopes
Dynamic Scope
public function scopeActive($query, $value)
{
return $query->where('active', $value);
}
$notification_type = NotificationTypes::active(value_you_want)->get();
Change value_you_want with what you want
Or you can use
$notification_type = NotificationTypes::where('id', 1)->orWhere('tenant_id', 1)->get();

Laravel / Eloquent: Is it possible to select all child model data without setting a parent?

I have various parent/child relationships, drilling down a few levels. What I want to know is if its possible to do something like this:
$student = Student::find(1);
$student->bursaries()->enrolments()->courses()->where('course','LIKE','%B%');
(With the end goal of selecting the course which is like '%B%'), or if I would have to instead use the DB Query builder with joins?
Models / Relationships
Student:
public function bursaries() {
return $this->hasMany('App\StudentBursary');
}
StudentBursary:
public function enrolments() {
return $this->hasMany('App\StudentBursaryEnrolment');
}
If what you want is to query all courses, from all enrollments, from all bursaries, from a students, then, unfortunately, you are one table too many from getting by with the Has Many Through relationship, because it supports only 3 tables.
Online, you'll find packages that you can import / or answers that you can follow to provide you more though of solutions, for example:
1) How to use Laravel's hasManyThrough across 4 tables
2) https://github.com/staudenmeir/eloquent-has-many-deep
Anyhow, bellow's something you can do to achieve that with Laravel alone:
// Eager loads bursaries, enrolments and courses, but, condition only courses.
$student = Student::with(['bursaries.enrolments.courses' => function($query) {
$query->where('course','LIKE','%B%');
}])->find(1);
$enrolments = collect();
foreach($student->bursaries as $bursary) {
$enrolments = $enrolments->merge($bursary->enrolments);
}
$courses = collect();
foreach ($enrolments as $enrolment) {
$courses = $courses->merge($enrolment->courses);
}
When you do $student->bursaries() instead of $student->bursaries, it returns a query builder instead of relationship map. So to go to enrolments() from bursaries() you need to do a bursaries()->get(). It should look like this.
$student->bursaries()->get()[0]->enrolments(), added the [0] because im using get(), you can use first() to avoid the [0]
$student->bursaries()->first()->enrolments()
But I'm not sure if it will suffice your requirement or not.

Laravel - eager loading filtering related model on different database instance

i'm trying to implement filtering as defined here.
This works (to filter model A from controller A), however controller A/model A has a relation to model B, which is what I want to filter, as well as a 3rd relationship to model C from model B.
Model A is hosted on DB instance 1, Model B is hosted on DB instance 2 and are totally separated.
These relationships, without any filters, work fine.
Trying to mess around, I tried something like the below, which clearly does not work, however will hopefully illustrate what i'm trying to do. This filter gets applied to model A
protected function sn($sn)
{
$s= Bid::where('crm', function ($query) {
$query->where('sNumber', '=', 'SN512345');
})->get();
return $s;
}
SQLSTATE[42S22]: [Microsoft][ODBC Driver 17 for SQL Server][SQL Server]Invalid column name 'crm'. (SQL: select * from [bids] where [crm] = (select * where [sNumber] = SN512345))
Bid is model A/controller A, CRM is model B which is the one I want to filter.
I thought about having numerous different functions in the model to filter, however I don't know if this was the best solution and I thought it was better to get it all out into another class.
I tried the below, which does not work as it applies the query to DB1.
$s= Bid::with('crm')->whereHas('crm', function ($query) {
$query->where('sNumber', '=', 'SN512345');
})->get();
[SQL Server]Invalid object name 'Opportunity'. (SQL: select * from [bids] where exists (select * from [Opportunity] where [bids].[crm_ref] = [Opportunity].[sNumber] and [sNumber] = SN512345))
Is there a way to implement this in some coherent and reusable way? I was thinking something along the lines of load Bid::, load CRM:: with applied filters, then append CRM:: to Bid:: somehow in the normal way that Eloquent would do this.
Thanks.
EDIT:
I am using the following filter in BidFilter.php
protected function sn($sn)
{
$users = DB::connection('sqlsrv_crm')->table('OpportunityBase')->select('*')->where('new_SalesNumber', '=', $sn)->get();
return $users;
}
And this filters the result set, as I can see in the debug bar:
debug bar queries
However this is also loading the normal unfiltered eager loaded CRM relationship. How can I switch to the filtered CRM results instead of the default unfiltered?
BidController index method:
public function index(BidFilter $filters)
{
$bids = $this->getBids($filters);
return view('public.bids.index', compact('bids'));
}
BidFilter
public function index(BidFilter $filters)
{
$bids = $this->getBids($filters);
return view('public.bids.index', compact('bids'));
}

Use Laravel 5.3 Query Builder to replicate Eloquent ORM data structure for Sub Query

I am trying to replicate the result set that I get when using Eloquent ORM, except with Laravel Query Builder. Basically using this code I can get the packs to appear nested within the products so that when I loop them on the view I can further loop the packs within each products. Seems pretty basic right (see result set below).
$get_posts_for_product = Product::where('active', 1)
->with('packs')
->get()->toArray();
I have tried a few ways using Query Builder to get this to work but it joins the packs inline as I thought it would.
What is the best way to get this same Array structure using Query Builder, I am aware that the result set is a different type of array and that is fine but for my project it must be done using Query Builder at this point.
Thanks.
I would say, that is why you have Eloquent: you don't have to worry about how to have those relationships together.
However incase you really want to achieve the same result I will demo this using two tables users and messages:
1st method:
Retrieve the users and transform it by querying the database for relationships:
$result = DB::table('users')->get()->transform(function ($user){
$user->messages = DB::table('messages')->where('user_id', $user->id)->get();
return $user;
});
Downside: Having many users means a lot of db query on messages table.
Upside: less codes to write
2nd method:
Retrieve both tables using all the ids of user to query the messages:
$users = DB::table('users')->get();
$messages = DB::table('messages')->whereIn('user_id', $users->pluck('id')->toArray())->get();
$result = $users->transform(function ($user) use ($messages){
$user->messages = $messages->where('user_id', $user->id)->values();
return $user;
});
Downside: The need to still transform it.
Upside: Less database trips. i.e two queries only.
3rd method
Looks like the second except that you can group messages by 'user_id' then you do no extra filter when transforming users result:
$user = DB::table('users')->get();
$messages = DB::table('messages')->whereIn('user_id', $user->pluck('id')->toArray())
->get()
->groupBy('user_id');
$result = $user->transform(function ($user) use ($messages){
$user->messages = $messages[$user->id];
return $user;
});
Downside: Same with two.
Upside: no extra filter when transforming users.
Other method
Join on both users and messages when querying then transform the response, or simply use it as it is.
PS: Eloquent uses query builder.
The answer is open for update.

Resources