Eloquent whereHas Relationship with constraint on particular related Entity - laravel

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();

Related

Laravel: Property [user] does not exist on this collection instance

In Laravel, I've Orders model where each order belongs to only one user,
I want to return only user names and orders ids when I fetch the orders, So in my OrdersController I've this function
public function getAllOrders()
{
$orders = Order::all();
return new OrderResource($orders);
}
and in my OrderResource I've
public function toArray($request)
{
return [
'user' => $this->user,
'id' => $this->id,
];
}
I'm getting an error saying Property [user] does not exist on this collection instance.
I think the reason is coming from the fact that $orders is a collection and I should loop through it and get for each order get the user associated with it, but I've no idea how to do that.
Note: I'm using oneToMany and belongsTo on Users and Orders Model. so orders table doesn't have a user column, I want to get the user from the relationship.
When using a collection use like this;
public function getAllOrders()
{
return OrderResource::collection(Order::all());
}
When using a model use like this;
public function getOrder($id)
{
return new OrderResource(Order::find($id));
}
More information in the documentation: https://laravel.com/docs/7.x/eloquent-resources#concept-overview
Note: To avoid N+1 queries, you should get your orders like this; Order::with('user')->get(); instead of Order::all()
The difference is that Order::with('user')->get(); will perform two queries.
select * from orders
select * from users where id in (?, ?, ?, ?, ...)
Whereas Order::all() will perform N+1 queries (N = number of orders)
select * from orders
select * from users where id = ?
select * from users where id = ?
select * from users where id = ?
... and so on

Laravel Pivot Table, Get Room belonging to users

The structure of my pivot table is
room_id - user_id
I have 2 users that exist in the same room.
How can I get the rooms they both have in common?
It would be nice to create a static class to have something like this.
Room::commonToUsers([1, 5]);
Potentially I could check more users so the logic must not restrict to a certain number of users.
Room::commonToUsers([1, 5, 6, 33, ...]);
I created a Laravel project and make users, 'rooms', 'room_users' tables and their models
and defined a static function in RoomUser Model as below :
public static function commonToUsers($ids)
{
$sql = 'SELECT room_id FROM room_users WHERE user_id IN (' . implode(',', $ids) . ') GROUP BY room_id HAVING COUNT(*) = ' . count($ids);
$roomsIds = DB::select($sql);
$roomsIds = array_map(function ($item){
return $item->room_id;
}, $roomsIds);
return Room::whereIn('id', $roomsIds)->get();
}
in this method, I use self join that the table is joined with itself, A and B are different table aliases for the same table, then I applied the where condition between these two tables (A and B) and work for me.
I hope be useful.
I don't know the names of your relations, but I guess you can do like this :
$usersIds = [1, 5];
$rooms = Room::whereHas('users', function($query) use ($usersIds) {
foreach ($usersIds as $userId) {
$query->where('users.id', $userId);
}
})->get();
It should work. whereHas allows you to query your relation. If you need to have a static method, you can add a method in your model.
There might be a more efficient way but laravel collection does have an intersect method. You could create a static function that retrieves and loop through each object and only retain all intersecting rooms. something like this
public static function commonToUsers($userArr){
$users = User::whereIn('id',$userArr)->get();
$rooms = null;
foreach($users as $user){
if($rooms === null){
$rooms = $user->rooms;
}else{
$rooms = $rooms->intersect($user->rooms);
}
}
return $rooms;
}
This code is untested but it should work.
Room has many users, user has many rooms, so you can find the room which have those two users.
If your pivot table's name is room_users, then you can easily get the common room like this:
public static function commonToUsers($user_ids) {
$room = new Room();
foreach($user_ids as $user_id) {
$room->whereHas('users', function($query) use ($user_id) {
$query->where('room_users.user_id', $user_id);
});
}
return $room->get();
}
This code will convert to raw sql:
select *
from `rooms`
where exists (
select * from `rooms` inner join `room_users` on `rooms`.`id` = `room_users`.`room_id` where `rooms`.`id` = `room_users`.`room_id` and `room_users`.`user_id` = 1
)
and exists
(
select * from `rooms` inner join `room_users` on `rooms`.`id` = `room_users`.`room_id` where `rooms`.`id` = `room_users`.`room_id` and `room_users`.`user_id` = 5
)

Call to a member function paginate() on array error in laravel

I want to create pagination in laravel but it gives error.
here is error "Call to a member function paginate() on array"
this is my query
$blog = DB::select("select blog_post.*,(select img_name from blog_image WHERE blog_image.bid=blog_post.bid limit 0,1) as img_name,
(SELECT count(*) FROM likes WHERE likes.bid =blog_post.bid) AS likes,
(SELECT count(*) FROM comment WHERE comment.bid =blog_post.bid ) as comments ,
(SELECT count(*) FROM blog_share WHERE blog_share.bid =blog_post.bid ) as share
from blog_post WHERE status=1 AND is_draft=0 AND is_publish=1 AND is_delete=0")->paginate(2);
You first need to rewrite your query using Laravel Eloquent. For example:
$blog = DB::table('blog_post')->where([ 'status' => 1, 'is_draft' => 0, 'is_publish' => 1, 'is_delete' = 0 ])->paginate(2);
You don't strictly need to put all the counts / inner-queries into one query. You can use Eloquent Relationships and Mutators to get that information for you. For example, the following Relationship to get the image:
class BlogPost extends Model
{
public function image()
{
return $this->hasOne('BlogImage');
}
}

Laravel - Eloquent "Has", "With", "WhereHas" - What do they mean?

I've found the concept and meaning behind these methods to be a little confusing, is it possible for somebody to explain to me what the difference between has and with is, in the context of an example (if possible)?
With
with() is for eager loading. That basically means, along the main model, Laravel will preload the relationship(s) you specify. This is especially helpful if you have a collection of models and you want to load a relation for all of them. Because with eager loading you run only one additional DB query instead of one for every model in the collection.
Example:
User > hasMany > Post
$users = User::with('posts')->get();
foreach($users as $user){
$users->posts; // posts is already loaded and no additional DB query is run
}
Has
has() is to filter the selecting model based on a relationship. So it acts very similarly to a normal WHERE condition. If you just use has('relation') that means you only want to get the models that have at least one related model in this relation.
Example:
User > hasMany > Post
$users = User::has('posts')->get();
// only users that have at least one post are contained in the collection
WhereHas
whereHas() works basically the same as has() but allows you to specify additional filters for the related model to check.
Example:
User > hasMany > Post
$users = User::whereHas('posts', function($q){
$q->where('created_at', '>=', '2015-01-01 00:00:00');
})->get();
// only users that have posts from 2015 on forward are returned
The documentation has already explained the usage, so I will use SQL to explain the methods.
Example:
Assuming there is an Order (orders) has many OrderItem (order_items) and you already built the relationship between them:
// App\Models\Order:
public function orderItems() {
return $this->hasMany('App\Models\OrderItem', 'order_id', 'id');
}
These three methods are all based on a relationship.
with
Result: with() return the model object and its related results.
Advantage: It is eager-loading which can prevent the N+1 problem.
When you are using the following Eloquent Builder:
Order::with('orderItems')->get();
Laravel change this code to only two SQL:
// get all orders:
SELECT * FROM orders;
// get the order_items based on the orders' id above
SELECT * FROM order_items WHERE order_items.order_id IN (1,2,3,4...);
And then Laravel merges the results of the second SQL query with the results of the first SQL by foreign key, finally returning the collection results.
So if you selected columns without the foreign_key in a closure, the relationship result will be empty:
Order::with(['orderItems' => function($query) {
// $query->sum('quantity');
$query->select('quantity'); // without `order_id`
}
])->get();
#=> result:
[{ id: 1,
code: '00001',
orderItems: [], // <== is empty
},{
id: 2,
code: '00002',
orderItems: [], // <== is empty
}...
}]
has
Has will return the model's object when its relationship is not empty.
Order::has('orderItems')->get();
Laravel changes this code to one SQL query:
select * from `orders` where exists (
select * from `order_items` where `orders`.`id` = `order_items`.`order_id`
)
whereHas
The methods whereHas and orWhereHas put where conditions on your has queries. These methods allow you to add customized constraints to a relationship constraint.
Order::whereHas('orderItems', function($query) {
$query->where('status', 1);
})->get();
Laravel changes this code to one SQL query:
select * from `orders` where exists (
select *
from `order_items`
where `orders`.`id` = `order_items`.`order_id` and `status` = 1
)

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