Just for reference I am using Laravel 5.
I have a two tables
users
id
first name
skills
id
name
and a pivot table
skill_user
skill_id
user_id
if I do a select in MySQL as follows:
select users.id as id, users.first_name, skills.name from users
left join skill_user on users.id = skill_user.user_id
left join skills on skill_user.skill_id=skills.id
I get:
id, first_name, skill
1, Jenna, Reliable
1, Jenna, Organized
2, Alex, Hardworking
3, Barry, Capable
3, Barry, Amiable
3, Barry, Patient
4, Janine, (null)
I pass this through to a view via a Controller:
$peoples = [];
$peoples = \DB::table('users')
->select(\DB::raw('users.id as id, first_name, skill.name as name"'))
->leftJoin('skill_user','users.id','=','skill_user.user_id')
->leftJoin('skills','skill_user.skill_id','=','skills.id')
->get();
return view('find-people', compact(['peoples']));
Now, I want to loop through this in the view (pseudocode):
forelse ( peoples as people )
people - > first_name
people - > skill
empty
no people found
endforelse
Which all works fine in a sense - but the first name gets repeated when there is more than one skill.
I can probably hack a loop of the skills by doing something like comparing user_id to itself but it seems such a clumsy way to do it.
user_id = $peoples->id
while (some looping criteria)
{
write out skills
if($peoples->id != user_id){break;}
}
How do I loop through the recordset in an elegant/eloquent fashion? Or is there a better entirely to do this?
If you define the relationships in your models you don't need to try and construct raw SQL to achieve that. This is what the Laravel ORM "Eloquent" is for!
class People extends Model {
public function skills () {
return $this->hasMany('Skill');
}
}
Then you define the skill model :
class Skill extends Model {
public function People () {
return $this->belongsToMany('People');
}
}
Now you're able to iterate over the People model and for each person, get their ->skills. This way you don't end up with the duplicate issue you're experiencing and you greatly simplify the SQL you're trying to achieve by leveraging the ORM.
Related
I have three models with the following hierarchy :
User
id
....some other properties
Journey
id
user_id
budget
....some other properties
Confirmation
id
journey_id
user_id
....some other properties
I have a HasMany from User to Journey, a HasMany from Journey to Confirmation.
I want to get the sum for a column of the journeys table by going through the confirmations table but I cannot create an intermediate HasManyThrough relation between User and Journey by using Confirmation.
I have tried to do
public function journeysMade(): HasManyThrough
{
return $this->hasManyThrough(Journey::class, Confirmation::class);
}
// And after,
User::with(...)->withSum('journeysMade','budget')
But it was not possible because the relations are not adapted.
With hindsight, the sql query I want to translate would look like
select coalesce(sum(journeys.budget), 0) as income
from journeys
inner join confirmations c on journeys.id = c.journey_id
where c.user_id = ? and c.status = 'finalized';
How can I implement this query considering how I will use my query builder :
$driversQueryBuilder = User::with(['profile', 'addresses']); // Here
$pageSize = $request->input('pageSize', self::DEFAULT_PAGE_SIZE);
$pageNumber = $request->input('pageNumber', self::DEFAULT_PAGE_NUMBER);
$driversPaginator = (new UserFilterService($driversQueryBuilder))
->withStatus(Profile::STATUS_DRIVER)
->withCountry($request->input('country'))
->withSex($request->input('sex'))
->withActive($request->has('active') ? $request->boolean('active') : null)
->get()
->paginate(perPage: $pageSize, page: $pageNumber);
return response()->json(['data' => $driversPaginator]);
The reason why I want to get a builder is because UserFilterService expects a Illuminate\Database\Eloquent\Builder.
Do you have any idea about how I can solve this problem ?
Not 100% sure what exactly you want to sum, but I think you need the following query
$user->whereHas('journeys', function($query) {
$query->whereHas('confirmations', function($subQuery) {
$subQuery->sum('budget);
}
});
If you the above query isn't summing the budget you need, you just add another layer of abstraction with whereHas methods to get exactly what you need. Hope this helps!
EDIT:
$user->whereHas('confirmations', function($q) {
$q->withSum('journeys', 'budget')->journeys_sum_budget;
}
I have 3 tables. Players, player_skills and Skills.
Players
- bb1_player_id
Skills
- bb1_skill_id
Player_skills
- bb1_player_id
- bb1_skill_id
I have created the Player Model with this method.
public function skills()
{
return $this->belongsToMany('App\Skills', 'player_skills', 'bb1_player_id', 'bb1_skill_id');
}
That outputs this Query:
"select `skills`.*, `player_skills`.`bb1_player_id` as `pivot_bb1_player_id`, `player_skills`.`bb1_skill_id` as `pivot_bb1_skill_id` from `skills` inner join `player_skills` on `skills`.`id` = `player_skills`.`bb1_skill_id` where `player_skills`.`bb1_player_id` = ?"
The query works, but I am hoping to get a query that does not every use the id field of the tables and only uses the fields I am specifying. It uses both the id of the skill and player.id at the '?' of the query. Is there a way around this? Or do I need to rework my tables to conform?
I have also tried adding more options like below to no avail.
public function skills()
{
return $this->belongsToMany('App\Skills', 'player_skills','bb1_player_id','bb1_skill_id','bb1_player_id','bb1_skill_id');
}
I have a Pivot table thats used to join two other tables that have many relations per hotel_id. Is there a way I can eagerload the relationship that pulls the results for both tables in one relationship? The raw SQL query, works correctly but when using belongsToMany the order is off.
Amenities Pivot Table
id
hotel_id
distance_id
type_id
Distance Table
id
name
Type Table
id
name
RAW Query (This works fine)
SELECT * FROM amenities a
LEFT JOIN distance d ON a.distance_id = d.id
LEFT JOIN type t ON a.type_id = t.id WHERE a.hotel_id = ?
My "Hotels" Model is using belongsToMany like so
public function distance() {
return $this->belongsToMany('Distance', 'amenities', 'hotel_id', 'distance_id');
}
public function type() {
return $this->belongsToMany('Type', 'amenities', 'hotel_id', 'type_id');
}
This outputs the collection, but they are not grouped correctly. I need to loop these into select fields side by side as entered in the pivot table, so a user can select a "type" and the "distance", but the order is off when using the collection. The raw query above outputs correctly.
Hotels::where('id','=','200')->with('distance', 'type')->take(5)->get();
Ok Solved it. So apparently you can use orderBy on your pivot table. Incase anyone else has this issue this is what I did on both relationships.
public function distance() {
return $this->belongsToMany('Distance', 'amenities', 'hotel_id', 'distance_id')->withPivot('id')->orderBy('pivot_id','desc');
}
public function type() {
return $this->belongsToMany('Type', 'amenities', 'hotel_id', 'type_id')->withPivot('id')->orderBy('pivot_id','desc');
}
It's not really a great practice to include other query building steps in the relationship methods on your models. The relationship method should just define the relationship, nothing else. A cleaner method is to apply eager load constraints. (scroll down a bit) Consider the following.
Hotels::where('id', 200)->with(array(
'distance' => function ($query)
{
$query->withPivot('id')->orderBy('pivot_id','desc');
},
'type' => function ($query)
{
$query->withPivot('id')->orderBy('pivot_id','desc');
},
))->take(5)->get();
If you find that you are eagerly loading this relationship in this way often, consider using scopes to keep things DRY. The end result will allow you to do something like this.
Hotels::where('id', 200)->withOrderedDistance()->withOrderedType()->take(5)->get();
P.S. Your models should be singular. Hotel, not Hotels. The model represents a single record.
Solved by using ->withPivot('id')->orderBy('pivot_id','desc');
Posted answer in the question.
So I am having some trouble figuring out how to do a feed style mysql call, and I don't know if its an eloquent issue or a mysql issue. I am sure it is possible in both and I am just in need of some help.
So I have a user and they go to their feed page, on this page it shows stuff from their friends (friends votes, friends comments, friends status updates). So say I have tom, tim and taylor as my friends and I need to get all of their votes, comments, status updates. How do I go about this? I have a list of all the friends by Id number, and I have tables for each of the events (votes, comments, status updates) that have the Id stored in it to link back to the user. So how can I get all of that information at once so that I can display it in a feed in the form of.
Tim commented "Cool"
Taylor Said "Woot first status update~!"
Taylor Voted on "Best competition ever"
Edit #damiani
So after doing the model changes I have code like this, and it does return the correct rows
$friends_votes = $user->friends()->join('votes', 'votes.userId', '=', 'friend.friendId')->orderBy('created_at', 'DESC')->get(['votes.*']);
$friends_comments = $user->friends()->join('comments', 'comments.userId', '=', 'friend.friendId')->orderBy('created_at', 'DESC')->get(['comments.*']);
$friends_status = $user->friends()->join('status', 'status.userId', '=', 'friend.friendId')->orderBy('created_at', 'DESC')->get(['status.*']);
But I would like them all to happen at once, this is because mysql sorting thousands of records in order is 100x faster then php taking 3 lists, merging them and then doing it. Any ideas?
I'm sure there are other ways to accomplish this, but one solution would be to use join through the Query Builder.
If you have tables set up something like this:
users
id
...
friends
id
user_id
friend_id
...
votes, comments and status_updates (3 tables)
id
user_id
....
In your User model:
class User extends Eloquent {
public function friends()
{
return $this->hasMany('Friend');
}
}
In your Friend model:
class Friend extends Eloquent {
public function user()
{
return $this->belongsTo('User');
}
}
Then, to gather all the votes for the friends of the user with the id of 1, you could run this query:
$user = User::find(1);
$friends_votes = $user->friends()
->with('user') // bring along details of the friend
->join('votes', 'votes.user_id', '=', 'friends.friend_id')
->get(['votes.*']); // exclude extra details from friends table
Run the same join for the comments and status_updates tables. If you would like votes, comments, and status_updates to be in one chronological list, you can merge the resulting three collections into one and then sort the merged collection.
Edit
To get votes, comments, and status updates in one query, you could build up each query and then union the results. Unfortunately, this doesn't seem to work if we use the Eloquent hasMany relationship (see comments for this question for a discussion of that problem) so we have to modify to queries to use where instead:
$friends_votes =
DB::table('friends')->where('friends.user_id','1')
->join('votes', 'votes.user_id', '=', 'friends.friend_id');
$friends_comments =
DB::table('friends')->where('friends.user_id','1')
->join('comments', 'comments.user_id', '=', 'friends.friend_id');
$friends_status_updates =
DB::table('status_updates')->where('status_updates.user_id','1')
->join('friends', 'status_updates.user_id', '=', 'friends.friend_id');
$friends_events =
$friends_votes
->union($friends_comments)
->union($friends_status_updates)
->get();
At this point, though, our query is getting a bit hairy, so a polymorphic relationship with and an extra table (like DefiniteIntegral suggests below) might be a better idea.
Probably not what you want to hear, but a "feeds" table would be a great middleman for this sort of transaction, giving you a denormalized way of pivoting to all these data with a polymorphic relationship.
You could build it like this:
<?php
Schema::create('feeds', function($table) {
$table->increments('id');
$table->timestamps();
$table->unsignedInteger('user_id');
$table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
$table->morphs('target');
});
Build the feed model like so:
<?php
class Feed extends Eloquent
{
protected $fillable = ['user_id', 'target_type', 'target_id'];
public function user()
{
return $this->belongsTo('User');
}
public function target()
{
return $this->morphTo();
}
}
Then keep it up to date with something like:
<?php
Vote::created(function(Vote $vote) {
$target_type = 'Vote';
$target_id = $vote->id;
$user_id = $vote->user_id;
Feed::create(compact('target_type', 'target_id', 'user_id'));
});
You could make the above much more generic/robust—this is just for demonstration purposes.
At this point, your feed items are really easy to retrieve all at once:
<?php
Feed::whereIn('user_id', $my_friend_ids)
->with('user', 'target')
->orderBy('created_at', 'desc')
->get();
I have a Group model that belongsToMany Contacts, and Contact belongsToMany Groups, usually a group could contain about 300,000 contact, and a contact could belong to 2 or 3 groups, and I want a function that retrieves all "unique" contacts in a number of given groups, so I tried
$contacts = Contact::With(array('groups'=>function($query) use ($groups)
{
$query->whereIn('groups.id' , $groups);
}
))->get();
It worked but the problem is that I discovered the sql generated uses where contact_id IN ( ALL CONTACTS' IDs). So, if I'm retrieving 100,000 contacts it will have 100,000 of contact ids in the where in clause.
Then I ended up using this
foreach($groups as $group)
{
$contacts = array_unique(array_merge($contacts , $group->contacts()->lists('email'))) ;
}
But I'm still concerned about performance. I don't know how lists works and if it is a good idea to array_unique and array_merge on arrays containing hundreds of thousands of email strings?
You said:
usually a group could contain about 300,000 contact, and a contact
could belong to 2 or 3 groups
According to this your relationship should be many-to-many and both Group and Contact should use belongsToMany to make relationship between them using a pivot table contact_group:
// Group model
public function contacts()
{
return $this->belongsToMany('Contact');
}
// Contact model
public function groups()
{
return $this->belongsToMany('Group');
}
The contact_group pivot table could be like this:
id | group_id | contact_id
If you have a setup like this then you'll be able to do this:
$groups = Group::has('contacts')
->with('contacts')
->whereIn('id', [1,2,3]) // group ids, could be any field
->get();
$uniqueContacts = $groups->map(function($group) {
return $group->contacts->lists('email');
})->flatten()->toBase()->unique();
// You got all unique contacts in $uniqueContacts
dd($uniqueContacts); // all unique contacts
Don't know how fast it'll work on 100,000 contacts but should work better.
Update: Could be other (maybe better) ways by joining DB tables and querying from DB scope.