One thing I’ve long thought missing from Ash framework is the ability to create a type of “has many through” relationships. These are slightly different from many-to-many relationships, which do exist in Ash already - these are one way relationships, one -> many -> many. The other day I realized that these types of relationships can still be created, using custom relationships - but it’s a little bit obscure.
Note: All of the code in this blog post is in a repo on GitHub. Download a copy of the repo and follow along!
The basic data model - countries, regions, and cities#
For this example, I’ll use something we’re hopefully all familiar with - a basic model of the world. Our app will have a resource for a Country, each Country will have many Regions (these could be states, prefectures, provinces, islands, etc.), and each Region will have many Cities.
In Ash, this could look like this:
In the example repo, you can populate some data for these resources by running mix run priv/seeds/repo_part1.exs
.
In iex
, you can use the names of the created countries, regions and cities to load data, eg.
Creating the new relationships#
Adding a “has many through” relationship would create a direct link between countries and cities. When added to the Country
resource, we’ll be able to fetch and work with a list of all of the cities in the country, without iterating over all of the regions and collecting them all in memory.
We can create a custom relationship to model these links, one that doesn’t rely on a foreign key like the City resource having a region_id
. To remove that foreign key requirement, we can use the no_attributes?
option on the relationship:
This means that loading cities
on a Country record, will now load all cities in the database. To fix this, we also need to add a filter
to the relationship, that will filter the list of cities to only those that belong to a region in this country. Note that the fields in the filter are from the target resource - City
- because that’s what the relationship is for.
This uses a join expression to filter cities by their related region, and also the parent expression function to be able to use attributes from the source Country resource in the filter. It’s kind of an inversion of a traditional has many through, that traces from the target resource back to the parent.
You can test this out in iex
, using the example repo:
The generated query for the cities is efficient - it uses inner joins and filters to load all of the correct cities for the specified country.
What else can we do with these relationships?#
These relationships can be used just like any other relationships, such as in filters and aggregates.
Want to count the number of cities in a country? Add an aggregate to the Country resource that uses the cities
relationship:
Want to find the country with the least number of cities?
Admittedly this isn’t the largest sample size, but I hope the idea comes through that these can be used like any other relationship, to really extract useful information from your data.
We need to go deeper#
You can add more layers of relationships to go through, and Ash will know exactly what to do. We can add another to the example repo - each City now has a set of Landmarks, notable locations that tourists might like to visit.
We can fetch all of the landmarks in a country, by adding another custom relationship to the Country resource:
The first version of the filter uses the same idea we did for relating cities. The base layer of the filter (such as city.region.country_id
or city_id
above) refers to the resource being filtered - in this case, it’s a list of Landmark instances, so we can use attributes and relationships that are defined on the Landmark
resource.
And the parent layer, parent(id)
or parent(cities.id)
above, refers to the resource we’re currently in, the Country resource. So we can use attributes like id
and relationships like cities
, that are defined on the Country resource.
What else can no_attributes?
be used for?#
It can be used for a lot of things! Any kind of relationship you can think of, that uses something other than a single static foreign key to link two resources together. It could be:
- previous/next instances in a list of records, eg. a
Version
resource that has a relationship to the previous/next version in the list - listing friends of a user, where users are connected to each other through a directed from/to
friendship
resource
And probably a lot more!
Using relationships like this can be a bit tricky to wrap your head around, but once you do, it’s a real superpower. Let me know about the cool stuff you’ve used them for!