PaleWatcher

PaleWatcher

How do you model different ‘types’ with totally different fields in Ecto?

I’m trying to model something in Ecto where records share a few common fields, but depending on a type, they have completely different additional fields.

Roughly:

  • there’s an items table with the common stuff (type, same_field, etc.)
  • and then separate tables for each type (items_a, items_b)
  • these subtype tables use the same id as the parent (so 1:1, PK = FK)

So everything is still one logical collection, just split across tables to avoid a lot of nullable columns and to keep proper DB constraints.

In Ecto I’m using has_one + cast_assoc, and dispatching based on the type.


I’m wondering:

  • does this approach fit well with Ecto, or is it something people generally avoid?
  • is there a more idiomatic way to solve this without collapsing everything into one table or using JSON?
  • are there any gotchas with this setup (especially around inserts or preloading)?

Example changeset:

def changeset(item, attrs) do
item
|> cast(attrs, [:type, :category, :owner_id])
|> validate_required([:type, :category, :owner_id])
|> cast_subtype()
end

The whole idea is to keep validation at the database level as much as possible, not just in changesets.

First Post!

sodapopcan

sodapopcan

I’m slightly confused by your wording… items_a and items_b, the plural makes me think it’s a has_many, but you then you said hash_one so I’m assuming that. SO, I actually have a mostly working on a library to handle this more gracefully as we had a few tables like that at my old work. But since I stopped working there I lost motivation. I could post what I have, though. The API is basically:

schema "items" do
  field :foo, :string

  one :item do
    belongs_to :item_one, Item_1
    belongs_to :item_two, Item_2
    belongs_to :item_three, Item_3
  end

  def changeset(item, attrs) do
    item
    |> cast(attrs, [:foo])
    |> cast_one_assoc(:item)
  end
end

It basically adds sister versions of a few functions with _one added (kinda like _embeds versions), so cast_one, put_one_)assoc, and get_one_assoc (for each you can refer to them by the first argument to the one macro). There is also MyRepo.preload_one (which is a stupid name and I think that function is unfinished for nested preloads). I can post what I have if you’re interested.

Otherwise, as far as preloading goes, you can do this and it’s efficient:

Repo.preload(items, [:item_a, :item_b, :item_c)

When preloading it will simply ignore the associations whose ids are nil. I would definitely hide this in a function, though, as they get unweildly if you are preloading a lot.

Most Liked

LostKobrakai

LostKobrakai

I recently read a good blog post on this:

This is about the plain db level. Once you have that you can add the schemas for that.

You likely want to have another layer of this eventually, which can map between [ItemA, ItemB] to those lower level nested schemas.

10
Post #4
krasenyp

krasenyp

Then there’s no time to refactor the database schema. I always encourage people to not half-ass the database design. Do it properly from the get go. It’s like in sports. If you lack the fundamentals, it’s very hard to compensate afterwards.

Last Post!

GrammAcc

GrammAcc

Your data model looks like a regular tagged union to me. Is there some reason you can’t use a separate table for each item type? If the relations are 1;1, then there’s not really any difference between the items;item_one schema and just an item_one schema with all the fields from both items and item_one on it. This also avoids a join, which can help if you need to sort across multiple columns since Postgres at least can’t use ordered indexes across joins.

Since you’ve got a tag field for the tagged union (the type field), you can pattern match on the value of that field passing structs or Ecto Schemas around in application code, and if each item type is a separate table, you could use Ecto Schemas and Ecto would take care of querying the correct table for you. If you don’t want to use ecto schemas it would be easy enough to pattern match on the type field to get the from clause. Something like:

def from_items(:item_one), do: from(i in "item_one", as: :item_one)
def from_items(:item_two), do: from(i in "item_two", as: :item_two)

Cheers!

Where Next?

Trending in Questions Top

lanycrost
Hi everyone! I need implement if…else if…else condition from my elixir code, and anymore of this control flow structures not work proper...
New
senggen
Erlang/OTP 25 [erts-13.2.2] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] 15:22:35.803 [error] gen_event {lager_file_backend...
New
hariharasudhan94
Lets say I have map like this fetching from my database %{"_id" => #BSON.ObjectId<58eb1a7a9ad169198c3dXXXX>, "email" => ...
New
tj0
I’ve been following the steps here for the upgrade from 1.6 to 1.7 and it has gone relatively smoothly all the way till the phoenix_view ...
New
cgraham
Hi! What is currently the best library/method for parsing text and tabular data out of PDF files in Elixir or Erlang?
New
stefanchrobot
Hi, I need a way to handle data migrations in my application. I found an article by @wojtekmach about manual migrations: Automatic and ma...
New
stjefim
Hello! Suppose you are building workflow (order / task / payment) processing system with the following requirements: Each workflow con...
New

Other Trending Topics Top

GenericJam
Edit: 2026 May 15 - This post is archived. Mob is alive!! Main docs: mob v0.7.11 — Documentation A bit of explanation for the slightly c...
New
JesseHerrick
Hey, I’m Jesse and I’m the main contributor behind Dexter, a full-featured, lightning-fast Elixir LSP optimized for large codebases. It s...
New
kip
Localize is the next generation localisation library for Elixir. Think of it as ex_cldr version 3.0. The first version will be released ...
New
webofbits
Squid Mesh is an open source workflow automation runtime for Elixir applications. It is aimed at Phoenix and OTP apps that want to defin...
New
jimsynz
Beam Bots (or just BB for short) is a framework for building fault-tolerant robotics applications in Elixir using familiar OTP patterns. ...
New
kip
In 2021 I started a new library called Tempo with the objective of modelling time as a set of intervals - not as instants. In 2022 I gave...
New

We're in Beta

About us Mission Statement