Adding 'orWhere' to laravel eloquent model relationship - laravel

I have a model relation like the below one in User model,
public function notifications(){
return $this->morphMany('App\Notification','receiver')
->orWhere('type', 2);
}
This works well if there are no other conditions. But if $user is an instance of User and i call like, $user->notifications->where('status', 1);,
query condition becomes,
select * from notifications where notifications.receiver_id = 3
and notifications.receiver_id is not null and
notifications.receiver_type = User or type = 2 and status = 1
here the problem is, the final condition is working as an OR since i'm not able to group the relation's conditions. Therefore, even if the base relationship condition fails, but just the status & type fields match, it will fetch that record.
What i need is to add a parenthesis before the first where and close after the relation conditions, so that any other where condition applied will not affect the base query.
What i need:
select * from notifications where (notifications.receiver_id = 3
and notifications.receiver_id is not null and
notifications.receiver_type = User or type = 2 ) and status = 1

Check out Parameter Grouping.
Something like this:
Notifications::where('receveier_id', '=', 3)
->whereNotNull('receveier_id')
->where(function ($query) {
$query->where('receiver_type', '=', 'User')
->orWhere('type', '=', 2);
})
->where('status', '=', 1);

Try this..
public function notifications(){
return $this->where(function($query){
$query->morphMany('App\Notification','receiver')
->orWhere('type', 2);
})
}
Replace your code with this code in Model
Hope this works

Related

Laravel: How to query a Model Relationship only if it exists

I have a model Survey with a column installer_id which is related to hasOne another model Installation which is related to hasMany another model Assignment.
I want to write a query to fetch all Survey where the installer_id is not null, and where IF an Assignment exists check all the assignments if any status is != 2 ie status = 0 or 1 return the survey.
I tried this query but it does not capture the "if an assignment exists then check the status" part
$surveys = Survey::whereNotNull('installer_id')
->orWhereHas('installation',function ($query) {
return $query->whereHas('assignments',function ($q){
return $q->where('status','!=', 2 );
});
})->get();
I also tried to define a hasManyThrough relationship in the model.
public function assignments()
{
return $this->hasManyThrough(Assignment::class,Installation::class);
}
and then using this query
$schedulables = Survey::whereNotNull('installer_id')
->orWherehas('assignments',function ($query){
return $query->where('assignments.status','!=', 2 );
})->get()
Please any suggestions and help would be appreciated
I think you had the correct approach, however whereNotNull is not necessary if you're already going to check with whereHas, so this way is sufficient I think:
$surveys = Survey::whereHas('installation',function ($query) {
return $query->whereHas('assignments',function ($q){
return $q->where('status','!=', 2 );
});
})->get();
and I think you're missing the ->get(); in your code, thats why you were not getting the results
Original answer
What i needed was the whereDoesntHave method.
$surveys = Survey::whereNotNull('installer_id')
->whereDoesntHave('installation.assignments', fn ($query) => $query->where('status', 2))
->get();

How to use where clause with local variable without passing a parameter in Laravel

I joined 2 tables and I want to display document_name from parent table(document_control_reviews) whose ID is currently posted in a child table (initial_approvals). The foreign key that holds the child document is 3.
If I hardcode the query with the ID = 3, I get the correct result just for testing purposes. But I want the documents to be display dynamically using $id instead of 3.
This hardcode test works fine when I use 3 as ID:
public function initial_approval(){
$approval_files = DB::table('document_control_reviews')
->leftJoin('initial_approvals', 'initial_approvals.document_control_review_id', '=', 'document_control_reviews.id')
->leftJoin('files', 'document_control_reviews.file_id', '=', 'files.id')
->select('document_control_reviews.*', 'initial_approvals.*', 'files.*')
->where('initial_approvals.document_control_review_id', '=', 3)
->get();
}
But I need a dynamic ID which is a local variable $id (as showed below):
public function initial_approval(){
$id = 3 //hardcoded. Here is where I'm stuck.
$docId = DocumentControlReview::find($id);
$approval_files = DB::table('document_control_reviews')
->leftJoin('initial_approvals', 'initial_approvals.document_control_review_id', '=', 'document_control_reviews.id')
->leftJoin('files', 'document_control_reviews.file_id', '=', 'files.id')
->select('document_control_reviews.*', 'initial_approvals.*', 'files.*')
->where('initial_approvals.document_control_review_id', '=', $id)
->get();
}
This is what I don't want. I dont need to pass any parameter. I don't need any argument in the method call.
public function initial_approval($id){
$approval_files = DB::table('document_control_reviews')
->leftJoin('initial_approvals', 'initial_approvals.document_control_review_id', '=', 'document_control_reviews.id')
->leftJoin('files', 'document_control_reviews.file_id', '=', 'files.id')
->select('document_control_reviews.*', 'initial_approvals.*', 'files.*')
->where('initial_approvals.document_control_review_id', '=', $id)
->get();
}
This piece of code won't work the way you intend to because here id is a collection not an integer.
$id = DB::table('document_control_reviews')->select('id')->get();
You think id will be an integer like $id = 3 but in fact here $id = Collection({"id"=>3},...) get() always return a collection.
What you are trying to do is unclear please elaborate with an example.
I finally figured it out. I simply use the where clause in my query where the child ID is a positive integer. And I get exactly what I wanted in this code below:
$approval_files = DB::table('document_control_reviews')
->leftJoin('initial_approvals', 'initial_approvals.document_control_review_id', '=', 'document_control_reviews.id')
->select('initial_approvals.*', 'document_control_reviews.*')
->where('initial_approvals.id', '>', 0)
->get();

Laravel eloquent query loads hundreds of models

I'm querying a relationship with pagination, yet in my debugbar I can see that all models are loaded in memory and if I'm correct that should not be happening.
I have a Post model with a hasMany relationship to Comments. I have a few lines of code as below. They are written in this order because there are parameters in between that I need to apply. I have shown filterScore here but there are multiple that work the same way.
$post = Post::find(1);
$comments = $post->comments();
$comments = $comment->filterScore($comments)
$comments = $comments->orderBy('created_at', 'DESC');
return $comments->paginate(25);
private function filterScore($q)
{
if($this->score > 0)
return $q->where('score', $this->score);
return $q;
}
The raw query if $this->score = 0:
select * from `comments` where `comments`.`post_id` = 1 and `comments`.`post_id` is not null order by `created_at` desc limit 25 offset 0
UPDATE
I've tried writing it like this, based on this post, but then I still get the same result: all models are loaded into memory.
$post = Post::find(1);
$comments = Comment::query();
$comments = Comment::where('post_id', $post->id);
$comments = $comment->filterScore($comments);
return $comments->paginate(25);
In the Laravel debugbar you can see that all models are loaded into memory, instead of just 25:
One solution is, as #nikistag wrote in comment: $post->comments()->orderBy('created at' , 'DESC')->paginate(25); (you have to have all in one chained expression).
But if you use optional parameters, it can be difficult to achieve someting like that.
In this case, you can just change it to 2 seperate codes, where you do not use laravel relationship:
$post = Post::find(1);
$comments = Comment::where('post_id', $post->id);
if(!empty($from)) //if set optional parameter, add condition
$comments->where('created_at', '>=', $from);
$comments->orderBy('created_at', 'desc')->paginate(25);

Eloquent whereHas Relationship with constraint on particular related Entity

Consider having two models User, and Book the last one has a status column that can obtain different string values active, inactive, deleted, so the user can have multiple books and the book belongs to the user.
how could I get only users that have their last book status = 'inactive'?
The SQL Query for the behavior is given below:
SELECT
*
FROM
`users`
WHERE EXISTS
(
SELECT
*
FROM
`books`
WHERE
`books`.`user_id` = `users`.`id` AND `books`.`status` = 'inactive' AND `books`.`id` =(
SELECT
nested.`id`
FROM
`books` AS nested
WHERE
nested.`user_id` = `users`.`id`
ORDER BY
nested.`created_at` DESC
LIMIT 1
)
)
I'm using Laravel 5.6
Create additional relationship in User model that returns wanted result. Basically you need 1-1 relationship for this.
/**
* #return \Illuminate\Database\Eloquent\Relations\HasOne
*/
public function inactiveBookStillLatestPerUser()
{
return $this->hasOne(Book::class)->where(['status' => 'inactive', 'id' => function (\Illuminate\Database\Query\Builder $nested) {
$nested->from('books as nested')
->selectRaw('max(id)')
->whereRaw('nested.user_id = books.user_id');
}]);
}
Then in somewhere in code (i.e. controller) you call it with
$users = User::has('inactiveBookStillLatestPerUser')->get();
// or if books are needed too
// $users = User::has('inactiveBookStillLatestPerUser')->with(['inactiveBookStillLatestPerUser'])->get();
I used id latest order [max(id)] in subquery to avoid unwanted result if one user made multiple books batch insert at same point of time and when all those books would have same time of insert so latest per created_at wouldn't be most accurate, maybe. But you can do that similarly, instead:
/**
* #return \Illuminate\Database\Eloquent\Relations\HasOne
*/
public function inactiveBookStillLatestPerUser()
{
return $this->hasOne(Book::class)->where(['status' => 'inactive', 'created_at' => function (\Illuminate\Database\Query\Builder $nested) {
$nested->from('books as nested')
->selectRaw('max(created_at)')
->whereRaw('nested.user_id = books.user_id');
}]);
}
Maybe second example is ok, but first example with id would work fine, though.
User::where('your-conditions')
->whereHas('books', function($query) {
$query->where('books.status', '=', 'inactive')
->orderBy('id', 'desc')
->first();
})->get();

Join query using eloquent model mapping

I am trying to do this
select notifications.id, reservations.number from
notifications
JOIN reservations
ON notifications.reservation_id = reservations.id
WHERE notifications.status = 1
using eloquent so I have this this
$await = Notification::with('Reservation')->
select('notifications.id', 'reservations.number')
->where('notifications.status', '=', 1)->get();
return Response::json($awaitLists);
In my Notification model
public function Reservation() {
return $this->belongsTO('Reservation');
}
In my Reservation Model
public function notification() {
return $this->hasMany('Notification');
}
So notification belongs to reservation while reservation has a 1 to many relationship
My question is why can't what I have tried works. I keep getting Unknown column 'reservation.number' but i do have column called number in the reservations table. I know they is a way to use eloquent relationship mapper to do this.
This should do it:
$notifications = Notification::where('status','=',1)->get();
foreach($notifications as $notification) {
$id = $notification->id;
$num = $notification->reservation->number;
$await = [$id,$num];
var_dump($await);
}
The error you're seeing is because eager loading relationships doesn't actually perform a join. It uses two separate queries, and then the relationship fields are assigned after the queries are run.
So, when you do Notification::with('Reservation')->get(), it is running two SQL statements, approximately:
Notification::with('Reservation')->get();
// select * from notifications;
// select * from reservations where id in (?, ?, ...);
You can see the actual queries run with a dd(DB::getQueryLog()), if you're interested.
How you move forward depends on what you need to do. If you need to duplicate your existing query exactly, then you'll need to manually perform the joins.
$notifications = Notification::select('notifications.id', 'reservations.number')
->join('reservations', 'notifications.reservation_id', '=', 'reservations.id`)
->where('notifications.status', '=', 1)
->get();
foreach($notifications as $notification) {
print_r($notification->number);
}
Otherwise, you can just use the objects as they are built by Laravel:
$notifications = Notification::with('Reservation')->where('status', '=', 1)->get();
foreach($notifications as $notification) {
print_r($notification->Reservation->number);
}

Resources