How to set softdeletes condition as first clause in query with laravel models? - laravel

I have a simple question. Could anyone help me please?
In laravel models, we can use Sotfdeletes trait. So this trait add the whereNull('deleted_at') sentence. It is ok. However, we have a big table and our queries are very slow.
We have Company model which use SotfDeletes trait. It has some scope methods such as createdAtBigger, nameLike, oneOfBoard etc.
This is the company model:
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Database\Query\Builder;
/**
* #method static|Builder createdAtBigger(\DateTime $datetime)
* #method static|Builder nameLike(string $name)
* #method static|Builder oneOfBoard(array $boardIds)
*/
class Company extends Model
{
use SoftDeletes;
public function scopeCreatedAtBigger(Builder $builder, $datetime)
{
return $builder->where('created_at', '>', $datetime);
}
public function scopeNameLike(Builder $builder, $name)
{
return $builder->where('name', 'LIKE', '%'.$name.'%');
}
public function scopeOneOfBoard(Builder $builder, $boardIds)
{
return $builder->whereIn('board_id', $boardIds);
}
/**
* the other methods and scopes
* ...
* ...
* ...
*/
}
When we try to use that model with these scopes, it likes that for example:
$companies = Company::nameLike('XX Company')
->oneOfBoard([1,2,3,4])
->createdAtBigger('2022-02-14')
->get();
and its sql is like that:
SELECT *
FROM company
WHERE name like '%XX Company%'
and created_at > '2022-02-14'
and board_id in(1,2,3,4)
and deleted_at=null;
You can see, the deleted_at clause is the last condition.
This table has mostly deleted rows.
I want to move deleted_at condition at the first.
If my sql will like that, it it will take half time than now:
But I couldn't.
SELECT *
FROM company
WHERE deleted_at=null
and name like '%XX Company%'
and created_at > '2022-02-14'
and board_id in(1,2,3,4);
I don't want to use deletedAtNotNull scope because this model is very huge. It has lots of calls.
How can I do it? Is that any way?
PS: Thanks for #timlewis, I have an index on deleted_at column.

Related

Get data from different tables using inner joins

I have column in results as 'user_id','test','subject' and in datatable i want to get the 'test_name' which is saved in 'tests' table, Student name 'name' saved in 'users' and 'subject name' in table 'subjects' in column 'subjects' now tell me what is the best way to get this data.
So far i had tried this but getting null while dumping.
$result = DB::table('results')
->where([
['results.subject',$request->subject],
['test',$request->test],
['user_id',$request->name]
])
->join('users','results.user_id','=','users.name')
->join('tests','tests.id','=','results.test')
->join('subjects','subjects.id','=','results.subject')
->select('results.*','users.name','tests.test_name','subjects.subjects As s_subject')
->first();
dd($result);
anyone who can guide me the best possible solution.
You should use the power of the Eloquent model of Laravel. You have to create Results model class as follow(and obviously need to create User, Test and Subject model class)
Results.php
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
use Carbon\Carbon;
class Results extends Model
{
/**
* The attributes that aren't mass assignable.
*
* #var array
*/
protected $guarded = ['id'];
/**
* Get the user profile associated with the results.
*/
public function user()
{
return $this->hasOne('App\User', 'user_id');
}
/**
* Get the test details associated with the results.
*/
public function test()
{
return $this->hasOne('App\Test', 'test');
}
/**
* Get the subject associated with the results.
*/
public function subject()
{
return $this->hasOne('App\Subject', 'subject');
}
}
Controller.php
$results = App\Results::where(['subject' => $request->subject, 'test' => $request->test, 'user_id' => $request->name])->first();
Now you will get the Result object with the test, subject, and test property.
Note that:- You should provide the output when you are asking any question.

Query Laravel nested relationship [Laravel 6.x]

I have a page in my Laravel app where I am fetching categories and eager loading events in that category. It works fine. What I want to do now is to fetch the categories alright but this time, fetch events based on a region/location selected by the user. These are the models I am working with;
1.Region Class
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Region extends Model
{
/**
* Get events within region
*
* #return \Illuminate\Database\Eloquent\Relations\HasMany
*/
public function events()
{
return $this->hasMany(Event::class);
}
}
Category Class
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Category extends Model
{
/**
* The attributes that are mass assignable.
*
* #var array
*/
protected $fillable = [
'category_name', 'description', 'type', 'slug'
];
/**
* Get all events that belong to a category
*
* #return Illuminate\Database\Eloquent\Relations\HasMany
*/
public function events()
{
return $this->hasMany(Event::class)->where('start_date', '>=', today())->orderBy('start_date', 'asc');
}
}
3.Event
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Event extends Model
{
/**
* Get an event's category
*
* #return Illuminate\Database\Eloquent\Relations\BelongsTo;
*/
public function category()
{
return $this->belongsTo(Category::class);
}
/**
* Get region of event
*
* #return [type] [description]
*/
public function region()
{
return $this->belongsTo(Region::class);
}
}
This is the query that returns categories with events;
if (session()->has('region')) {
$region_name = session()->get('region');
$region = Region::where('region_name', $region_name)->firstOrFail();
$categories = Category::withCount('events')
->with('events')
->whereHas('events', function (Builder $query) use ($region) {
$query->where('region_id', $region->id);
})
->orderBy('events_count', 'desc')
->take(5)
->get();
}
Summary: I want to fetch top 5 categories with events in user's selected location.
Thanks in advance for your help.
Try this for the categories query:
$categories = Category::with(['events' => function ($query) use ($region) {
$query->where('region_id', $region->id);
}])->take(5)->get();
Check the documentation on restraining eager loads
Have you looked at the hasManyThrough relationship?
https://laravel.com/docs/5.8/eloquent-relationships#has-many-through
If you give your Region model many Categories through the Event class:
public function categories()
{
return $this->hasManyThrough('App\Category', 'App\Event');
}
You should be able to call:
$region->categories;
Which should return all the categories that have events for that region. You can then eager load the events in each category, sort by the number of events and take the first 5 before outputting to your view.
Alternative
An alternative would be to simply get all events where the region_id is the selected region and add a groupBy for category:
$eventsByCategory = Event::where('region_id', $selectedRegion)->groupBy('category_id);
Then once you have the events grouped by category you can sort by the count and take the top 5.
$eventsByCategory->sort()->take(5);
Side Note:
I'd be careful with your events relationship on the Category model. I think adding your where to the returned value will return an instance of Builder rather than Relation which means you may not be able to utilise some of the functionality of relationships.
I'd suggest looking at a scope on the Event model instead that you can apply globally or locally depending on how often you might want to get past events.

Laravel - one-to-one relation through pivot table with eager load

I have this relationship
A Movement can have multiples steps
A Step can belongs to multiples Movements
So a had to create a pivot table and a belongsToMany relationship, but my pivot table have some extras columns, like finished and order
I want to have two relationships, one to get all steps from a movement and another one to get the current step from the movement (the last finished step)
I know how to get all steps
public function steps()
{
return $this->belongsToMany(MovementStep::class, 'movement_movement_steps')
->withPivot('order', 'finished')
->orderBy('pivot_order');
}
But how about the current step? I need this kind of relationship, but returning only one record and be able to eager load it cause I'm passing it to vue.js
public function current_step()
{
return $this->belongsToMany(MovementStep::class, 'movement_movement_steps')
->withPivot('order', 'finished')
->where('finished', true)
->orderBy('pivot_order', 'desc');
}
Notice, I'd like to do that without extras packages
alternative solution, but with extra package: Laravel hasOne through a pivot table (not the answer marked as correct, the answer from #cbaconnier)
A different approach from the answer provided by #mrhn is to create a custom relationship. Brent from Spatie did an excellent article about it
Although my answer will do the exact same queries than the one provided by staudenmeir's package it makes me realized that either you use the package, this answer or #mrhn answer, you may avoid the n+1 queries but you may still ends up will a large amount of hydrated models.
In this scenario, I don't think it's possible to avoid one or the other approach. The cache could be an answer though.
Since I'm not entirely sure about your schema, I will provide my solution using the users-photos example from my previous answer.
User.php
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class User extends Model
{
public function photos()
{
return $this->belongsToMany(Photo::class);
}
public function latestPhoto()
{
return new \App\Relations\LatestPhotoRelation($this);
}
}
LastestPhotoRelation.php
<?php
namespace App\Relations;
use App\Models\User;
use App\Models\Photo;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Relations\Relation;
class LatestPhotoRelation extends Relation
{
/** #var Photo|Builder */
protected $query;
/** #var User */
protected $user;
public function __construct(User $user)
{
parent::__construct(Photo::query(), $user);
}
/**
* #inheritDoc
*/
public function addConstraints()
{
$this->query
->join(
'user_photo',
'user_photo.photo_id',
'=',
'photos.id'
)->latest();
// if you have an ambiguous column name error you can use
// `->latest('movement_movement_steps.created_at');`
}
/**
* #inheritDoc
*/
public function addEagerConstraints(array $users)
{
$this->query
->whereIn(
'user_photo.user_id',
collect($users)->pluck('id')
);
}
/**
* #inheritDoc
*/
public function initRelation(array $users, $relation)
{
foreach ($users as $user) {
$user->setRelation(
$relation,
null
);
}
return $users;
}
/**
* #inheritDoc
*/
public function match(array $users, Collection $photos, $relation)
{
if ($photos->isEmpty()) {
return $users;
}
foreach ($users as $user) {
$user->setRelation(
$relation,
$photos->filter(function (Photo $photo) use ($user) {
return $photo->user_id === $user->id; // `user_id` came with the `join` on `user_photo`
})->first() // Photos are already DESC ordered from the query
);
}
return $users;
}
/**
* #inheritDoc
*/
public function getResults()
{
return $this->query->get();
}
}
Usage
$users = \App\Models\User::with('latestPhoto')->limit(5)->get();
The main difference from Brent's article, is that instead of using a Collection we are returning the latest Photo Model.
Laravel has a way to create getters and setters that act similar to columns in the database. These can perfectly solve your problem and you can append them to your serialization.
So instead your current_step is gonna be an accessor (getter). The syntax is getCurrentStepAttribute() for the function which will make it accessible on the current_step property. To avoid N + 1, eager load the steps when you retrieve the model(s) with the with('steps') method. Which is better than running it as a query, as it will execute N times always.
public function getCurrentStepAttribute() {
return $this->steps
->where('finished', true)
->sortByDesc('pivot_order')
->first();
}
Now you can use the append property on the Movement.php class, to include your Eloquent accessor.
protected $appends = ['current_step'];

Has many: display regions with 1 or more elements

I have relationship between adv and region model. Actually I want to display all regions which has one or more active adv (I have active column into adv). And next similar problem. All regions contain cities. When user choose some region into view he saw all cities from this region. What is the best way to do that?
I won't write manually every cities. I have 2 ideas:
first cities will be string column. Actually user will choose region and write name of city into input field.
controller will check that this city exist with this region. If not, it'll create a new.
I think that second idea is better because create search engine and filter results will be simpler.
You can use a global scope in your adv like this to filter to just active advs. (this will filter the advs in all eloquent queries) Adv Model:
namespace App\Models;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
class Adv extends Model
{
/**
* The "booting" method of the model.
*
* #return void
*/
protected static function boot()
{
parent::boot();
static::addGlobalScope('aktiv', function (Builder $builder) {
$builder->where('aktiv', '=', true);
});
}
}
And then use Relationship in the Region Model
namespace App\Models;
use Eloquent;
class Region extends Eloquent
{
public function adv()
{
return $this->hasMany('App\Models\Adv');
}
}
And Get the results in your controller:
$region = Region::has('adv')->get();
You have 2 Models
Region ( has many Adv)
Adv ( brlongs to Region)
1) Region.php
<?php
namespace App\Models;
use Eloquent;
class Region extends Eloquent
{
protected $table = "region";
public function regionadv()
{
/*For active adv only*/
return $this->hasMany('App\Models\Adv')->where('active', '=', 1);
}
}
Query in eloquent:
$regionlist = = Region::has('regionadv')->get();
Good luck.

Issues with Laravel hasManyThrough relationship

I'm having issues using the hasManythrough relationship in larval. Just following the documentation using the example there, which are:
countries
id - integer
name - string
users
id - integer
country_id - integer
name - string
posts
id - integer
user_id - integer
title - string
Here is how I set up the relationship in the models
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Country extends Model
{
public function posts() {
return $this->hasManyThrough('App\Post', 'App\User', 'user_id', 'country_id', 'id');
}
}
Here is the User model
class User extends Authenticatable
{
use Notifiable;
/**
* The attributes that are mass assignable.
*
* #var array
*/
protected $fillable = [
'name', 'email', 'password',
];
/**
* The attributes that should be hidden for arrays.
*
* #var array
*/
protected $hidden = [
'password', 'remember_token',
];
public function posts() {
return $this->hasMany('App\Post');
}
public function country() {
return $this->hasOne('App\User');
}
}
Here is the Posts model
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Post extends Model
{
public function user() {
return $this->belongsTo('App\User');
}
}
So, the website doesn't go through enough detail on how to extract the posts through the country model. Using the routes file, this is the query I used
Route::get('posts/countries/{id}', function($id) {
$countries = App\Country::where('id', $id)->get();
return $countries->posts;
});
It looks to me like I set up the relationship up correctly the way the docs say to. There is a country_id on the users table, so I'm not sure if the query is wrong or maybe I did set up the relationship incorrectly.
You aren't actually requesting the relationship, you are simply looking at the attribute on countries.
If you want to eagerload the posts in the query builder you will need to add with('posts') when you build the query. (Before you call ->get() which executes the query and turns it into a collection.)
Route::get('posts/countries/{id}', function($id) {
$country = App\Country::with('posts')->where('id', $id)->first();
return $country->posts;
});
Or if you want to lazyload you can ask for the relationship on the country model by doing ->posts() like this:
Route::get('posts/countries/{id}', function($id) {
$country = App\Country::with('posts')->where('id', $id)->first();
return $country->posts();
});
Notice: in both cases I changed the ->get() to ->first(). I assume you only want one country's posts returned.
->get() executes the query and returns the related models as a collection and ->first() takes the first model from the query.
#Nicklas Kevin Frank
Your solution didn't work for me. at least not completely, but you were right in some respects. I tinkered around, and discovered that the query worked like this better:
Route::get('posts/countries/{id}', function($id) {
$country = App\Country::where('id', $id)->first();
return view('country')->with('country', $country);
});
So, like you said, it diffidently needed the ->first() option, but it didn't need the with('posts') portion. But much thanks my friend. I couldn't have solved this without you.

Resources