I'm trying to make a artisan command in Laravel to remove all venues that have the same address and leave the one with the lowest ID number (so first created).
For this I need to check 3 fields: 'street', 'house_number', 'house_number_addition'
This is how far I've got:
$venues = Venue::select('street', 'house_number', 'house_number_addition', DB::raw('COUNT(*) as count'))
->groupBy('street', 'house_number', 'house_number_addition')
->having('count', '>', 1)
->get();
foreach ($venues as $venue) {
$this->comment("Removing venue: {$venue->street} {$venue->house_number} {$venue->house_number_addition}");
$venue->delete();
}
Only the delete is not working but is also not giving an error.
To be able to delete an item, Eloquent needs to know it's id. If you make sure your models' id is queried, you can call delete() without issues.
In your query, however, that won't work because you have a GROUP_BY statement, so SQL doesn't allow you to select the id column (see here).
The easiest solution here is to utilize Eloquent's Collection class to map over the models, something like:
$uniqueAddresses = [];
Venue::all()
->filter(function(Venue $venue) use (&$uniqueAddresses) {
$address = sprintf("%s.%s.%s",
$venue->street,
$venue->house_number,
$venue->house_number_addition);
if (in_array($address, $uniqueAddresses)) {
// address is a duplicate
return $venue;
}
$uniqueAddresses[] = $address;
})->map(function(Venue $venue) {
$venue->delete();
});
Or, to make your delete query a little more efficient (depending on how big your dataset is):
$uniqueAddresses = [];
$duplicates = [];
Venue::all()
->map(function(Venue $venue) use (&$uniqueAddresses, &$duplicates) {
$address = sprintf("%s.%s.%s",
$venue->street,
$venue->house_number,
$venue->house_number_addition);
if (in_array($address, $uniqueAddresses)) {
// address is a duplicate
$duplicates[] = $venue->id;
} else {
$uniqueAddresses[] = $address;
}
});
DB::table('venues')->whereIn('id', $duplicates)->delete();
Note: the last one will permanently delete your models; it doesn't work with Eloquent's SoftDeletes functionality.
You could, of course, also write a raw query to do all this.
Related
I have a complicated filter for my hotels and in the end i have a collection that I want to sort the parent relations by its nested relationship so here I have as below :
public function resultFilter($from_date, $to_date, $bed_count, $city_id, $stars, $type_id, $hits, $price, $accommodation_name, $is_recommended, $start_price, $end_price, $has_discount, $facility_id)
{
// $data = QueryBuilder::for(Accommodation::class)
// ->allowedFilters(['city_id','grade_stars','accommodation_type_id'])
// ->allowedIncludes('gallery')
// ->when($bed_count, function ($q, $bed_count) {
// $q->with([
// 'accommodationRoomsLimited' => function ($q) use ($bed_count) {
// $q->where('bed_count', $bed_count);
// }
// ]);
// })
// ->paginate(10);
// ->get();
// ->orderBy('hits','DESC')->paginate(10);
$data = Accommodation::with(['city','accommodationFacilities', 'gallery', 'accommodationRoomsLimited.discount', 'accommodationRoomsLimited', 'accommodationRoomsLimited.roomPricingHistorySearch' => function ($query) use ($from_date, $to_date) {
$query->whereDate('from_date', '<=', $from_date);
$query->whereDate('to_date', '>=', $to_date);
}])->when($bed_count, function ($q, $bed_count) {
$q->whereHas('accommodationRoomsLimited', function($query) use ($bed_count) {
$query->where('bed_count', $bed_count);
});
})->when($accommodation_name, function ($query, $accommodation_name) {
$query->where('name', 'like', $accommodation_name);
})->when($is_recommended, function ($query,$is_recommended){
$query->where('is_recommended', $is_recommended);
})->when($start_price, function ($query, $start_price) {
$query->with([
'accommodationRoomsLimited.roomPricingHistorySearch' => function ($q) use ($start_price) {
$q->where('sales_price', '<', $start_price);
}
]);
})->when($has_discount, function ($query, $has_discount) {
$query->with([
'accommodationRoomsLimited' => function ($q) use ($has_discount) {
$q->has('discount');
}
]);
})
->whereIn('city_id', $city_id)
->whereIn('grade_stars', $stars)
->orWhere('accommodation_type_id', $type_id);
if ($hits) { // or == 'blabla'
$data = $data->orderBy('hits','DESC');
} elseif ($price) { // == A-Z or Z-A for order asc,desc
$f = $data->get();
foreach ($f as $datas) {
foreach ($datas->accommodationRoomsLimited as $g) {
dd($data);
$data = $data->accommodationRoomsLimited()->orderBy($g->roomPricingHistorySearch->sales_price);
}
}
}
$data = $data->paginate(10);
return $data;
}
So if you read code I added the sales_price that I want to sort my $data by it if the $price exists in the request. So in a short term question, I want to sort $data by sales_price in this query above.
NOTE
: this filters may get more complicated so any other best practice or better way for that like spatie Query builder or local scopes would be appreciated although i tried both and yet they have their own limitation
I've faced that problem before. And it seems I need to explain a little about eager loading first.
You can't order by eager loading, you can order it after you fetch the data. Because
eager load will split join query for better performance. For example you querying accomodation and has relation with city. The accomodation table has 1000 records and the city table has 10.000 records. let's say the maximum id for eager loading is 250, the unique city_id from accomodation table is 780. There will be 5 query generated.
$data = Accomodation::with('city')->get();
select * from accomodation
select * from city where id in [unique_id_1 - unique_id_250]
select * from city where id in [unique_id_251 - unique_id_500]
select * from city where id in [unique_id_501 - unique_id_750]
select * from city where id in [unique_id_751 - unique_id_780]
Then laravel will do the job to create the relation by city query results. By this method you will fix N+1 problem from join query, thus it's should be faster.
Then imagine you want to order accomodation bycity.name with with method in query builder. let's take the 3rd query for example.
$data = Accomodation::with([
'city' => function($q) { return $q->orderBy('name'); },
])->get();
the query will be:
select * from city where id in [unique_id_251 - unique_id_500] order by name
The city results will be ordered, but laravel will read it the same way. It'll create accomodation first, then relate it with city queries. So the order from city won't affected accomodation order.
Then how to order it? I found out couple ways to achieve that.
Join query. this is the easiest way, but will make query slower. if your data isn't really big and the join query won't hurt your performance. Maybe 0.003 seconds better performance isn't really worth your 8 hours.
sortBy in collection function. You can sort it with a method from collection.
for example if you want to order the accomodation based on country.name from city relation, this script will help you.
$data = Accomodation::with('city.country')->get();
$data->sortBy(function($item) {
return $item->city->country->name;
});
Flatten the collection. This method will try to flatten the collection so the results will be like join query then sorting it. You can use map method from collection. I do believe all the filters and searchable strings is should be included in data.
$data->map(function($item) {
return [
'city_name' => $city->name,
...
all_searchable_data,
all_shareable_data,
...
];
})->sortBy('key1);
Change eager loading direction if possible. You can order it with changing base models. For example you use city instead accomodation to order it by city.name
$data = City::with('accomodation')->orderBy('name')->get();
And last, If your data rarely changes (example every 2 hours), You might thinking to use cache. You only need to invalidate the cache every 2 hours and create the new one. From my experiences, cache always faster than querying database if the data is big. You just need to know the interval or event to invalidate the cache.
Anything you choose is up to you. But please remember this, when you processing bulk data with the collection from laravel, It could be slower than querying from the database. Maybe it's because PHP performance.
For me the best way is using eager loading then ->map() it then cache it. Why do I need to map it first before cache it? The reason is, by selecting some attribute will reduce the cache size. Then you'll be gain more performance by. And I can say it will produce more readable and beatiful code.
Bonus
this is how I doing this.
$data = Cache::remember("accomodation", 10, function() {
$data = Accommodation::with([
'city',
...
])
->get();
return $data->map(function($item) {
return [
'city_name' => $item->city->name,
...
all_additional_data,
all_searchable_data,
all_shareable_data,
...
];
});
}
return $data->search(function($item) use ($searchKey, $searchAnnotiation, $searchValue) {
switch ($searchAnnotiation) {
case '>':
return $item[$searchKey] > $searchValue;
break;
case '<':
return $item[$searchKey] < $searchValue;
break;
}
})->sortBy($sortKey)->paginate();
The cache will save the processed data. thus the execution time needed is fetch data from cache, filter it, and sorting it. then transform it into paginate. you can set any additional cache in those flow for faster results.
$data->paginate() by create macro paginate for Collection.
I have related items in my database. I selected all of items from database by related id:
$next_stock = $this->model->get()->where('part_id', $in_data['part_id'])->all();
and I collection of rows grouped by one specific id, like on the picture. All of them selected by "part_id":
Selection Of Items
Grouped By Same Id
Also with this line of code i can select one of the items from this collection:
$next_stock = $this->model->get()->where('id', $old_stock['id'])->where('part_id', $in_data['part_id'])->first();
But how can I select the following items after this one?
Or, how can I select second or third item from this collect?
I cannot just increase id number by one from first, because sometimes this item ids not following each other.
Having a collection, you can take a specific element in the position with a combination of take() and last().
$collection = $this->model->get()->where('part_id', $in_data['part_id'])->all();
$second = $collection->take(2)->last(); //if this doesnt work, do it in 2 steps
$third = $collection->take(3)->last(); //if this doesnt work, do it in 2 steps
If you don't have a collection, take directly from database like this
$second = $this->model
->where('part_id', $in_data['part_id'])
->skip(1)
->first();
If it doesn't work with first()
$collect = $this->model
->where('part_id', $in_data['part_id'])
->skip(1)
->take(1)
->get();
$second = $collect->first();
Edit
skip() and take() are actually part of the query builder, not eloquent model. So it won't work with Eloquent in Laravel 5.4
Try with
$collect = $this->model
->where('part_id', $in_data['part_id'])
->get(1); //For the second record, 0 being the first
If you aren't doing it yet, you should set your model's relationships.
E.g. If you use "one-to-many", Eloquent will automatically determine the proper foreign key column on the model for you.
$parts = App\Stock::find(1)->partId;
foreach ($parts as $part) {
//
}
I presently have 3 tables: Shows, Genres and Show_Genre (associates the two). I have a search form that submits a series of checkboxes with values into an array based on what genres they selected. Presently I want to associate the Shows table and the Genres table into a variable and run a query against it once for every genre checkbox selected. Then, once the selection is filtered, I can display the resulting Show objects that matched the users parameters.
My present setup is the following
public function searchShows(SearchRequest $request)
{
//$ShowsWithGenres give a collection inside a collection which I can't seem to even access its values without doing a bunch of ugly code using count() in a for loop
$ShowsWithGenres = Show::with('genres')->get();
$genres = $request->name;
if(isset($genres))
{
foreach($genres as $genre)
{
//iterate against the selection repeatedly reducing the number of results
}
}
}
Thanks.
You should use whereHas() and whereIn.
Perhaps something like this should do it:
$shows = Show::whereHas('genres', function($q) use($genre_ids)
{
$q->whereIn('id', $genre_ids);
})->get();
EDIT
Try this, however I'm unsure about the performance.
$query= Show::query();
foreach($genre_ids as $id){
$query->whereHas('genres', function($q) use($id)
{
$q->where('id', $id);
})
}
$shows = $query->get();
Using Eloquents whereHas() function you can query results based on the relation's data. http://laravel.com/docs/5.0/eloquent#querying-relations
public function searchShows(SearchRequest $request)
{
// Start a query (but don't execute it at this point as we may want to add more)
$query = Show::with('genres');
$genreNames = (array) $request->name;
// Check there are some genre names, if theres not any it'll cause an SQL syntax error
if (count($genreNames) > 0)
{
$query->whereHas('genres', function($subQuery) use ($genreNames)
{
$subQuery->whereIn('genre_name', $genreNames);
}
}
// Finally execute the query. $shows now contains only shows with the genres that the user has searched for, if they didn't search with any genres, then it contains all the results.
$shows = $query->get();
}
I have populate a form of which every text field generated is based on the database result. I simply name every text field using the id. Now when the form is filled, I use controller to save it. But prior to insert the database, I loop the Request::input() to check every item whether such entry is exist or not. I just wonder if there is efficient way to check every item in the loop to insert it into db. Here is my code
public function store(Request $request, $id, $inid)
{
$startOfDay = Carbon::now()->startOfDay();
$endOfDay = Carbon::now()->endOfDay();
$instruments = InstrumentReading::whereBetween('created_at', [$startOfDay, $endOfDay])
->where('iv_inid', '=', $inid)
->get();
foreach ($request->input() as $k => $v) {
$read = new InstrumentReading;
$read->iv_inid = $inid;
$read->iv_ipid = $k;
$read->iv_usid = Auth::user()->id;
$read->iv_reading = $v;
$read->save();
}
if ($instruments->count() > 0) {
//to filter the iv_ipid...
foreach($instruments as $instrument)
{
$instrument->iv_status = "VOID";
$instrument->save();
}
}
}
In words of efficent approach what you can do is to simple check / fetch ONLY all posible rows from the database, and the check in the loop if the row was already inserted. Also fetch only iv_ipid column, as we do not need all columns from the table to do our check. It will be faster to select only the column we need. You can use directly Fluent (Query Builder) over Eloquent to pull the data from database as it greatly increase the performance for a simple query like this.
public function store(Request $request, $id, $inid)
{
// Search only records with submitted iv_ipid, iv_inid and created today
$alreadyInserted = DB::table('instrument_readings')
->whereBetween('created_at', [
Carbon::now()->startOfDay(),
Carbon::now()->endOfDay()
])
// Get only records with submitted iv_ipid
->whereIn('iv_ipid', array_keys($request->input()))
// Get records with given iv_inid only
->where('iv_inid', $inid)
// For our check we need only one column,
// no need to select all of them, it will be fast
->select('iv_ipid')
// Get the records from DB
->lists('iv_ipid');
foreach ($request->input() as $k => $v) {
// Very simple check if iv_ipid is not in the array
// it does not exists in the database
if (!in_array($k, $alreadyInserted)) {
$read = new InstrumentReading;
$read->iv_inid = $inid;
$read->iv_ipid = $k;
$read->iv_usid = Auth::user()->id;
$read->iv_reading = $v;
$read->save();
} else {
//todo
}
}
This is the most efficent way suggested until now, because you fetch at once only the records you are interested in, not all records from today. Also you fetch only one column, the one that we need for out check. Eloquent ususlally give a lot of overheat on the perfomance, so in the suggested code I use directly Fluent, which will boost the speed this part of code is executed by ~ 20%.
Your mistake in the original code is that you are doing database call each time in a loop. When you need such a simple task as a check, never put database calls, queries etc. in a loop. It is an overkill. Instead select all needed data before the loop and then do your checks.
Now this is in case you only need to save new records to database. In case you want to manipulate each record in the loop, let's say you need to loop through each submited entry, get get the model or create it if it does not exists and then do something else with this model, the most efficent way then will be this one:
public function store(Request $request, $id, $inid)
{
foreach ($request->input() as $k => $v) {
// Here you search for match with given attributes
// If object in DB with this attributes exists
// It will be returned, otherwise new one will be constructed
// But yet not saved in DB
$model = InstrumentReading::firstOrNew([
'iv_inid' => $inid,
'iv_ipid' => $k,
'iv_usid' => Auth::user()->id
]);
// Check if it is existing DB row or a new instance
if (!$model->exists()) {
// If it is a new one set $v and save
$model->iv_reading = $v;
$model->save();
}
// Do something with the model here
.....
}
This way Laravel will check if model with the passed parameters already exist in database, and if so it will return it for you. If it does not exist, it will create new instance of it, so you can then set the $v and save to db. So you are good to go to do anything else with this model and you can be sure it exists in database after this point.
First approach (efficiency first)
Consider using a simple SQL INSERT IGNORE Query and use Fluent, i.e.:
Make a composite unique key containing:
iv_inid
iv_ipid
created_time, up to an hour granularity, this is important, because created_at might have a far greater granularity than your intended purpose, and might slow things down a bit.
Use DB, i.e.:
DB::query(
"INSERT IGNORE INTO $yourTable VALUES ( ... )"
);
Pros:
- Extremely fast, all the necessary checking is done on the DB Server
Cons:
- You cannot know which values triggered a duplicate value / unique key violation, as related errors are treated as warnings.
Second approach (convenience first)
Use firstOrFail, i.e.:
$startOfDay = Carbon::now()->startOfDay();
$endOfDay = Carbon::now()->endOfDay();
// ... for
try {
InstrumentReading::where('iv_inid', $inid)
->where('iv_ipid', $k)
->whereBetween('created_at', [$startOfDay, $endOfDay])
->firstOrFail();
continue;
} catch (ModelNotFoundException $e) {
$instrumentReading = InstrumentReading::create([
// your values
]);
}
// ... endfor
Pros:
- Easy to implement
Cons:
- Somewhat slower than simple queries
Your code will send request to database every time you need to check the value. Instead, search all value of this day then check the value. This approach will send request to database only one time.
$startOfDay = Carbon::now()->startOfDay();
$endOfDay = Carbon::now()->endOfDay();
// Search only this day
$instruments = InstrumentReading::whereBetween('created_at', [$startOfDay, $endOfDay])->get();
foreach($instruments as $instrument)
{
// Check the value
}
I have two models, User and Training, with Many to many relationship between them. I'm using the Laravel Datatables package to display a table of all the users. This is how the data controller method (which retrieves the query results and creates a Datatables table) looks like:
public function getData()
{
$users = User::select(array('users.id', 'users.full_name', 'users.email', 'users.business_unit', 'users.position_id'))
->where('users.is_active', '=', 1);
return \Datatables::of($users)
->remove_column('id')
->make();
}
How can I add a column to the created table which displays the total number of relations for each user (that is, how many Trainings does each User have)?
The brute force way would be to try a User::selectRaw(...) which has a built in subquery to get the count of trainings for the user and expose it as a field.
However, there is a more built-in way to do this. You can eager load the relationship (to avoid the n+1 queries), and use the DataTables add_column method to add in the count. Assuming your relationship is named trainings:
public function getData() {
$users = User::with('trainings')->select(array('users.id', 'users.full_name', 'users.email', 'users.business_unit', 'users.position_id'))
->where('users.is_active', '=', 1);
return \Datatables::of($users)
->add_column('trainings', function($user) {
return $user->trainings->count();
})
->remove_column('id')
->make();
}
The name of the column in add_column should be the same name as the loaded relationship. If you use a different name for some reason, then you need to make sure to remove the relationship column so it is removed from the data array. For example:
return \Datatables::of($users)
->add_column('trainings_count', function($user) {
return $user->trainings->count();
})
->remove_column('id')
->remove_column('trainings')
->make();
Edit
Unfortunately, if you want to order on the count field, you will need the brute force method. The package does its ordering by calling ->orderBy() on the Builder object passed to the of() method, so the query itself needs the field on which to order.
However, even though you'll need to do some raw SQL, it can be made a little cleaner. You can add a model scope that will add in the count of the relations. For example, add the following method to your User model:
Note: the following function only works for hasOne/hasMany relationships. Please refer to Edit 2 below for an updated function to work on all relationships.
public function scopeSelectRelatedCount($query, $relationName, $fieldName = null)
{
$relation = $this->$relationName(); // ex: $this->trainings()
$related = $relation->getRelated(); // ex: Training
$parentKey = $relation->getQualifiedParentKeyName(); // ex: users.id
$relatedKey = $relation->getForeignKey(); // ex: trainings.user_id
$fieldName = $fieldName ?: $relationName; // ex: trainings
// build the query to get the count of the related records
// ex: select count(*) from trainings where trainings.id = users.id
$subQuery = $related->select(DB::raw('count(*)'))->whereRaw($relatedKey . ' = ' . $parentKey);
// build the select text to add to the query
// ex: (select count(*) from trainings where trainings.id = users.id) as trainings
$select = '(' . $subQuery->toSql() . ') as ' . $fieldName;
// add the select to the query
return $query->addSelect(DB::raw($select));
}
With that scope added to your User model, your getData function becomes:
public function getData() {
$users = User::select(array('users.id', 'users.full_name', 'users.email', 'users.business_unit', 'users.position_id'))
->selectRelatedCount('trainings')
->where('users.is_active', '=', 1);
return \Datatables::of($users)
->remove_column('id')
->make();
}
If you wanted the count field to have a different name, you can pass the name of the field in as the second parameter to the selectRelatedCount scope (e.g. selectRelatedCount('trainings', 'training_count')).
Edit 2
There are a couple issues with the scopeSelectRelatedCount() method described above.
First, the call to $relation->getQualifiedParentKeyName() will only work on hasOne/hasMany relations. This is the only relationship where that method is defined as public. All the other relationships define this method as protected. Therefore, using this scope with a relationship that is not hasOne/hasMany throws an Illuminate\Database\Query\Builder::getQualifiedParentKeyName() exception.
Second, the count SQL generated is not correct for all relationships. Again, it would work fine for hasOne/hasMany, but the manual SQL generated would not work at all for a many to many relationship (belongsToMany).
I did, however, find a solution to both issues. After looking through the relationship code to determine the reason for the exception, I found Laravel already provides a public method to generate the count SQL for a relationship: getRelationCountQuery(). The updated scope method that should work for all relationships is:
public function scopeSelectRelatedCount($query, $relationName, $fieldName = null)
{
$relation = $this->$relationName(); // ex: $this->trainings()
$related = $relation->getRelated(); // ex: Training
$fieldName = $fieldName ?: $relationName; // ex: trainings
// build the query to get the count of the related records
// ex: select count(*) from trainings where trainings.id = users.id
$subQuery = $relation->getRelationCountQuery($related->newQuery(), $query);
// build the select text to add to the query
// ex: (select count(*) from trainings where trainings.id = users.id) as trainings
$select = '(' . $subQuery->toSql() . ') as ' . $fieldName;
// add the select to the query
return $query->addSelect(DB::raw($select));
}
Edit 3
This update allows you to pass a closure to the scope that will modify the count subquery that is added to the select fields.
public function scopeSelectRelatedCount($query, $relationName, $fieldName = null, $callback = null)
{
$relation = $this->$relationName(); // ex: $this->trainings()
$related = $relation->getRelated(); // ex: Training
$fieldName = $fieldName ?: $relationName; // ex: trainings
// start a new query for the count statement
$countQuery = $related->newQuery();
// if a callback closure was given, call it with the count query and relationship
if ($callback instanceof Closure) {
call_user_func($callback, $countQuery, $relation);
}
// build the query to get the count of the related records
// ex: select count(*) from trainings where trainings.id = users.id
$subQuery = $relation->getRelationCountQuery($countQuery, $query);
// build the select text to add to the query
// ex: (select count(*) from trainings where trainings.id = users.id) as trainings
$select = '(' . $subQuery->toSql() . ') as ' . $fieldName;
$queryBindings = $query->getBindings();
$countBindings = $countQuery->getBindings();
// if the new count query has parameter bindings, they need to be spliced
// into the existing query bindings in the correct spot
if (!empty($countBindings)) {
// if the current query has no bindings, just set the current bindings
// to the bindings for the count query
if (empty($queryBindings)) {
$queryBindings = $countBindings;
} else {
// the new count query bindings must be placed directly after any
// existing bindings for the select fields
$fields = implode(',', $query->getQuery()->columns);
$numFieldParams = 0;
// shortcut the regex if no ? at all in fields
if (strpos($fields, '?') !== false) {
// count the number of unquoted parameters (?) in the field list
$paramRegex = '/(?:(["\'])(?:\\\.|[^\1])*\1|\\\.|[^\?])+/';
$numFieldParams = preg_match_all($paramRegex, $fields) - 1;
}
// splice into the current query bindings the bindings needed for the count subquery
array_splice($queryBindings, $numFieldParams, 0, $countBindings);
}
}
// add the select to the query and update the bindings
return $query->addSelect(DB::raw($select))->setBindings($queryBindings);
}
With the updated scope, you can use the closure to modify the count query:
public function getData() {
$users = User::select(array('users.id', 'users.full_name', 'users.email', 'users.business_unit', 'users.position_id'))
->selectRelatedCount('trainings', 'trainings', function($query, $relation) {
return $query
->where($relation->getTable().'.is_creator', false)
->where($relation->getTable().'.is_speaker', false)
->where($relation->getTable().'.was_absent', false);
})
->where('users.is_active', '=', 1);
return \Datatables::of($users)
->remove_column('id')
->make();
}
Note: as of this writing, the bllim/laravel4-datatables-package datatables package has an issue with parameter bindings in subqueries in the select fields. The data will be returned correctly, but the counts will not ("Showing 0 to 0 of 0 entries"). I have detailed the issue here. The two options are to manually update the datatables package with the code provided in that issue, or to not use parameter binding inside the count subquery. Use whereRaw to avoid parameter binding.
I would setup your DB tables and Eloquent models using the conventions provided at http://laravel.com/docs/4.2/eloquent. In your example you would have three tables.
trainings
training_user
users
Your models would look something like this.
class Training {
public function users() {
return $this->belongsToMany('User');
}
}
class User {
public function trainings() {
return $this->belongsToMany('Training');
}
}
You can then use Eloquent to get a list of users and eager load their trainings.
// Get all users and eager load their trainings
$users = User::with('trainings')->get();
If you want to count the number of trainings per user you can simply iterate over $users and count the size of the trainings array.
foreach ( $users as $v ) {
$numberOfTrainings = sizeof($v->trainings);
}
Or you can simply do it in pure SQL. Note that my example below assumes you follow Laravel's conventions for naming tables and columns.
SELECT
u.*, COUNT(p.user_id) AS number_of_trainings
FROM
users u
JOIN
training_user p ON u.id = p.user_id
GROUP BY
u.id
Now that you have a couple of ways to count the number of relations, you can use whatever method you like to store that value somewhere. Just remember that if you store that number as a value in the user table you'll need to update it every time a user creates/updates/deletes a training (and vice versa!).