PaleWatcher

PaleWatcher

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.

Showing Posts 1 to 10

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.

PaleWatcher

PaleWatcher OP

Yeah, good point about the naming — that’s on me. it’s has_one in practice.

Your one :item abstraction is actually very close to what I’m doing manually.

I’d definitely be interested in seeing it, especially how you handled:

  • dispatching based on type
  • and preloading (that part gets messy fast)

Also curious: did you end up using this pattern long-term, or did you move away from it?


I’m definitely interested, mostly from a learning perspective.

This approach feels “right” to me so far — especially compared to pushing everything into a JSON field. Even if it means a few extra tables and joins, I like that it keeps things well-structured and avoids nullable fields.

I should also mention that I haven’t really worked with macros yet, so seeing how you approached that part would be especially interesting.

On the UI side, I’ve also tried modeling this in LiveView forms using function components for each subtype, so each type has its own set of inputs. It works, but after a while it starts to feel a bit cumbersome to maintain.

sodapopcan

sodapopcan

Sure thing! I’ll clean it up and push it a bit later and let you know.

Note I made a mistake in the API I showed which I’ve edited. I just forgot the belongs_tos so the one macro is doesn’t do anything too magical. It’s has a lot in common with how test works which is outlined in the official guides.

And yes, preloading gets hairy if they are deeply nested, but it’s just a wrapper around Repo.preload that looks for :item and replaces it with the proper preload. Of course, it’s not that simple because it actually has to reflect on the schema to ensure that :item in a preload really does mean the one :item since it could refer to something else in a nested preload. That’s where it gets messy.

I’m sure my old team has not moved from this pattern (I only left a few weeks ago, lol) and ya, I don’t think there is anything wrong with it. Ecto very specifically does not support polymorphic associations with a type column.

On speaking of that, my lib allows for adding either a virtual or concrete type column which can come in handy in pattern matching and doing all queries for a specific type respectively.

I’ll let you know when I’ve posted it.

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
sodapopcan

sodapopcan

I really like this, thanks for sharing. It’s quite clever and a pretty clean solution.

sodapopcan

sodapopcan

As promised here is the repo. I’m a bit embarrassed by the preload_one as I was in the middle of working on it (this was made with 0 vibes) but I keep hopping around on projects and I need to stop doing that so here ya go!

I think the approach linked by LK is a much more interesting way of doing this, although the same Ecto API could still apply.

Also the README is not fleshed out at all but the all the docstrings are.

pjode

pjode

This is a non-answer but something I learned to do early on in a project is not even worry about it and just have all the columns on the one table and then document the schema which fields belong to which subtype and then have separate changesets for creating the different subtypes.

This is usually good enough until a better understanding of requirements show themselves.

sodapopcan

sodapopcan

This is the strategy my library was trying to solve. We did just this at my last job but it ballooned very quickly. EDIT: By “solve” I mean make easier, but same idea: it’s when you put all the sub types right on the parent table and only allow one of them to be present.

sodapopcan

sodapopcan

Oh ya, I keep meaning to check this out. It looks really nice I just haven’t played with it yet as there are a bunch of things I wanna do (and someone recently starting to use AI a bit has made that worse). I’ve also been heavy into an Ash project and I’m ass-u-me’ing it wouldn’t make much sense to use in Ash?

Where Next? Top

Trending in Questions Top

katta
I having some trouble figuring out if I have set myself too strict of standards for my production server. Currently I can handle 75% of r...
New
achenet
Hello, I’m trying to build a basic Phoenix web-app, and I’d like to use Tailwind. However, when I launch mix phx.server, I get an error...
New
Cxx-mlr
I’m working on a small exercise involving update_in/3, and I came up with this solution: data = %{ name: "Periodic Table", category:...
New
ChrisAmelia
I’ve got trouble wrapping my head around the order in which functions are called in this snippet (from Phoenix’s authentication): toke...
New
unaware8150
Hello folks! So at work, we are seeing some situations where we have to define some “fixed” strings that are used across the codebase in...
New
dillonoconnor
Is there any way to avoid the Hologram compiler running when using iex? It seems like the front-end code could potentially be disregarded...
New
thiagogsr
** (ArgumentError) expected :max_attempts to be a positive integer, got: {:@, [line: 10, column: 19], [{:max_attempts, [line: 10, column:...
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
garrison
Hobbes is a low-level distributed database for the Elixir programming language. Hobbes provides a simple, safe, and scalable storage lay...
New
budgie
A little off-topic, but I feel like people here have a good head on their shoulders. I used to be quite good at making software. Was luc...
New
KristerV
Hey. Is there anyone here who creates agents in their apps? Not talking about using agents, but creating them. I’m finding it pretty diff...
New
mcass19
ExRatatui lets you cook up rich terminal UIs in Elixir, powered by Rust’s ratatui via Rustler NIFs. Build interactive terminal applicatio...
New
georgeguimaraes
Just published claude-code-elixir, a plugin marketplace for Claude Code with Elixir support. These are the plugins I’ve been using for my...
New

We're in Beta

About us Mission Statement

Options

Thread Display Mode




Thread Preview

Skip Thread Previews