Lately, I have been working on multiple projects that have to deal with Polymorphic relationships, Many-To-Many, relationships, and the occasional Polymorphic Many-To-Many relationship. In working with these, a few patterns begun to arise that really irked me when thinking about good architecture and traditional ORM relationships. So, starting this week, we'll look at new patterns when creation your relationship maps. Today, we will look at standard Many-To-Many tables and their related pivot table.
Consider a run mapper as our domain. A user can log a day which will have multiple laps of various tracks or routes. It's relatively simple you think so you create the following schema
days:
date
temperature
#more info
routes:
gis_points
public
distance
title
#more stuff
day_route: #this will be our many-many pivot table
day_id
routes_id
laps
This works ok, but things start to get a bit strange when you consider how would you grab the total distance for a day. Well, you might make something like this (pseudo-PHP implementation use Eloquent)
public function getTotalDistanceAttribute()
{
$routes = $this->routes()->list('laps', 'distance'); //Makes an associative array where with each $lap => $distance
$total = 0;
foreach($routes as $laps => $distance) {
$total += ($laps * $distance);
}
return $total;
}
That works ok, but it does seem like the day is doing a bit much. For instance, what would happen if I wanted to see the total distance that you spent on a single route for that day. You could do some custom accessors, but where would you put that logic? In the route model? In a custom service? That seems a bit mad to me.
We already have a Pivot table and we want to start doing logic on this table so shouldn't that be more of a model? Consider this schema instead of the original
days:
date
temperature
#more info
routes:
gis_points
public
distance
title
#more stuff
runs: #this was out day_route table
day_id
routes_id
lap_distance
laps
If you look at this it is just renaming our pivot to be a model Run
with the exact same information!
Now our Run
will know it's own total distance that is really easily accessible!
To make things even better, our totalDistance accessor benefits from this too.
public function getTotalDistanceAttribute()
{
$sum = 0;
$this->runs->each(function($run) use(&$sum) {
$sum += $run->distance;
});
return $sum;
}
This is much cleaner in my mind.
The Day
shouldn't need to think about how to calculate the distance for each route within that day, it just knows how to sum up the distances.
Then Run
can be ready to worry about all of its logic while also storing the distance of the route when it it tackled (what if later in time the user modifies the route, then all of the past days' distances would be messed up).
We'll look at tackling similar solutions as the week goes on and see how this helps us all over the place, not just by saving our getTotalDistanceAttribute
function one line.