Laravel many to many loading related models with count - php

I am trying to link 4 tables and also add a custom field calculated by counting the ids of some related tables using laravel.
I have this in SQL which does what I want, but I think it can be made more efficient:
DB::select('SELECT
posts.*,
users.id AS users_id, users.email,users.username,
GROUP_CONCAT(tags.tag ORDER BY posts_tags.id) AS tags,
COUNT(DISTINCT comments.id) AS NumComments,
COUNT(DISTINCT vote.id) AS NumVotes
FROM
posts
LEFT JOIN comments ON comments.posts_id = posts.id
LEFT JOIN users ON users.id = posts.author_id
LEFT JOIN vote ON vote.posts_id = posts.id
LEFT JOIN posts_tags ON posts_tags.posts_id = posts.id
LEFT JOIN tags ON tags.id = posts_tags.tags_id
GROUP BY
posts.id,
posts.post_title');
I tried to implement it using eloquent by doing this:
$trending=Posts::with(array('comments' => function($query)
{
$query->select(DB::raw('COUNT(DISTINCT comments.id) AS NumComments'));
},'user','vote','tags'))->get();
However the NumComments value is not showing up in the query results.
Any clue how else to go about it?

You can't do that using with, because it executes separate query.
What you need is simple join. Just translate the query you have to something like:
Posts::join('comments as c', 'posts.id', '=', 'c.id')
->selectRaw('posts.*, count(distinct c.id) as numComments')
->groupBy('posts.id', 'posts.post_title')
->with('user', 'vote', 'tags')
->get();
then each post in the collection will have count attribute:
$post->numComments;
However you can make it easier with relations like below:
Though first solution is better in terms of performance (might not be noticeable unless you have big data)
// helper relation
public function commentsCount()
{
return $this->hasOne('Comment')->selectRaw('posts_id, count(*) as aggregate')->groupBy('posts_id');
}
// accessor for convenience
public function getCommentsCountAttribute()
{
// if relation not loaded already, let's load it now
if ( ! array_key_exists('commentsCount', $this->relations)) $this->load('commentsCount');
return $this->getRelation('commentsCount')->aggregate;
}
This will allow you to do this:
$posts = Posts::with('commentsCount', 'tags', ....)->get();
// then each post:
$post->commentsCount;
And for many to many relations:
public function tagsCount()
{
return $this->belongsToMany('Tag')->selectRaw('count(tags.id) as aggregate')->groupBy('pivot_posts_id');
}
public function getTagsCountAttribute()
{
if ( ! array_key_exists('tagsCount', $this->relations)) $this->load('tagsCount');
$related = $this->getRelation('tagsCount')->first();
return ($related) ? $related->aggregate : 0;
}
More examples like this can be found here http://softonsofa.com/tweaking-eloquent-relations-how-to-get-hasmany-relation-count-efficiently/

as of laravel 5.3 you can do this
withCount('comments','tags');
and call it like this
$post->comments_count;
laravel 5.3 added withCount

Related

withExists() or withCount() for nested relationship (Many To Many and One To Many) in Laravel Eloquent

I have the following relationships in models:
Product.php
public function skus()
{
return $this->belongsToMany(Sku::class);
}
Sku.php
public function prices()
{
return $this->hasMany(Price::class);
}
I need to get an attribute indicating whether a product has at least one price or not (in the extreme case, just the number of prices).
Product::withExists('sku.prices') or Product::withCount('sku.prices')
I know about this repository https://github.com/staudenmeir/belongs-to-through, but I prefer to use complex query once
UPDATE: I have already written a sql query for this purpose, but I don't know how to do it in Laravel:
SELECT
*,
EXISTS (SELECT
*
FROM prices
INNER JOIN skus
ON prices.sku_id = skus.id
INNER JOIN product_sku
ON skus.id = product_sku.sku_id
WHERE products.id = product_sku.product_id
) AS prices_exists
FROM products
Here you can get at least one record
$skuPrice = Sku::with('prices')
->has('prices', '>=', 1)
->withCount('prices')
->get();

whereHas vs join -> applying global scopes

I have a query with a few joins and global scopes for each model, for example:
SELECT *
FROM products p
WHERE EXISTS(
SELECT *
FROM orders o
WHERE o.user_id = 4
AND o.status_id = 1
AND o.user_id = 3
AND EXISTS(
SELECT *
FROM suborders s
WHERE s.status_id = 2
)
);
This means that I can simply write a few whereHas statements and my query will have some nested EXIST clauses, but all global scopes (like the user_id on the orders table) will be applied automatically:
$this->builder->whereHas('orders', function ($q) {
$q->where('status_id', '=', 1)
->whereHas('suborder', function ($q) {
$q->where('status_id', '=', 2);
});
});
The problem is that it's slow, it would be much better to have something with plain JOINs instead of ugly nested EXIST clauses:
SELECT *
FROM products p
INNER JOIN orders o ON p.order_id = o.id
INNER JOIN suborders s ON o.id = s.order_id
WHERE o.status_id = 1
AND u.user_id = 3
AND s.status_id = 2;
The problem with this is that I need to use query builder to join these:
$this->builder->join('orders', 'products.order_id', '=', 'orders.id')
->join('suborders', 'orders.id', '=', 'suborders.order_id')
->where('orders.status_id', 1)
->where('suborders.id', 2);
And that will not include any of my global scopes on Order and Suborder model. I need to do it manually:
$this->builder->join('orders', 'products.order_id', '=', 'orders.id')
->join('suborders', 'orders.id', '=', 'suborders.order_id')
->where('orders.status_id', 1)
->where('suborders.id', 2)
->where('orders.user_id', 3);
It's bad, because I need to replicate my global scopes logic every time I write a query like this, while whereHas applies them automatically.
Is there a way to join a table, and have all global scopes from the joined model applied automatically?
I have worked with a similar issue before and come up with some approach.
First, lets define a macro for Illuminate\Database\Eloquent\Builder in a service provider:
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Query\JoinClause;
class AppServiceProvider
{
public function boot()
{
Builder::macro('hasJoinedWith', function ($table) {
return collect(
$this->getQuery()->joins
)
->contains(function (JoinClause $joinClause) use ($table) {
return $joinClause->table === $table;
})
});
}
}
Then, lets define the scopes:
class Product extends Model
{
public function scopeOrderStatus($query, $orderStatusId)
{
if (! $query->hasJoinedWith('orders')) {
$query->join('orders', 'products.order_id', '=', 'orders.id');
}
return $query->where('orders.status_id', $orderStatusId)
}
public function scopeOrderUser($query, $userId)
{
if (! $query->hasJoinedWith('orders')) {
$query->join('orders', 'products.order_id', '=', 'orders.id');
}
return $query->where('orders.user_id', $userId)
}
public function scopeSubOrder($query, $subOrderId)
{
if (! $query->hasJoinedWith('orders')) {
$query->join('orders', 'products.order_id', '=', 'orders.id');
}
if (! $query->hasJoinedWith('suborders')) {
$query->join('suborders', 'orders.id', '=', 'suborders.order_id');
}
return $query->where('suborders.id', $subOrderId)
}
}
Finally, you use the scopes together:
Product::orderStatus(1)
->subOrder(2)
->orderUser(3)
// This is optional. There are possibly duplicate products.
->distinct()
->get();
This is the best approach that I can come up with, there may be better ones.
Actually, that what you describe can't be true (will provide proof below). EXISTS don't make query slower and in 99% of case it make faster!
So laravel don't provide ability to make joins for relations from box.
I saw different solutions for this and package on github, but I will not provide link to it, because when I reviewed logic, found a lot of problems with field selecting and rare cases.
Laravel don't generate code with EXISTS as you describe, it add relation search by ID to each EXISTS subquery like
SELECT *
FROM products
WHERE EXISTS(
SELECT *
FROM orders
WHERE o.user_id = 4
AND o.status_id = 1
AND o.id = p.order_id
AND EXISTS(
SELECT *
FROM suborders s
WHERE s.status_id = 2
AND s.id = o.suborder_id
)
);
attention to AND o.id = p.order_id and AND s.id = o.suborder_id
When you do select with joins, you should exactly set SELECT from main table, to have right filled Model fields.
Global scopes are global. Purpose to be really global. If you have more then 1-2 places without them, then you should find another solution instead of global scopes. Otherwise you application will be very hard to support and write new code. Developer should not remember every time, that there can be global scopes that he should turn off

laravel: convert many to many (but only one related item) mysql query to a laravel query

Hi I'm having trouble converting a mysql query that I've been working on into a laravel eloquent query and need some help.
I have a reservations table which links to a product table with a many to many relationship. I want to pull all the reservations and just the first product it finds regardless of how many products are related to the reservation.
Here's my sql:
SELECT reservations.id,
reservations.play_date,
reservations.group_size,
reservations.status,
reservations.event_title,
t4.product_id,
t4.id AS link_id,
p1.name,
CONCAT_WS(" ", customers.first_name, customers.last_name, customers.group_name) AS customerName,
reservations.event_type
FROM reservations
LEFT JOIN customers ON reservations.customer_id = customers.id
LEFT JOIN
(SELECT *
FROM product_reservation AS t3
GROUP BY t3.reservation_id ) AS t4 ON t4.reservation_id = reservations.id
LEFT JOIN products AS p1 ON t4.product_id = p1.id
I can place this as a raw query but that produces an array with the result - I need to be able to create a query object so I can work with another module on the results
Is there an eloquent way of doing this - or how can I get this query to work in laravel?
Thank you
Yeah, you can use Eloquent relationships. They would look something like this...
class Reservation extends Eloquent
{
public function products()
{
return $this->belongsToMany('Product');
}
}
class Product
{
public function reservations()
{
return $this->belongsToMany('Reservation');
}
}
$reservations = Reservation::with(array('products', function($q) {
$q->take(1);
}))->get();
foreach($reservations as $reservation) {
echo $reservation->name;
foreach($reservation->products as $product) {
echo $product->description;
}
}

Can I query relations using an INNER JOIN instead of two queries in Eloquent?

Can I write something like this:
$post = Post::join(['author'])->find($postId);
$authorName = $post->author->name;
To produce only ONE select with inner join (no 2 selects) and without using DB query builder
SELECT
post.*,
author.*
FROM post
INNER JOIN author
ON author.id = post.author_id
WHERE post.id = ?
You can do it in Eloquent using the join method:
$post = Post::join('author', function($join)
{
$join->on('author.id', '=', 'post.author_id');
})
->where('post.id', '=', $postId)
->select('post.*', 'author.*')
->first();
Please note that your results in $post will be an object where their attributes will correspond to the result set, if two columns has the same name it will be merged. This happen when using:
->select('post.*', 'author.*')
To avoid this, you should create alias to those columns in the select clause as shown below:
->select('post.id AS post_id', 'author.id AS author_id')
Try
Post::join('author',function($join){
$join->on('author.id','=','post.author_id');
})->where('post.id','=',$postId)->select('post.*','author.*');

Running 2 queries in model (codeigniter)?

For example, if I run one query in model:
public function list_users() {
$q = "SELECT user_id, username FROM users";
return $q->result_array();
}
And now, to lists posts from that user, I need to refer to it's id within this function:
public function list_posts() {
$q = "SELECT post_id, post_title, post_content FROM posts
WHERE user_id = what??";
return $q->result_array();
}
OK both of these functions are in Model. Now, How to use RESULT from list_users() in list_posts(). Please have in mind that I need to pass ARRAY of IDs and, to use it only for particular id from list_users() which also returns ARRAY
I know I can use joined query, but that's not the point at all, as I have lots of queries that I need to split
why arent you using a JOIN statement, and making two queries into one?
This will reduce db load, decrease query times, and also reduce clutter in your models.
SELECT p.post_id, p.post_title, p.post_content, u.user_id, u.username FROM posts p LEFT JOIN users u ON u.user_id = p.user_id
you can also do this using active records. Which will avoid having to use full blown queries, and more of a CI methodology to SQL.
http://codeigniter.com/user_guide/database/active_record.html
$this->db->select('p.post_id, p.post_title, p.post_content, u.user_id, u.username');
$this->db->from('posts p');
$this->db->join('users u', 'u.user_id = p.user_id');
$q = $this->db->get();
$q->result();
Edit:
You can return the value as an object.. IE: $this->user_id then reference it in the posts function. Ideally you should call the first function in your Controller, return $user_id and then reference that in your next function.. This is definitely not best case though, you should use JOINs as they are less taxing on the db.
//controller
function test(){
$users = $this->exampleModel->list_users();
//manipulate user data if needed
$posts = $this->exampleModel->list_posts($users);
}

Categories