Query multiple, nested Relationships in Laravel/Eloquent - php

I'm trying to build a simple news feed for posts in Laravel with Eloquent.
I basically want to retrieve all Posts...
where I am author
where people I follow are author (followable)
where people I follow have commented on
where people with same field_id are author
where poeple with same school_id are author
in one query.
As I never worked intensivly with joined/combined SQL queries, any help on this is greatly appreciated!
My Tables
users table
+----+
| id |
+----+
posts table
+----+-----------+-------------+
| id | author_id | author_type |
|----|-----------|-------------|
| | users.id | 'App\User' |
+----+-----------+-------------+
comments table
+----+----------------+------------------+-----------+-------------+
| id | commentable_id | commentable_type | author_id | author_type |
|----|----------------|------------------|-----------|-------------|
| | posts.id | 'App\Post' | users.id | 'App\User' |
+----+----------------+------------------+-----------+-------------+
schoolables table
+---------+-----------+----------+
| user_id | school_id | field_id |
+---------+-----------+----------+
followables table
+-------------+---------------+---------------+-----------------+
| follower_id | follower_type | followable_id | followable_type |
|-------------|---------------|---------------|-----------------|
| users.id | 'App\User' | users.id | 'App\User' |
+-------------+---------------+---------------+-----------------+
My Models
class Post extends Model
{
/**
* #return \Illuminate\Database\Eloquent\Relations\MorphTo
*/
public function author()
{
return $this->morphTo();
}
/**
* #return \Illuminate\Database\Eloquent\Relations\MorphMany
*/
public function comments()
{
return $this->morphMany(Comment::class, 'commentable');
}
}
class Comment extends Model
{
/**
* #return \Illuminate\Database\Eloquent\Relations\MorphTo
*/
public function author()
{
return $this->morphTo();
}
/**
* #return \Illuminate\Database\Eloquent\Relations\MorphTo
*/
public function commentable()
{
return $this->morphTo();
}
}
class User extends Model
{
/**
* #return \Illuminate\Database\Eloquent\Relations\MorphMany
*/
public function posts()
{
return $this->morphMany(Post::class, 'author')->orderBy('created_at', 'desc');
}
/**
* #return \Illuminate\Database\Eloquent\Relations\MorphMany
*/
public function comments()
{
return $this->morphMany(Comment::class, 'author')->orderBy('created_at', 'desc');
}
/**
* #return \Illuminate\Database\Eloquent\Relations\BelongsToMany
*/
public function schoolables()
{
return $this->hasMany(Schoolable::class);
}
/**
* #return \Illuminate\Database\Eloquent\Relations\MorphMany
*/
public function following()
{
return $this->morphMany(Followable::class, 'follower');
}
}

You can try to use left joins for this query, but it would become a complex thing, because you have to make all the joins with leftJoins and then a nested orWhere clause on all the
$posts = Post::leftJoin('..', '..', '=', '..')
->where(function($query){
$query->where('author_id', Auth::user()->id); // being the author
})->orWhere(function($query){
// second clause...
})->orWhere(function($query){
// third clause...
.....
})->get();
I don't think this will be manageable, so I would advice using UNIONS, http://laravel.com/docs/5.1/queries#unions
So it would be something like..
$written = Auth::user()->posts();
$following = Auth::user()->following()->posts();
After getting the different queries, without getting the results, you can unite them..
$posts = $written->union($following)->get();
Hopefully this will direct you in the right direction

Okay, so you want to retrieve all posts where any of the 5 conditions above apply. The trick to writing such queries is to break them up into smaller, more manageable pieces.
$query = Post::query();
So let's say you are $me.
The ids of users you are following can be obtained with
$followingUserIds = $me
->following()
->where('followable_type', User::class)
->lists('followable_id');
The ids of users in the same fields as you can be obtained with
$myFieldIds = $me->schoolables()->lists('field_id');
$sharedFieldUserIds = Schoolable::whereIn('field_id', $myFieldIds)->lists('user_id');
Similarly, users in the same school as you can be obtained with
$mySchoolIds = $me->schoolables()->lists('school_id');
$sharedSchoolUserIds = Schoolable::whereIn('school_id', $mySchoolIds)->lists('user_id');
Let's define each of those conditions:
Where I am author
$query->where(function($inner) use ($me) {
$inner->where('posts.author_type', User::class);
$inner->where('posts.author_id', $me->id);
});
Where people I follow are author (followable)
$query->orWhere(function($inner) use ($followingUserIds) {
$inner->where('posts.author_type', User::class);
$inner->whereIn('posts.author_id', $followingUserIds);
});
where people I follow have commented on
This one is actually slightly tricky: we need to use the ->whereHas construct, which finds posts with at least 1 comment matching the subquery.
$query->orWhereHas('comments', function($subquery) use ($followingUserIds) {
$subquery->where('comments.author_type', User::class);
$subquery->whereIn('comments.author_id', $followingUserIds);
});
The remaining two are simple.
where people with same field_id are author
$query->orWhere(function($inner) use ($sharedFieldUserIds) {
$inner->where('posts.author_type', User::class);
$inner->whereIn('posts.author_id', $sharedFieldUserIds);
});
and you can see where this is going
where poeple with same school_id are author
$query->orWhere(function($inner) use ($sharedSchoolUserIds) {
$inner->where('posts.author_type', User::class);
$inner->whereIn('posts.author_id', $sharedSchoolUserIds);
});
To get the matching posts, you just need to do
$posts = $query->get();
While constructing the query from scratch works in this particular case, it will create a fairly brittle structure if your requirements ever change. For added flexibility, you probably want to build query scopes on the Post and Comments models for each of those components. That will mean that you only need to figure out one time what it means for a post to be authored by someone a user follows, then you could simply do Post::authoredBySomeoneFollowedByUser($me) to get the collection of posts authored by someone you follow.

Related

Laravel Relationship with integer[] type of postgresql column

I have categories table and products table. in products table have category_id column type of integer[].
ex: {1,2,3}
.
And I need products list with category relation which categories.id exist products.category_id
I tried in model Product:
public function category()
{
return $this->belongsTo(Category::class, \DB::raw("ANY(category_id)"), 'id');
}
no get category is null.
you should use belongs to many relation.
because integer[] type is for saving arrays of ints.
try to set it in your model like this:
in your Product(model) you will get this relation method:
public function categories(): BelongsToMany
{
return $this->belongsToMany(Category::class);
}
And in your Category(model):
public function products(): BelongsToMany
{
return $this->belongsToMany(Product::class);
}
Refrence
You can try this using laravel query builder
public function category()
{
return DB::table('products')
->join('categories', 'products.category_id', '=', 'categories.id')
->get();
}
First of all, I dont think it's possible to do this with the Laravel Relationship Methods.
Second of all, if you are using Postgres, which is a relational Database, you should definitely read up on the fundamentals of database normalization.
I would recommend you have a so called pivot table, that links your products to your categories, which could look something like this:
Disclaimer: You dont need to create a Model for this. Just make a migration with php artisan make:migration create_categories_products_table
categories_products
| id | category_id | product_id |
|---------------------|------------------|---------------------|
| 55 | 1 | 5 |
| 56 | 2 | 5 |
| 57 | 3 | 5 |
| 58 | 1 | 6 |
This table links your tables and this is much more easy to handle than some arrays stored as json.. maybe think about it, it is not that much work to do. You can read upon it on the Laravel Documentation or try searching on google for pivot tables and normalization.
When you have done that:
Now you can just use the Laravel belongsToMany Relationship like so:
// Product.php
public function categories()
{
return $this->belongsToMany(Category::class, 'categories_products');
}
// Category.php
public function products()
{
return $this->belongsToMany(Product::class, 'categories_products');
}
I can't relation but with attribute i can get categories
firstly cast category_id to array and
public function getCategoriesAttribute()
{
return Category::whereIn('id',$this->category_id)->get();
}
and it works

How get self value from row to use in another select query in the self eloquent model?

I'm new at the laravel, Just started at few days ago and I already searched about this but nothing found about my question.
For example... I have the offers and offers_prices table.
offers:
+----+-----------+
| id | name |
+----+-----------+
| 1 | Product 1 |
| 2 | Product 2 |
| 3 | Product 3 |
| 4 | Product 4 |
+----+-----------+
offers_prices:
+----+------------+-------+---------------------+
| id | offer_id | price | created_at |
+----+------------+-------+---------------------+
| 1 | 1 | 65.90 | 2020-12-17 16:00:00 |
| 2 | 1 | 64.99 | 2020-12-17 17:00:00 |
| 3 | 1 | 58.90 | 2020-12-17 18:00:00 |
| 4 | 1 | 60.99 | 2020-12-17 19:00:00 |
+----+------------+-------+---------------------+
To get the current offer price (60.99) I have created the function below to get it from a relationship eloquent model:
Models/Offer.php
public function price() {
return parent::hasOne(OfferPrices::class)
->where("date", "<=", \Carbon\Carbon::now()->toDateTimeString())
->orderBy("date", "DESC")
->limit(1);
}
And now I want to get the most recent previous price but higher than current price like in the query below:
SELECT *
FROM offers_prices
WHERE offers_prices.offer_id = 1
AND
offers_prices.price > (
SELECT offers_prices.price
FROM offers_prices
WHERE offers_prices.offer_id = 1
AND offers_prices.date <= NOW()
ORDER BY offers_prices.date DESC
LIMIT 1
)
AND offers_prices.date <= NOW()
ORDER BY offers_prices.date DESC
LIMIT 1;
It means that the recent previous and higher than current price can't be 58.90 because it's lower than current (60.99). Must be 64.99.
How can I do it? I have tried creating another function in the self eloquent model:
Models/Offer.php
public function prev_high_price() {
return parent::hasOne(OfferPrices::class)
->where("date", "<=", \Carbon\Carbon::now()->toDateTimeString())
->where("price", ">", $this->price()->price)
->orderBy("date", "DESC")
->limit(1);
}
But it doesn't work..
You're using the hasOne relationship for a database relationship that looks more like a hasMany. Your Offers can have many Prices. Your table naming is outwith convention too as you're not using a pivot table but have named your offers_prices table as if it was.
Off topic to the question but relevant to your situation, it's advisable to not store currency values as floats or doubles. Store them as integers in the lowest denominator.
To answer your question, given the following two migrations:
Offers table migration
class CreateOffersTable extends Migration
{
/**
* Run the migrations.
*
* #return void
*/
public function up()
{
Schema::create('offers', function (Blueprint $table) {
$table->id();
$table->timestamps();
$table->string('name');
});
}
/**
* Reverse the migrations.
*
* #return void
*/
public function down()
{
Schema::dropIfExists('offers');
}
}
Prices table migration
class CreatePricesTable extends Migration
{
/**
* Run the migrations.
*
* #return void
*/
public function up()
{
Schema::create('prices', function (Blueprint $table) {
$table->id();
$table->timestamps();
$table->foreignId('offer_id');
$table->integer('price');
$table->foreign('offer_id')->references('id')->on('offers');
});
}
/**
* Reverse the migrations.
*
* #return void
*/
public function down()
{
Schema::dropIfExists('prices');
}
}
Define the following in your Offer model:
class Offer extends Model
{
use HasFactory;
public function prices()
{
return $this->hasMany(Price::class);
}
public function lastPrice()
{
return $this->prices()->orderBy('id', 'desc')->limit(1);
}
public function lastHighPrice()
{
return $this->prices()
->where('id', '<', $this->lastPrice()->first()->id)
->orderBy('price', 'desc')
->limit(1);
}
}
Run your migration and database seeders (if you have any, otherwise just add data manually). In your terminal and whilst you're current working directory is your Laravel project, fire up tinker:
php artisan tinker
From inside tinker, you can now play around with your data and models.
// get a collection of all prices for the Offer with id 1
Offer::find(1)->prices
// get a collection with the last price for the offer with id 1
Offer::find(1)->lastPrice
// get a collection with the previous highest price for the offer with id 1
Offer::find(1)->lastHighPrice

Laravel Eloquent Inner Join on Self Referencing Table

I'm trying to inner join a users table to itself using an eloquent model. I've looked everywhere but can't seem to find a solution to this without creating two queries which is what I am currently doing.
A users table has a many to many relationship itself through the pivot table friends
I tried and failed inner joining Users::class to itself. The best I can get at an inner join is by running two queries and seeing if there is an overlap. Thus one person has reached out to the other and vice versa.
friends | users
----------|------
send_id | id
receive_id| name
is_blocked|
sample data & expected result
users.id | name
---------|------
1 | foo
2 | bar
3 | baz
friends
send_id | receive_id | is_blocked
--------|------------|-----------
1 | 2 | 0
2 | 1 | 0
1 | 3 | 0
3 | 1 | 1
2 | 3 | 0
The user should have an eloquent relationship called friends. It should be what you expect comes out of requestedFriends or receivedFriends just joined.
foo->friends
returns `baz`
bar->friends
returns `foo`
baz->friends
returns empty collection
currently using
// User.php
public function requestedFriends()
{
$left = $this->belongsToMany(User::class, 'friends','send_id','receive_id')
->withPivot('is_blocked')
->wherePivot('is_blocked','=', 0)
->withTimestamps();
return $left;
}
public function receivedFriends()
{
$right = $this->belongsToMany(User::class, 'friends','receive_id','send_id')
->withPivot('is_blocked')
->wherePivot('is_blocked','=', 0)
->withTimestamps();
return $right;
}
public function friends()
{
$reqFriends = $this->requestedFriends()->get();
$recFriends = $this->receivedFriends()->get();
$req = explode(",",$recFriends->implode('id', ', '));
$intersect = $reqFriends->whereIn('id', $req);
return $intersect;
}
Research so far
Laravel Many to many self referencing table only works one way -> old question, but still relevant
https://github.com/laravel/framework/issues/441#issuecomment-14213883 -> yep, it works… but one way.
https://laravel.com/docs/5.8/collections#method-wherein
currently the only way I have found to do this in eloquent.
https://laravel.com/docs/5.7/queries#joins -> Ideally I would find a solution using an innerjoin onto itself, but no matter which way I put the id's I couldn't get a solution to work.
A solution would
A solution would inner join a self referencing table using eloquent in laravel 5.7 or 5.8, where a relationship only exists if send_id & receive_id are present on multiple rows in the friends table.
OR
Somehow let the community know that this can't be done.
Thanks in advance!
I have not checked this solution in every detail yet, but I have written a "ManyToMany" Class extending the "BelongsToMany" Class shipped with laravel, which appears to work.
The class basically just overrides the "get" method, duplicating the original query, "inverting" it and just performing a "union" on the original query.
<?php
namespace App\Database\Eloquent\Relations;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
class ManyToMany extends BelongsToMany
{
/**
* Execute the query as a "select" statement.
*
* #param array $columns
* #return \Illuminate\Database\Eloquent\Collection
*/
public function get($columns = ['*'])
{
// duplicated from "BelongsToMany"
$builder = $this->query->applyScopes();
$columns = $builder->getQuery()->columns ? [] : $columns;
// Adjustments for "Many to Many on self": do not get the resulting models here directly, but rather
// just set the columns to select and do some adjustments to also select the "inverse" records
$builder->addSelect(
$this->shouldSelect($columns)
);
// backup order directives
$orders = $builder->getQuery()->orders;
$builder->getQuery()->orders = [];
// clone the original query
$query2 = clone($this->query);
// determine the columns to select - same as in original query, but with inverted pivot key names
$query2->select(
$this->shouldSelectInverse( $columns )
);
// remove the inner join and build a new one, this time using the "foreign" pivot key
$query2->getQuery()->joins = array();
$baseTable = $this->related->getTable();
$key = $baseTable.'.'.$this->relatedKey;
$query2->join($this->table, $key, '=', $this->getQualifiedForeignPivotKeyName());
// go through all where conditions and "invert" the one relevant for the inner join
foreach( $query2->getQuery()->wheres as &$where ) {
if(
$where['type'] == 'Basic'
&& $where['column'] == $this->getQualifiedForeignPivotKeyName()
&& $where['operator'] == '='
&& $where['value'] == $this->parent->{$this->parentKey}
) {
$where['column'] = $this->getQualifiedRelatedPivotKeyName();
break;
}
}
// add the duplicated and modified and adjusted query to the original query with union
$builder->getQuery()->union($query2);
// reapply orderings so that they are used for the "union" rather than just the individual queries
foreach($orders as $ord)
$builder->getQuery()->orderBy($ord['column'], $ord['direction']);
// back to "normal" - get the models
$models = $builder->getModels();
$this->hydratePivotRelation($models);
// If we actually found models we will also eager load any relationships that
// have been specified as needing to be eager loaded. This will solve the
// n + 1 query problem for the developer and also increase performance.
if (count($models) > 0) {
$models = $builder->eagerLoadRelations($models);
}
return $this->related->newCollection($models);
}
/**
* Get the select columns for the relation query.
*
* #param array $columns
* #return array
*/
protected function shouldSelectInverse(array $columns = ['*'])
{
if ($columns == ['*']) {
$columns = [$this->related->getTable().'.*'];
}
return array_merge($columns, $this->aliasedPivotColumnsInverse());
}
/**
* Get the pivot columns for the relation.
*
* "pivot_" is prefixed ot each column for easy removal later.
*
* #return array
*/
protected function aliasedPivotColumnsInverse()
{
$collection = collect( $this->pivotColumns )->map(function ($column) {
return $this->table.'.'.$column.' as pivot_'.$column;
});
$collection->prepend(
$this->table.'.'.$this->relatedPivotKey.' as pivot_'.$this->foreignPivotKey
);
$collection->prepend(
$this->table.'.'.$this->foreignPivotKey.' as pivot_'.$this->relatedPivotKey
);
return $collection->unique()->all();
}
}
I came across the same problem quite some time ago and have thus been following this problem closely and have made a lot of research. I have come across some of the solutions you have also found, and some more, and also have thought of other solutions that I summed here, mostly how to get both user_ids in the same column. I am afraid they will all not work well. I am also afraid that using any custom classes will stop you from using all of Laravel's handy relation features (especially eager loading). So I still thought what one could do, and, until one comes up with a hasMany-function on many columns, I think I have come up with a possible solution yesterday. I will show it first and then apply it to your project.
My project
Initial solution
In my project, one user partners with another one (= partnership) and then later will be assigned a commission. So I had the following tables:
USERS
id | name
---------|------
1 | foo
2 | bar
17 | baz
20 | Joe
48 | Jane
51 | Jim
PARTNERSHIPS
id | partner1 | partner2 | confirmed | other_columns
----|-----------|-----------|-----------|---------------
1 | 1 | 2 | 1 |
9 | 17 | 20 | 1 |
23 | 48 | 51 | 1 |
As each user should always have only one active partnership, the non-active being soft-deleted, I could have helped myself by just using the hasMany function twice:
//user.php
public function partnerships()
{
$r = $this->hasMany(Partnership::class, 'partner1');
if(! $r->count() ){
$r = $this->hasMany(Partnership::class, 'partner2');
}
return $r;
}
But if I had wanted to lookup all partnerships of a user, current and past, this of course, wouldn't have worked.
New solution
Yesterday, I came up with the solution, that is close to yours, of using a pivot table but with a little difference of using another table:
USERS
(same as above)
PARTNERSHIP_USER
user_id | partnership_id
--------|----------------
1 | 1
2 | 1
17 | 9
20 | 9
48 | 23
51 | 23
PARTNERSHIPS
id | confirmed | other_columns
----|-----------|---------------
1 | 1 |
9 | 1 |
23 | 1 |
// user.php
public function partnerships(){
return $this->belongsToMany(Partnership::class);
}
public function getPartners(){
return $this->partnerships()->with(['users' => function ($query){
$query->where('user_id', '<>', $this->id);
}])->get();
}
public function getCurrentPartner(){
return $this->partnerships()->latest()->with(['users' => function ($query){
$query->where('user_id', '<>', $this->id);
}])->get();
}
// partnership.php
public function users(){
return $this->belongsToMany(User::class);
}
Of course, this comes with the drawback that you always have to create and maintain two entrances in the pivot table but I think this occasional extra load for the database -- how often will this be altered anyway? -- is preferable to having two select queries on two columns every time (and from your example it seemed that you duplicated the entries in your friends table anyway).
Applied to your project
In your example the tables could be structured like this:
USERS
id | name
---------|------
1 | foo
2 | bar
3 | baz
FRIENDSHIP_USER
user_id | friendship_id
---------|------
1 | 1
2 | 1
3 | 2
1 | 2
FRIENDSHIPS
id |send_id* | receive_id* | is_blocked | [all the other nice stuff
--------|---------|-------------|------------|- you want to save]
1 | 1 | 2 | 0 |
2 | 3 | 1 | 0 |
[*send_id and receive_id are optional except
you really want to save who did what]
Edit: My $user->partners() looks like this:
// user.php
// PARTNERSHIPS
public function partnerships(){
// 'failed' is a custom fields in the pivot table, like the 'is_blocked' in your example
return $this->belongsToMany(Partnership::class)
->withPivot('failed');
}
// PARTNERS
public function partners(){
// this query goes forth to partnerships and then back to users.
// The subquery excludes the id of the querying user when going back
// (when I ask for "partners", I want only the second person to be returned)
return $this->partnerships()
->with(['users' => function ($query){
$query->where('user_id', '<>', $this->id);
}]);
}

Laravel: Get users based on Eloquent relation

I have a User model which has an attribute type among other attributes. Type is used to identify parents and children.
Parent and children (students) have many-to-many relationship.
Also students belong to one or many groups (model Group).
User model
/**
* Filter the scope of users to student type.
*
* #param $query
*/
public function scopeStudent($query){
$query->where('type', '=', 'std');
}
/**
* Filter the scope of users to parent type.
*
* #param $query
*/
public function scopeParent($query){
$query->where('type', '=', 'prt');
}
/**
* List of groups the user is associated with.
*
* #return \Illuminate\Database\Eloquent\Relations\BelongsToMany
*/
public function groups(){
return $this->belongsToMany('\App\Group', 'group_user_assoc')
->withTimestamps();
}
/**
* List of students associated with the user(parent).
*
* #return \Illuminate\Database\Eloquent\Relations\BelongsToMany
*/
public function children(){
return $this->belongsToMany('\App\User', 'student_parent_assoc', 'parent_id', 'student_id')
->withPivot('relation');
}
/**
* List of parents associated with the user(student).
*
* #return \Illuminate\Database\Eloquent\Relations\BelongsToMany
*/
public function parents(){
return $this->BelongsToMany('\App\User', 'student_parent_assoc', 'student_id', 'parent_id')
->withPivot('relation');
}
The aforementioned relations are working correctly.
Below are my association tables.
student_parent_assoc
----------------------
+------------+------------------+------+-----+
| Field | Type | Null | Key |
+------------+------------------+------+-----+
| student_id | int(10) unsigned | NO | PRI |
| parent_id | int(10) unsigned | NO | PRI |
| relation | varchar(25) | YES | |
+------------+------------------+------+-----+
group_user_assoc
----------------------
+------------+------------------+------+-----+
| Field | Type | Null | Key |
+------------+------------------+------+-----+
| group_id | int(10) unsigned | NO | MUL |
| user_id | int(10) unsigned | NO | MUL |
| created_at | timestamp | NO | |
| updated_at | timestamp | NO | |
+------------+------------------+------+-----+
I need to find students who do not belong to any group along with their parents. I have managed to find students like so
$students = User::student()
->whereDoesntHave('groups')
->get();
Question:
Now I want to find parents of these students. But I am not able to build an eloquent query for the same. How do I do it?
Note: I could get collection of students from above query and run a foreach loop to get their parents like so
$parents = new Collection;
foreach($students as $student) {
foreach($student->parents as $parent) {
$parents->push($parent);
}
}
$parents = $parents->unique();
But I need a Builder object and not a Collection as I am using Datatables server side processing with Yajra/datatables.
for loading parents relation you hae to use eager loading.
2 methods are with($relation) and load($relation). Difference is just you get parents already with result objects or load them later.
So in your example to get parents you can use with('parents') or if you want to modify resulted set:
User::student()
->with(['parents' => function ($parentsQueryBuilder) {
$parentsQueryBuilder->where('condition', 1)->whereIn('condition2', [1,2,3]);
}])
->whereDoesntHave('groups')
->get();
Then you will get your parents in a relationship aswell but performance will be high cause you will spend only one query to load parents to your objects. Then you can pluck if needed them in one collection like this:
$students->pluck('parents')->flatten()->unique();
Or example 2 - if you just need all parents related to selected students - almost the same what eager loading does:
$studentIds = $students->modeKeys();
User::parent()->whereHas('children', function ($query) use($studentIds) {
$query->whereIn('id', $studentIds);
})->get();
UPDATED
For getting builder of parents try this:
/** BelongsToMany <- relation query builder */
$relation = with(new User)->query()->getRelation('parents');
$relation->addEagerConstraints($students->all());
This will create for you new instance of BelongsToMany relation and attach Constraints of whereIn($studentIds) to it. Then hitting ->get() on it you have to receive related $parents
Well, I managed to solve it like so
$students = User::student()
->with('parents')
->whereDoesntHave('groups')
->has('parents') //to get only those students having parents
->get();
$parent_ids = array();
// get ids of all parents
foreach ($students as $student) {
foreach ($student->parents as $parent) {
$parent_ids[] = $parent->user_id;
}
}
// build the query
$users = User::parent()
->with('children')
->whereIn('user_id', $parent_ids);
I would still like it if someone could suggest a better and simple approach.

Laravel: how to get average on nested hasMany relationships (hasManyThrough)

I have three tables:
products: id|name|description|slug|category_id|...
reviews: id|product_id|review_text|name|email|...
review_rows id|review_id|criteria|rating
the review table stores the review text, writer of the review and has a foreign product_id key. The review_rows table stores the ratings for different criteria like:
----------------------------------------
| id | criteria | rating | review_id |
----------------------------------------
| 1 | price | 9 | 12 |
----------------------------------------
| 2 | service | 8 | 12 |
----------------------------------------
| 3 | price | 6 | 54 |
----------------------------------------
| 4 | service | 10 | 54 |
----------------------------------------
review rows are linked to the review table with the review_id foreign key. I've set up my model relationships like this:
Product -> hasMany -> Review
Review -> belongsTo -> Product
Review -> hasMany -> ReviewRow
ReviewRow -> belongsTo -> Review
Now I would like to display the average rating for a product on my category and product pages. How can I achieve this?
I need to sum and average all the reviewRows per review and then sum and average all of those for each review to end up with the overall rating for that product. Is this possible via Eloquent or do I need a different solution or a different database design/structure?
Thanks in advance!
You need something like this http://softonsofa.com/tweaking-eloquent-relations-how-to-get-hasmany-relation-count-efficiently/ only slightly adjusted to match your needs:
public function reviewRows()
{
return $this->hasManyThrough('ReviewRow', 'Review');
}
public function avgRating()
{
return $this->reviewRows()
->selectRaw('avg(rating) as aggregate, product_id')
->groupBy('product_id');
}
public function getAvgRatingAttribute()
{
if ( ! array_key_exists('avgRating', $this->relations)) {
$this->load('avgRating');
}
$relation = $this->getRelation('avgRating')->first();
return ($relation) ? $relation->aggregate : null;
}
Then as simple as this:
// eager loading
$products = Product::with('avgRating')->get();
$products->first()->avgRating; // '82.200' | null
// lazy loading via dynamic property
$product = Product::first()
$product->avgRating; // '82.200' | null
Maybe you can try with Eloquent relationships and a little help from php function array_reduce
//model/Reviews.php
public function sum() {
return array_reduce($this->hasMany('ReviewRows')->lists('rating'), "sumItems");
}
public function sumItems ($carry, $item) {
$carry += $item;
return $carry;
}
Or with Eloquent RAW querys like:
//model/Reviews.php
public function avg() {
$result = $this->hasMany('ReviewRows')
->select(DB::raw('avg(rating) average'))
->first();
return $result->average;
}
Simple and easy solution. Add this into product model
protected $appends = ["avg_rating"];
public function reviewRows()
{
return $this->hasManyThrough('App\ReviewRow','App\Review','product_id','review_id');
}
public function getAvgRatingAttribute()
{
return round($this->reviewRows->average('rating'),2);
}
see https://github.com/faustbrian/laravel-commentable
public function comments(): MorphMany
{
return $this->morphMany($this->commentableModel(), 'commentable');
}
public function avgRating()
{
return $this->comments()->avg("rating");
}
$products = \App\Models\Products::with(
[
"comments" => function ($q) {
$q->with(["children" => function ($qch) {
$qch->take(2);
}
])->withCount("children")->where("parent_id", '=', null);
},]
)->take(5)->get();
foreach ($products as &$product) {
$product["avgRating"] = $product->avgRating();
}
dd($products);
use withAvg() as mentioned in laravel official documentation here

Categories