AWS Partner

Polymorphic Associations in Elixir

Antonia, Nov 2020

If you're coming from Ruby on Rails, you're probably familiar with the polymorphic associations between resources and you probably miss them in Elixir. We recently had to migrate a Rails functionality to Elixir without making any changes to our database, so in this article we'll cover how you can achieve that. If you've already gone through the Ecto documentation you've probably found out that copying this functionality into Еlixir is not recommended and is not the Еlixir way, yet no one actually describes how you could go about it if you want to replicate it. The purpose of this guide is to clear that up.

Rails Associations

Say we have a User model, an Image model and a Product model.
An Image can belong to a User or to a Product. 

In Rails the go-to approach would be to define a polymorphic association like this:

And our images table would have the following fields:

or

Now when we are creating images, we can access the User or the Product record by image.imageable.

How can we achieve the same result in Elixir

First we create migrations for each table: users, products and images. For simplicity purposes we'll create all resources under the same context which we'll call Shop.

1. Schema files and associations

The images schema should look like this:

Here we have added a virtual :imageable field which later on we will populate with either a user or a product record.

The users schema should currently look like this:

We are going to add a has_many association to our schema.

Don't forget to alias the Image module.
This association will allow us to access all images the user has simply by calling Repo.preload(:images). It will basically run a query to find all images with imageable_id equal to the user's ID and with imageable_type "user"

Lastly, we'll add the same has_many association to our product schema, but this time imageable_type's value will be "product". This is how the schema file should look like:

2. Preloading resources in queries

Now let's move on to our context file and rework the get_user! and get_product! functions to load the images as well.
This is how currently out get_user! function looks like:

We are going to change it to this:

We'll apply the same to the get_product! function.

In case you want to write it on a single line:

What we want next is when we call the get_image/1 function to have the imageable field populated with either a user or a product record. This is how we are going to achieve this:

We are going to query the data base for an image and pass the result to the add_imageable/1 function that we'll create in a bit.

We create the new function:

Our function has two clauses. In the first clause we pattern match on a nil value in case the image we are querying for does not exist in our database. The second clause will match on anything, but in our case this will always be an Image struct, and then it will populate the empty :imageable field of our Image struct with whatever is returned from the get_imageable/1 function.
This is what our get_imageable function looks like:

Our get_imageable function expects a map containing imageable_type and imageable_id fields and uses them to query the database for the corresponding imageable_type. For the cases where our map does not contain the aforementioned fields our function will return nil.

The whole code related to getting an image should look like this:

The same logic can be applied for listing images, so let's modify our list_images/0 function a little in order for every image in the list to have its imageable field populated.

The above code can be considered slow, since for each image record we will query the database for its imageable. It's out of this article's scope to optimise it, but for those of you that are curious you can find an optimised snippet at the end of the article.

3. Populating imageable field on resource creation

There is one last thing that we need to do before we can move on to testing. To fully copy the Rails functionality we expect when we call Shop.create_image() and pass the imageable as a parameter to have the two resources associated with each other. Let's see how we can achieve that in Еlixir.

We'll modify our create_image/1 function and before we pass the attributes to the changeset function we'll apply some changes to them by calling imageable_attrs/1.

The imageable_attrs/1 function looks like this:

This function pattern matches on a map with an imageable field and uses the field's value to get the imageable_type and imageable_id. And of course we have another clause that will just return the input in case the map is missing an imageable field.
The only thing that might look a bit odd is the way we get the imageable_type. The value of the type variable is an atom of either :Elixir.Shop.User or :Elixir.Shop.Product. Since we only care whether the imageable_type is a "user" or a "product" let's parse the atom to get only what we need.

This function should now return a "user" or a "product" string.
Let's see how the whole code regarding the creation of an image record looks like.

Now when we call create_image and pass the imageable as a parameter we'll have the resources associated with each other. But we also want to access the imageable by image.imageable right?

So this is how we can do that:
We go back to our Image changeset function and we'll add put_change/3 to populate our imageable field. We'll get the Imageable either from the params or by using the get_imageable/2 function we have previously defined.

That's all, we can move on to testing now.

4. Testing the functionality

Let's start our console and create an user:

Now let's create an image with the user as an imageable:

The imageable is populated, as well as the imageable Id and type. We can access the user by image.imageable.

We can create a product and an image for that product:

As this article has become a little lengthy we'll skip the tests for the remaining functions that we modified, but we encourage you to go ahead and test out the list_images, get_user!, get_product! and get_image functions.
As we can see images are created along with the corresponding association, so this concludes the Rails polymorphic associations in Еlixir, at least the Rails way, the Еlixir way is coming up.

*Optimised list_images

We have mentioned that the code for listing images can be optimised. You can use this snipped to do so. Note that the parse_type/1 function has already been defined in section 3 of this article.


Join our newsletter