Where is the best place for additional model functionality?

In many of the examples I’ve seen, Ecto models are kept simple. They only define a schema and changesets. I don’t see developers baking in additional functionality such that the model is significantly more than a wrapper around the database row.

But sometimes I need to define functionality specific to one model that will be used in several places. For instance, I have a function that returns true or false depending on the output of an Ecto query. I’m used to fatter models that know lots about their domain, and can then be referenced from traditional controllers/GraphQL schemas/whatever else we might imagine in the future without the need to teach each of these things how to interact with the model.

I’ve taken to stashing reusable query fragments in descriptive functions on the model, then composing them at the call site if I need some additional functionality. But where should I place a query that needs to run itself against the repo, then return a more appropriate result based on the result of the query?

Normally I’d just put it in the model as well, but given that models and repos are separate Ecto concepts, that the model essentially calls the repo module directly feels like a bit of a smell.

Should I put the code in the model, then accept the repo as an argument so the two aren’t dependencies? Is there a scenario where that actually yields a benefit, and isn’t just extra ceremony?

I suppose it isn’t a huge deal right now, but I’m trying to start out with Elixir/Ecto/Phoenix best practices early on.

Thanks.

6 Likes

I think the confusion probably comes from calling it “Ecto models” in the first place. What Phoenix docs call a model, Ecto actually calls it a schema. Ecto schema is a data mapper, and as such, its purpose is to describe how the target data source is represented as an Elixir struct (via the schema macro) as well as defining the constraint and validations of such mapping (via changesets).

If I were being pedantic, suppose we have a User entity, my approach would be: put only the schema definition and changesets on the MyApp.Schema.User module, put the composable query fragments in a MyApp.Query.User or MyApp.UserQuery module, and lastly put the repo operations in a separate MyApp.User module (which some might call a “service” layer).

Only modules in service layer can call the schema and query. Controllers should only interface with the service layers. We will then have a nice separation between the data layer and the web layer, enabling you to have other client for the data layer (probably via CLI).

That being said, this approach might be overkill in some cases, and the service-schema-query relationship might be tightly-coupled across entities. What I like about Elixir is that refactoring is darn easy, that you might start with a typical “fat model” for now and extract the modules should it become a problem later.

6 Likes

pretty much that

I also thought about the whole new schema / model / service separation and ended up with models (schemas) not using repo but having changesets and queries and a service Layer that does heavy lifting when needed

one exception to the “no repo in models” rule being “already taken” kinds of validations that have to hit the DB to be able to determine the validity of data, I don’t know how to avoid using repo in schema in this case, maybe someone has an idea

1 Like

try to keep your models and repo separated and if you want to share some functionality between models simply use helper. You can create a model_helper or even new directory in helpers if you have large number of models and use proper helpers by their functionality.

In my opinion if you need to do something with repo and model and it’s gonna be shared by other models as well, it’s helper for the controller. Don’t you think? :wink:

1 Like

At the moment I believe “no repo in models” != “no DB hits”, since checking for uniqueness can be done using a unique index on the database level and a unique_constraint in your changeset, without having to call Repo. But yes, it would make the changesets impure since it has to make a call to DB.

1 Like

At the moment I believe “no repo in models” != “no DB hits”, since checking for uniqueness can be done using a unique index on the database level and a unique_constraint in your changeset, without having to call Repo. But yes, it would make the changesets impure since it has to make a call to DB.

I meant cases that went beyond the DB constraints so I had to actually do a DB hit manually, e.g. I have a “change email” feature in an app that only uses confirmed emails. It goes in two steps, first the user enters the desired new email and it is saved in the unconfirmed_email field, second - they follow the confirmation link sent to their new address and the new address actually moved to the email field.

Step 2 is covered by unique_constraint, but in the first one, I have to check if the email is taken using email field and actually save it to unconfirmed_email, Ecto might have some trick to do that without manually hitting the DB, but I didn’t find it.

1 Like