brettbeatty

brettbeatty

How do y’all organize your Ecto changeset functions? Right now I’ve got a mix of two different styles in a codebase:

  • “one changeset to rule them all” I just have a changeset that can modify any non-auto fields and use it anywhere I want to perform inserts/updates with the schema
  • “a changeset per operation” I currently prefer to have one changeset per update operation. So if I have my_field, my_field_type, and my_field_status as three separate fields that usually get updated together I might have a my_fieldset_changeset function that casts and validates just those fields.

I like having a changeset per operation because I know it will only change the fields I intend to change, and (this may be bikeshedding on my part) if I change where data lives it’s easier to find the exact places that change those fields.

But sometimes I find myself wanting to break up changesets more by role, separating the fields that can be updated by my app from the fields that can be updated by a user (or whatever untrusted input). I’m envisioning either a system_changeset/2 and a user_changeset/2 or a changeset/3 that looks something like this:

def changeset(my_resource, system_attrs, user_attrs) do
  my_resource
  |> cast(system_attrs, __schema__(:fields))
  |> cast(user_attrs, @user_editable_fields)
  |> other_validations()
end

I kind of like this idea because then anywhere the user passes params, even if they pass params unintended for the operation, we still limit them to only fields they’re allowed to update anyway. So kind of like a middle ground between my existing changeset patterns. So the tradeoff with my per-operation pattern would be giving up some of the understanding of where updates happen for just having fewer changeset functions to worry about.

I do think the changeset/3 example above has one big downside: I use changesets very differently for user-provided data and system-provided data. With untrusted data I perform all the validations and do an insert/update that may return a changeset if the validations fail. If I get something wrong with the system-provided data I don’t want to tell the user they did something wrong or potentially expose fields they know nothing about. With trusted data I don’t do as many validations and do insert!/udpate! because it’s more exceptional for the data to be wrong. I could just call the changeset/3 example with an empty map in either attrs column and only do the updates I want, but I would maybe tend to just do separate changesets at that point.

I guess for me too having a more universal+/system changeset has the bonus of working better for seeds and test fixtures where I may want control of every single field.

But I’m curious how the community tends to organize changesets. What patterns or principles have you found helpful when working with changesets? I feel like I have to be over-complicating the issue.

Showing Posts 1 to 8

egze

egze

You can also do like @sasajuric https://medium.com/very-big-things/towards-maintainable-elixir-the-core-and-the-interface-c267f0da43 and move changesets to your context.

Me personally, I like to stick with a single schema changeset function and have special changeset functions rarely, when needed. For special fields like :is_admin or something.

garrison

garrison

I keep separate changesets for different functionality. You can also compose changesets, which I don’t see mentioned often.

E.g. I have a Bookmark schema which holds both user fields (the name and url) and data fetched from the web (title, page content, an image url, etc). Both need to be validated, but sometimes I only need to validate the latter (when fetching later).

So on creation I can do this:

%Bookmark{}
|> Bookmark.user_changeset(%{name: "foo"})
|> Bookmark.metadata_changeset(%{title: "Hello world"})

But if I was just updating the fetched metadata I could do this:

Bookmark.metadata_changeset(bookmark, new_metadata)

The distinction in this case is that the user fields are validated for the user while the metadata fields are validated with very large constraints mostly as a sanity check (e.g. 4096 char limit for a URL, which should never be hit by normal use).

al2o3cr

al2o3cr

IMO functions are basically free, make as many of them as you need to keep things clear.

The worst possible messes I’ve seen in code have been when everything was jammed through a “universal” function and then the needs of different callsites diverged. Suddenly every place that Foo.change_all_the_things is called needs to be reviewed (it’s covered by tests, right?)

Regarding the last case you mention (test setup / factories) - there’s no rule that says those can’t have their own test-specific modules. I’d keep the “change every column” stuff there in preference to having it sitting around in the main app waiting for misuse.

dimitarvp

dimitarvp

Same. Good prefixes do the job, provided you don’t need 20+ changesets.

tfwright

tfwright

Absolutely one of my favorite things I’ve picked up from him. That tip alone convinced me to read Elixir In Action. It can be combined with “changeset functions” by abstracting common pieces into the schema when that becomes a good refactoring opportunity.

brettbeatty

brettbeatty OP

Hmm yeah that resonates with the part of me that wants a stronger separation between my web layer and the rest of my app.

Yeah I think I’m coming back around to that approach.

I think there’s a balance there, balancing “as many of them as you need” with DRY principles. I’ve written functions before that did essentially the same thing as each other with minor differences, then as “essentially the same thing” became increasingly complex how each function handled the edge cases started unintentionally to diverge a bit. But that’s probably more a sign I needed to refactor at that point. And they weren’t just changeset functions.

I like that idea. Do you find that the common pieces are usually more like entire changeset functions for a subset of fields or more like custom validations and such?

tfwright

tfwright

Usually there is a set of base required fields and other validations that I put there. Logic that would be shared between all context functions manipulating changesets, less to DRY anything up and more to make things explicit and as self-documenting as possible. I tend to be really particular about API surface so I generally only cast fields, for example, that are needed by some client for a specific operation, so I don’t find putting cast logic there to be as helpful as it would be if you just include all fields by default.

cjbottaro

cjbottaro

I’m probably going to get drug through the mud for this, but…

I just put changeset functions directly in my LiveView, not in my models. So far, I’ve never had to reuse them, so I just make them specific to the page they are being used on.

Further, more often than not, the form being rendered does not map 100% cleanly to any model. So there is an an Ecto embedded_schema directly in the LiveView also… :smiley:

If there is a need for sharing, I’ll refactor into a model or context.

— All posts loaded —

Where Next? Top

Trending in Discussions Top

AstonJ
As the title says, please share what you’ve been up to with Elixir. Whether that’s been learning it, looking into it, making stuff with i...
2977 91898 914
New
AstonJ
The obligatory hello world thread! Who are you and where are you from? :stuck_out_tongue:
4616 55835 594
New
byu
@chrismccord : I just saw the Extract AGENTS.md from Phoenix.new into phx.new generator commit to the phoenix project. My initial shotgu...
New
arcanemachine
I was working on an Ecto migration and I needed a timestamp. So, for the nth time, I looked up the different data types for timestamps, a...
New
alexslade
Fly’s CEO posted this recently - Turn And Face The Strange · The Fly Blog It says that Fly is going all-in on sprites, which is a worry ...
New
Herve37
We’re evaluating API mocking tools for OpenAPI-based projects and would love to hear what other teams are using. We’re particularly inte...
New
matt-savvy
Is there a word for the ~> symbol used in Version strings? Do you also just call it a Squiggle Arrow™ ?!
New

Other Trending Topics Top

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
jimsynz
Beam Bots (or just BB for short) is a framework for building fault-tolerant robotics applications in Elixir using familiar OTP patterns. ...
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
Damirados
Hello everyone. After busy few months I am happy to announce v0.1.0 of Emerge & Solve. They are GUI (Emerge) and State management (S...
New
netoum
Corex is an accessible, unstyled UI component library for Phoenix that integrates Zag.js state machines using Vanilla JavaScript and Live...
New
ausimian
Emily is an Elixir library that runs Nx computations on Apple’s MLX. Install it as the default Nx backend and Nx, defn, Axon, Nx.Serving,...
New

We're in Beta

About us Mission Statement

Options

Thread Display Mode




Thread Preview

Skip Thread Previews