tmattio

tmattio

How to handle schemas polymorphism in Phoenix?

Hi,

I’ve already posted this question on Stack Overflow, but it didn’t receive much attention: https://stackoverflow.com/questions/49328342

Basically, I see a few ways to handle schemas polymorphism (w/ a belongs_to association), and I was wondering which one was recommended, and why.

Here is the question:

The recommended way to handle polymorphic associations in Phoenix seems to be adding an intermediate schema that contains the references to the other schemas:

So if I would like to create schemas with different kinds of animals, I would do:

defmodule Animal do
  use Ecto.Model

  schema "animals" do
    belongs_to(:dog, Dog)
    belongs_to(:cat, Cat)
    belongs_to(:owner, PetOwner)
  end
end

defmodule Dog do
  use Ecto.Model

  schema "dogs" do
  end
end

defmodule Cat do
  use Ecto.Model

  schema "cats" do
  end
end

defmodule PetOwner do
  use Ecto.Model

  schema "pet_owners" do
    has_one(:pet, Animal)
  end
end

But I could also have the PetOwner schemas containing a binary field and the type:

defmodule Dog do
  use Ecto.Model

  schema "dogs" do
  end
end

defmodule Cat do
  use Ecto.Model

  schema "cats" do
  end
end

defmodule PetOwner do
  use Ecto.Model

  schema "pet_owners" do
    field(:pet, :binary)
    field(:pet_type, :integer)
  end
end

Or even just having a nullable reference to all the animals in the owner schema:

defmodule Dog do
  use Ecto.Model

  schema "dogs" do
    belongs_to(:owner, PetOwner)
  end
end

defmodule Cat do
  use Ecto.Model

  schema "cats" do
    belongs_to(:owner, PetOwner)
  end
end

defmodule PetOwner do
  use Ecto.Model

  schema "pet_owners" do
    has_one(:cat, Cat)
    has_one(:dog, Dog)
  end
end

The first method seems to add complexity to the schemas. What are the pros and cons of the different methods?

EDIT: Let’s assume that a pet owner can own only one pet, if the schema allows multiple pet, the verification is done in the changeset.

Marked As Solved

ndac_todoroki

ndac_todoroki

I think your new solution violates your previous this:

Stepping back, why would you want an Animal module (or struct type)? Why would you need it as a DB table?
Do you want to list all animals and create a list of all Owners? If so, that makes sense.
If you want to create owned animals like owner |> own_new_animal(attrs) no matter what type of animals, then why would you need concrete tables for each types of animals? Are there unique fields that each animals should have? Then it makes sense.
The problem is, being polymorphic and having unique things at the same time is not a easy thing to dodge. But it can be done by trading-off some aspects.

I’ll try to list pros and cons of each solutions (including some not yet appearing in this thread):

Single Table Inheritance

This is when you have a big table (Animal) and each concrete animal tables are a sub-division of that table. You won’t have multiple tables.

pros
– you will only have one table.
– fetching all animals are very easy
– it can be easily implemented in Ecto, because all concrete animal modules can have schemas different with each other but referring to the same big table.

cons
– if you want unique fields for each types of animals, you get NULL columns everywhere (but you don’t care when using the concrete modules’ schema: it doesn’t appear.)

Class Table Inheritance

This is when you have a base table (= Animal), and each concrete animal tables have a reference to the base table plus their unique fields. (Animal {id: 1, birth: "20180101", vaccinated: true} Animal {id: 2, birth: "20111225", vaccinated: false / Cat {animal_id: 1, colour: "brown"} Snake {animal_id: 2, length: 150})

pros
– listing all animals are quite easy, if you only need the basic informations
– no NULL columns

cons
– while getting the full information (of an animal) from the concrete modules are quite easy (you just JOIN :animal), getting the full info from the base side is not (you cannot know what to JOIN).
– This is unless you write some kind of logic similar to polymorphic tables in Rails (animal_type and animal_id), which are mentioned “not preferable” in the Ecto docs.

Concrete Table Inheritance

No tables for Animals will be created. Each concrete animal tables will have all basic informations + their unique informations. This will be good if you want to assure that all animals are animals, but you don’t work with Animals directly.
(I’d rather create a protocol named Animal rather that doing this)

pros
– no need to JOIN when fetching a concrete animal
– works good with Ecto’s abstract tables (Animal will be an abstract table)

cons
– you need to be careful at migrations not to break the entire inheritance
– unique assertions must be done completely on application logic
– especially ids must be unique across all animal tables, so using :binary_ids is preferred.
– any search using keys in Animal should go through all tables (ugh)

Abstract tables

This is described in the Ecto docs. (Sample repo) Using only this seems not very helpful on your usage, but if you are looking at STI or CTI, it may help the implementation.
When you create a Class Table Inheritance, each table refers to an Animal base table. Using abstract tables, you can split that into multiple base tables for each concrete animal tables, for example a cat table will refer tocat_base and snake to snake_base, where cat_base and snake_base will have same columns. Then we will create an abstract table animal, and when you cat |> Animal.add_base_animal_info() it will create a cat_base.

pros
– you can retrieve concrete animal from Animals

cons
– you need to JOIN every concrete *_base tables to do list_animals

I think this is somewhere between Class Table Inheritance and Concrete Table Inheritance.

Embedding data

Embedding can be done in Postgres and MongoDB etc. You can have a single animal table with a field that accepts a map (field :details, :map). Then you define many concrete animal modules which its schema refers to that animal table, having a embeds_one :details, CatDetails where you define a embed_schema for CatDetails. (This example is STI with Embeds)

pros
– tables will be very clean
– no NULLs
– listing all animals is easy
– storing data using the concrete animals’ schema will validate the shape of maps you pass

cons
– the shape of the maps inside :details cannot be verified in DB level
– searching for fields inside embeds may not be good (depends on what db you use)

Hope it helps;

Also Liked

redrapids

redrapids

Author of Adopting Elixir

I am a little late in here, but here are the principles I use for this problem set.

  • Single-table solutions are best.For your example, it’s OK to have some columns used for just cats, and others for just dogs.

  • Pattern matching makes coding “inheritance” ridiculously simple. Just match on a “kind” or “type” attribute.

  • Flat trees are your friend.

  • Pattern matching can work across multiple dimensions because you can match on multiple attributes.

When you think about problems in this way, the data structure falls out.

Think of your data in terms of structs, the tables match those exactly. Manage polymorphic behavior with pattern matching across as many dimensions as you need. Keep like things in the same struct wherever possible.

redrapids

redrapids

Author of Adopting Elixir

Nice bump! Three years ago!

I was responding specifically to the idea of modeling inheritance. I was not too clear.

I should have said "Don’t model inheritance. If you must, use single table inheritance. When that’s not enough, go back to rule 1.

If I am Noah and building an arc, I might should try to create a table for each major type of animal. I would be in a world of hurt. Think about weaving together the main entity in Elixir.

Instead, I should think about modeling traits of animals. Size, food, waste, etc around the parts of my system. This would put me on more familiar ground, and will split the table across more logical boundaries.

Hope this is more clear.

OvermindDL1

OvermindDL1

Also, anything related to database work is not a phoenix thing, it’s a database thing, so you should actually be asking How would I model <this> in PostgreSQL?, as it is entirely unrelated to Phoenix anyway. :slight_smile:

michalmuskala

michalmuskala

The reason for this is that there’s no “the best” solution to this problem in SQL in general. There are several options, each with different trade-offs that @ndac_todoroki beautifully outlined. The best solution often depends not only on the data itself but also on the particular usage patterns.

This means there won’t be a single definitive answer to the question of “how to do this with Ecto”, since there’s no single definitive answer to the question of “how to do this with SQL”.

tmattio

tmattio

Wow, thank you so much for this detailed answer! It helped me a lot and I am sure it will help people who ask similar questions. Feel free to post this as an answer to my Stack Overflow question linked in the initial question, I’ll mark it as solved.

I am currently implementing showcases for each solution you describe, with your post and the implementations, I will be better equipped to take a decision.

I will post a link to the repository once I publish it :slight_smile:

Where Next?

Popular in Questions Top

dotdotdotPaul
Okay, I'm having a heck of a time trying to figure out how to best handle the validation of belongs_to associations in Ecto. I'm sure I'...
New
tduccuong
Hi, is there any work on GUI with Elixir, that is similar to Electron/Javascript? My idea is to bundle Phoenix and BEAM into a single se...
New
nsuchy
Hi. I’ve noticed that Windows Powershell has it’s own IEX command and you cannot access Elixir’s IEX due to the conflict. This isn’t a cr...
New
vertexbuffer
Hello, can anybody help here..? I have a list of players and I what to delete an element, but every for loop the list is reverting to ori...
New
lastday4you
I wanted to check elixir version in phoenix because i found that my elixir is 1.5 but when i use Enum.chunk_by it said the function is un...
New
aadeshere1
I have a another noob question about loop. Since elixir is immutable, while loop is not directly possible. total = 10 while total != 0 ...
New
fayddelight
I tried installing elixir 1.11.2 erlang 23.3.4 via asdf in my zsh shell. Enabled the versions locally and globally. When I list them ...
New
skosch
To my knowledge, put_in, Map.update etc. all have the one limitation of not automatically creating intermediate keys when needed (for exa...
New
jc00ke
Expanding on this topic: https://forum.elixirforum.net/t/map-typespec-question/19217 Let’s say I have a map with required and optional k...
New
joeerl
Hello again - after a longish gap I’ve decided I really must dig into Elixir and see what’s been happening here - so I have a few questio...
New

Other popular topics Top

peerreynders
Manning 2016 Halloween weekend sale via Deal of the Day Friday, October 28 - Half off all MEAPs - code WM102816LT Saturday, October 29 ...
326 29600 154
New
gshaw
What is the idiomatic way of matching for not nil in Elixir? E.g., First way: defp halt_if_not_signed_in(conn, signed_in_account) when...
New
ycv005
I have followed this StackOverflow post to install the specific version of Erlang. And When I am running mix ecto.setup then getting fol...
New
malloryerik
Hi, this is for people who, like me, have had some friction using .html.heex templates in VSCode. The solution seems to be, in a hyphena...
New
vac
Hi, I'm quite new in Elixir and I'm trying to format a string to a PEM format. I have the certificate value like MIIDBTCCAe2...... and ...
New
shahryarjb
Hello, I have map which I want to convert it to string like this: the map: %{last_name: "tavakkoli", name: "shahryar"} the string I ne...
New
ovidiubadita
Hey all, I discovered Elixir and I love it. I always wanted to learn a functional programming and I intended to go for Haskell, but afte...
New
electic
Hi, I am new to Elixir. I am trying to use the DateTime component to insert a date into MySQL however the there seems to be no way to fo...
New
AstonJ
by Lance Halvorsen Elixir and Phoenix are generating tremendous excitement as an unbeatable platform for building modern web application...
460 27162 124
New
siddhant3030
Hi, I have to write a raw query for one of my project. But till now I have used ecto queries and don’t have much experience writing raw ...
New

We're in Beta

About us Mission Statement