Use Repo in Ecto "model"? When to use changesets?

Hi!

I have two questions.

  1. Use Repo in models?
    I rarely see any Repo... function calls in the models. Why? Is it “only” to make the functions have less side effects? Is it better to always execute the Repo... functions in the controllers?

  2. When to use changesets?
    Should I always use changesets when updating a model?

Example:
On my User I have a confirmation_token. When the user is confirmed this token should be cleared.

Option 1

# Controller
...
Repo.get_by!(User, %{confirmation_token: token})
|> User.confirm_changeset()
|> Repo.update!()

render(...)

# User model
def confirm_changeset(user) do
  change(user, %{confirmation: nil})
end

That code works fine, but would it be better to just do something like:

Option 2

# Controller
User.confirm!(user)

render(...)

# User model
def confirm!(user) do
  Repo.update!(user, confirmation_token: nil)
end

So, my gut tells me that option 1 is the better choice here… But (if so) why really? Keep the model pure and make the controller be un-pure?

1 Like

Use Repo in models? I rarely see any Repo… function calls in the models. Why?

It’s to keep two completely different concerns separate. One is working with your data regardless of where and how you persist it and another is executing calls to the persistence layer itself.

I’d definitely recommend keeping those separate for variety of reasons. One is keeping the models as thin as possible. This is one of the oldest topics among Rails developers and multiple patterns were invented to keep devs from putting all business logic in models. Phoenix and Ecto invite you to that, and try to keep you away from making the same mistakes, by introducing Repo as a separate entity. It’s a bit similar story as with model callbacks (which I wrote about here).

Another reason is ease of unit testing. You can test changesets and alike without any involvement from the database. It may be a life-saver in larger projects but it’s nice to keep the same practice in projects of all sizes.

But most of all, Repo should be used in the controller as that is the place where all world-changing functions should be called and success/failure scenarios that follow should be laid out. This way you’ll understand what meaningful changes each controller does by just one look at it instead of jumping from model to model in order to see what User.confirm!(user) does. You can delegate smaller parts of larger actions to separate functions or modules (like service modules) but the basic layout should be as transparent from the top level (the controller) as possible.

When to use changesets?

It depends on what you do in them and if you need that in a particular place. If you’re writing seeds code and you know you can populate records properly without them, you could probably go without a dedicated changeset. But usually you’ll get back to using them as they’re so useful and make the system much more error-resistent.

I’d also say they should be always used when input from forms is involved as they’ll handle casting values, validating, handling constraint errors and so forth. You almost always need those functionalities in order to handle form input and respond to it properly.

2 Likes