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
itemstable 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
idas 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.
Trending in Questions
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
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
I’m working on a small exercise involving update_in/3, and I came up with this solution:
data = %{
name: "Periodic Table",
category:...
New
I’ve got trouble wrapping my head around the order in which functions are called in this snippet (from Phoenix’s authentication):
toke...
New
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
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
** (ArgumentError) expected :max_attempts to be a positive integer, got: {:@, [line: 10, column: 19], [{:max_attempts, [line: 10, column:...
New
Other Trending Topics
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
Hobbes is a low-level distributed database for the Elixir programming language.
Hobbes provides a simple, safe, and scalable storage lay...
New
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
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
ExRatatui lets you cook up rich terminal UIs in Elixir, powered by Rust’s ratatui via Rustler NIFs. Build interactive terminal applicatio...
New
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
Categories:
Sub Categories:
Forums
Popular Tags
- #ecto
- #liveview
- #troubleshooting
- #learning-elixir
- #library
- #deployment
- #erlang
- #testing
- #genserver
- #mix
- #absinthe
- #remote-other
- #otp
- #plug
- #how-to-question
- #macros
- #postgres
- #elixirconf
- #channels
- #exunit
- #discussion
- #code-sync
- #podcasts
- #javascript
- #onsite
- #dialyzer
- #docker
- #authentication
- #umbrella
- #full-time-contract
- #podcasts-by-brainlid
- #ai
- #ecto-query
- #elixirconf-us
- #blog-post
- #elixir-ls
- #phoenix_html
- #iex
- #graphql
- #genstage
- #websockets
- #supervisor
- #advent-of-code
- #distillery
- #processes
- #elixirconf-eu
- #api
- #forms
- #security
- #metaprogramming










Showing Posts 1 to 10- Show Best Posts
- Show All (oldest first)
- Show All (newest first)
sodapopcan
I’m slightly confused by your wording…
items_aanditems_b, the plural makes me think it’s ahas_many, but you then you saidhash_oneso 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:It basically adds sister versions of a few functions with
_oneadded (kinda like_embedsversions), socast_one,put_one_)assoc, andget_one_assoc(for each you can refer to them by the first argument to theonemacro). There is alsoMyRepo.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:
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
Yeah, good point about the naming — that’s on me. it’s
has_onein practice.Your
one :itemabstraction is actually very close to what I’m doing manually.I’d definitely be interested in seeing it, especially how you handled:
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
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 theonemacro is doesn’t do anything too magical. It’s has a lot in common with howtestworks 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.preloadthat looks for:itemand 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:itemin a preload really does mean theone :itemsince 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
typecolumn.On speaking of that, my lib allows for adding either a virtual or concrete
typecolumn which can come in handy in pattern matching and doingallqueries for a specific type respectively.I’ll let you know when I’ve posted it.
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.sodapopcan
I really like this, thanks for sharing. It’s quite clever and a pretty clean solution.
sodapopcan
As promised here is the repo. I’m a bit embarrassed by the
preload_oneas 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
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
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.
Schultzer
Whats easy in SQL are hard in Ecto.
Thats why I’m building GitHub - elixir-dbvisor/sql: SQL provides state-of-the-art, high-performance SQL integration for Elixir, built to handle extreme concurrency with unmatched expressiveness and ergonomic query composition. Write safe, composable, parameterized queries directly, without translating to Ecto or any ORM. · GitHub to make it easy both places.
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?