Eloquent Relationships Optimize Query - laravel

I have the following models: CardBoard, User, UserPricingPlans, PricingPlanLimits
Note: Don't mind if there is something wrong with the models code.They are working fine.
CardBoard
class CardBoard extends Model{
public function user(){
return $this->belongsTo('Models\User','id_user');
}
}
User
class User extends Model{
public function pricingPlans(){
return $this->hasMany('Models\UserPricingPlan','id_user');
}
}
PricingPlan
class PricingPlan extends Model{
public function limits(){
return $this->hasOne('Models\PricingPlanLimits','id_pricing_plan','id_pricing_plan');
}
}
PricingPlanLimits
I'll not describe that Model, its not necessary for the problem. But keep in mind that there is an attribute called maxBoards.
The problem is that I only have the CardBoard Model Instance to work on and I want to get the maxBoard attribute from PricingPlanLImits. So I did it like this:
Note: I Already have the CardBoard Model Instance here!
$maxBoard = $cardBoard->user->pricingPlans->last()->limits->maxBoard;
return $maxBoard;
The code above runs great, but the number of queries generated by this operation is an overhead to me. Eloquent do an SELECT for every Relationship called and I don't want all these data and operations.
{
"query": "select * from `users` where `users`.`id_user` = ? limit 1",
"bindings": [
],
"time": 0.96
}
{
"query": "select * from `users_princing_plan` where `users_princing_plan`.`id_user` = ? and `users_princing_plan`.`id_user` is not null",
"bindings": [
],
"time": 0.8
}
{
"query": "select * from `pricing_plan_limits` where `pricing_plan_limits`.`id_pricing_plan` = ? and `pricing_plan_limits`.`id_pricing_plan` is not null limit 1",
"bindings": [
],
"time": 0.88
}
Isn't there an way to optmize this and run fewer queries in a Eloquent-Way ?

you can get a data in one query if you use with() method.
for example: CardBoard::with('user.pricingPlans')->get();
so can optimize your query using with method.

Previous comments were not too relevant to this solution...
example
$cardboard->user()->whereHas('pricingPlans', function ($plans) {
$plans->selectRaw('price_plan_limits.id, MAX(price_plan_limits.maxBoard) as MB'))
->from('price_plan_limits')
->where('price_plan_limits.id', 'price_plan.id')
->orderBy('MB', 'DESC')
})->get();

I usually go in reverse order:
$maxBoard = PricingPlanLimits::whereHas(function($q)use($cardBoard){
$q->whereHas('PricingPlan', function($q1)use($cardBoard){
$q1->whereHas('User', function($q2)use($cardBoard){
$q2->whereHas('CardBoard', function($q3)use($cardBoard){
$q3->where('id', $cardBoard['id']);
});
});
// Probably you have to improve this logic
// It is meant to select select the last occurrence
$q1->orderBy('created_at', 'desc');
$q1->limit(1);
});
})->first()['maxBoard'];
Totally untested, but this should be the correct approach to achieve your goal in one query.

You probably could reduce the number of calls by using hasManyThrough relation (See:https://laravel.com/docs/5.4/eloquent-relationships#has-many-through).
In that case you'd have something like
class CardBoard extends Model{
public function userPricingPlans(){
return $this->hasManyThrough('Models\UserPricingPlan', 'Models\User', 'id_user', 'id_user');
}
}
And then you could call it like this:
$maxBoard = $cardBoard->userPricingPlans->last()->limits->maxBoard;
To have it all in a single query you'd need fluent and real SQL joins, can't be done with eloquent (but then you'll miss all of ORM fun)

Typically it is totally fine to achieve the result with 3 queries, a query like that normally take 10ms. But each of your queries is taking nearly 1 second, which is way too long. I don't know the reason why.
You are also able to achieve the same result with a single query though.
Your naming is a bit unconventional. I use a more popular naming convention, hopefully you can apply to your case.
class CardBoard extends Model
{
protected $table = 'card_boards';
public function user()
{
return $this->belongsTo(User::class,'user_id');
}
}
class User extends Model
{
protected $table = 'users';
public function pricingPlans()
{
return $this->hasMany(UserPricingPlan::class,'user_id');
}
}
class PricingPlan extends Model
{
protected $table = 'pricing_plans';
// This is a one to one relationship so I use a singular form.
public function limit()
{
return $this->hasOne(PricingPlanLimit::class,'pricing_plan_id');
}
}
class PricingPlanLimit extends Model
{
protected $table = "pricing_plan_limits";
}
The query to get the result:
$carboardId = 100;
$latestPricingPlanSubQuery = PricingPlan::select('pricing_plans.*', DB::raw('MAX(created_at) as last_post_created_at'))
->groupBy('user_id');
$carboard = Carboard::select('card_boards.*', 'pricing_plan_limits.max_board')
->join('users', 'cardboards.user_id', '=', 'users.id')
->joinSub($latestPricingPlans, 'latest_pricing_plans', function ($join){
$join->on('users.id', '=', 'latest_pricing_plans.user_id');
})
->join('pricing_plan_limits', 'latest_pricing_plans.id', '=', 'pricing_plan_limits.pricing_plan_id')
->find($cardboardId);
The key thing is to have a sub query getting only the latest pricing plans for each user.

Related

How to sort parent data based on child column detial in laravel 5.4?

I have 3 way relationship firstl i have get code like this in a controller
Trial::with('subjects')->where('source_id', $sourceId)->get()->toArray()
Now I want to sort subject.reactions on desc order of subject.reactions.accounts.nb_followers column. I tried to use orderby on relationship but it does not work because it sorting account indsted on reactions. I want to sort reaction based on value of "nb_followes" column present inside account table.
Trail Model
class Trial extends Model
{
use HasFactory;
public $table = 'trials';
public function subjects()
{
return $this->hasMany(Subject::class, 'trial_id')->with(['reactions', 'news'])->withCount('reactions');
}
}
Subject Model
class Subject extends Model
{
use HasFactory;
public $table = 'subjects';
public function reactions()
{
return $this->hasMany(Reaction::class, 'subject_id', 'id')->with(['accounts' => function ($q) {$q->orderBy('nb_followers', 'DESC');}])->where('twitter_error', 0)->where('active', 1)->orderby('key_reaction', 'DESC');
}
public function news()
{
return $this->hasone(News::class, 'id', 'news_item_id');
}
Reaction Model
class Reaction extends Model
{
use HasFactory;
public $table = 'reactions';
public function accounts()
{
return $this->belongsTo(Account::class, 'account_id', 'id')->where('name', '!=', '');
}
Thank you in Advance.
I want to sort reactions based on account table's column yes i cant use simple eloquent query because it will not create a structure that i want so that's why i created these relationships.
first you need to loop over trails using map method
second you need to use transform method to transform your subject in
sorted manner
as show in Subject model you need sort reaction by key_reaction
feild
$trails = ViewTrial::with('subjects')->where('source_id', $sourceId)->get();
$trails->map(function($trails){
return $trails->subjects = $trails->subjects->transform(function (&$subject) {
return [
"column" => $subject->column,
"reactions" => $subject->reactions->sortByDesc(function ($reactions) {
return $reactions['accounts']['nb_followers'];
})->sortByDesc('key_reaction')->values()->all()
]
})->values();
});
return $trails->toArray();

Laravel Eloquent: whereHas on relationship involving multiple tables

I am working on a Laravel project. I am using Eloquent to query data from the database.
I have the following models
Donation.php
class Donation extends Model
{
public function donator()
{
return $this->belongsTo(User::class);
}
}
User.php
class User extends Model
{
public function charities()
{
return $this->belongsToMany(Charity::class);
}
}
Charity.php
class Charity extends Model
{
public function donators()
{
return $this->belongsToMany(User::class);
}
}
I am writing a query on Donation model class and I am trying to query something like this. The query is just abstraction.
Donation::whereHas('donator.charities', function($query) {
$query->whereIn('charities.id', [ 1,2,3,4 ])
})
As you can see in the whereHas, I am applying the where clause on charities of donator. Is it possible to do that? How can I do that?
I hope this would be the answer of your question.
Donation::whereHas('donator',function($query){
$query->whereHas('charities',function($query){
$query->whereIn('id',[1,2,3,4]);
})
})->get();

Limit Eloquent Model to specific columns

I'm pulling from a rather large database and for security reasons, my database user can only select a limited number of columns from the student table: name, graduation_date, and gender. But there are dozens of other columns returned in a select * statement.
In regular SQL, if I run something like:
SELECT * FROM students
will return an error on that table. Same if I run the eloquent model
Students::all();
will return an error as well.
I know in Eloquent, you can limit your selects when defining a relationship similar to:
class Students extends Eloquent {
protected $table = 'student_info';
public function classes() {
return $this->hasMany('classes')->select(array('room', 'time'));
}
}
So, My question is, can the select limits be done on the main model, similar to limiting it on the classes table. So, when I run Student::all(); it only selects the columns I need.
The main problem is every time I run a student Query, I'm having to do a specific select command each time instead of just saying "Student::all()". Same thing for Student::find(1); will also return an error, because it still runs a SELECT * FROM student_info WHERE id = 1.
I tried setting $visible variable, but it still renders sql equivalent to SELECT * FROM ...
Anyone have a solution?
UPDATE:
Please note that I'm looking or a solution on the model level, not the controller level. I can select from the controller side, but that defeats the purpose of a Model concept and have to declare the columns to select at every query.
Thanks!
Troy
You can create an intermediate class and overload the all() function. Let's call this class Elegant
Elegant.php
abstract class Elegant extends Model
{
public static $returnable = [];
public function all()
{
return $this->get(static::$returnable)->all();
}
}
Then you extend this class, and define your returnable columns to it.
Student.php
<?php
class Student extends Elegant
{
public static $returnable = ['room', 'time'];
}
Now use it as you wanted: Student::all() in your controller. If you leave returnable as an empty array, then you will get everything.
Expanding on Jarek's suggestion of using Global Scope you could do it like this, I'm not 100% sure the remove part is right though, will need testing.
SelectLimitTrait.php
trait SelectLimitTrait {
public static function bootSelectLimitTrait()
{
static::addGlobalScope(new SelectLimitScope);
}
public function getQueryable()
{
if(! $this->queryable ) return array('*');
return $this->queryable;
}
}
SelectLimitScope.php
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\ScopeInterface;
class SelectLimitScope implements ScopeInterface {
public function apply(Builder $builder)
{
$query = $builder->getQuery();
$queryable = $builder->getModel()->getQueryable();
$query->columns = $queryable;
}
public function remove(Builder $builder)
{
$query = $builder->getQuery();
$query->columns = null;
}
}
And then in your Eloquent model put this:
Students.php
class Students extends \Eloquent {
use SelectLimitTrait;
protected $queryable = array('name','graduation_date', 'gender');
}
Now Students::all() and Students::find(1) etc. are limited to querying name, graduation_date and gender
You may use something like this:
public function newQuery()
{
return parent::newQuery()->select('room', 'time');
}
Put the newQuery method in your Students model and use the Student model normally you would use. It's a hacky way but easiest one. Just override the parent::query(). In this way you'll always get the selected fields.

Laravel 4 - How to use where conditions for relation's column

This is what I want, I have two tables. one is 'Restaurants' and other is 'Facilities'.
The tables are simple.. and One-To-One relations. like there is a restaurant table with id, name, slug, etc and another table called facilities with id, restaurant_id, wifi, parking, etc
Here are my models:
class Restaurant extends Eloquent {
protected $table = 'restaurants';
public function facilities() {
return $this->hasOne('Facilities');
}
}
class Facilities extends Eloquent {
protected $table = 'facilities';
public function restaurant() {
return $this->belongsTo('Restaurant');
}
}
I want do like this Select * from restaurants r left join facilities rf on r.id=rf.restaurant_id where r.name = 'bbq' and rf.wifi != '1'.
How to use Eloquent to do that?
ps. sorry for modify from https://stackoverflow.com/questions/14621943/laravel-how-to-use-where-conditions-for-relations-column#= , but I have the similar problem.
You can use where and other sql-based methods on the relationship objects.
That means you can either create a custom method in your model:
class Restaurant extends Eloquent {
protected $table = 'restaurants';
public function facilities($wifi) {
return $this->belongsTo('Facility')->where('wifi', '=', $wifi);
}
}
Or you can try to use query scopes:
class Restaurant extends Eloquent {
protected $table = 'restaurants';
public function facility() {
return $this->belongsTo('Restaurant');
}
public function scopeFiltered($query, $wifi)
{
return $query->where('wifi', '>', 100);
}
}
Then:
$wifi = 1;
$restaurants = Restaurant::facilities()->filtered($wifi)->get();
This isn't exactly what you need likely, but query scopes is likely what you want to use to get what you're attempting.
THe key point is to know that relationship classes can be used like query builders - for example:
$this->belongsTo('Facility')->where('wifi', '=', $wifi)->orderBy('whatever', 'asc')->get();
There are some ways to filter both, this is using QueryBuilder:
Restaurant::join('facilities','facilities.restaurant_id','=','restaurants.id')
->where('name','bbq')
->where('facilities.wifi','!=', 1)
->get();

Many to Many with eager loading in Laravel 4

I have this model:
class Ownership extends Eloquent {
protected $table = 'game_user';
public function games() {
return $this->belongsToMany('Game');
}
public function type() {
return $this->belongsToMany('owntype');
}
}
Models for Game and Owntype are simple ...extends Eloquent. This is how I pull the data:
$games = Ownership::with('games','type')->where('user_id','=','1')->get();
Theoretically, it works. Practically, not, because it returns empty games and owntype collections. Here's what it returns: http://paste.laravel.com/s94
How can I get games and users table contents? I don't want to Game::find in foreach, because it would produce a lot of queries.
You need to pass an array inside with().
I.e.
$games = Ownership::with(array('games','type'))->where('user_id','=','1')->get();

Resources