If you treat your storage as a separate subsystem you’ll likely not face the problem with `create_changeset`. I’m saddened to see that almost every Phoenix project is tightly coupled with the database as every context function calls the database and clean business logic is nowhere to be found. Also, why are we using the contexts directly in the controllers and keep the top-level module empty?
This confuses me as a relatively green Phoenix dev. Wouldn't this be standard practice? As I've been reading through Programming Phoenix, this is done all over the place. For example, the "show" action for a "user" calls into the Users context to get a User struct, then passes that struct into the view for rendering.
How else would you do it? It would seem to me (admittedly, naively) that anything else would introduce an unnecessary extra layer between the context and the view. What am I missing?
It might be an unnecessary layer but sometimes it might not be. By having structs that you persist and structs that are “bound” to business logic functions you can have clean business logic and treat the persistence as a side effect, which it ultimately is.
If your application is a simple CRUD app, then yes, bubbling up Ecto schema structs to the vies is sufficient.
I'm not positive I see your complaint. Ecto is supposed to be exactly the translation layer between your business logic and your storage layout. Adding another translation layer on top of that just results in a half-baked implementation of half of Ecto. If your Ecto structs aren't useful throughout your application, then maybe you need to do a little more translating in the schema itself.
Hello, article author here. Can you expand on what you mean by top-level module?
My understanding of contexts (from the docs) is that they are intended to "...encapsulate data access and data validation." They're intended to be the layer of your application that talks to the database. Do you have a different approach?
I don’t know what the person you’re responding to is getting at, but I found the article very useful! Exposing the changeset through the context is also useful in LiveViews when dealing with forms — once the changeset is valid, calling the relevant function with the same attrs should succeed.
Re: the sibling comment referring to queries, nothing you’re doing in the article prevents that.
Ecto’s changesets are often an unfamiliar concept, so I wonder if that’s where some confusion is coming from.
I like to use changesets for form validation, for example, but those changesets are different than the ones I use in my public API functions. Most of the time what I present to the user don’t map 1:1 to my Ecto schemas and it doesn’t have to.
This is going to be an unpopular opinion, but I think it's unfortunate that changesets are tied to the views in Phoenix. It's major leakage of ecto and the inner domain at best and at worst it intertwines various concerns - validation and what's changing. The naming of it gives that design choice away. It it didn't it would probably have a name with "validation" in it somewhere. I'd much rather have domain structs implement a protocol. Maybe that's possible I haven't really looked into it.
However, what you say about having different changesets for validation seems to make a ton of sense to me and has given me some ideas. Thanks.
I _think_ they are talking about treating the top level module as the facade for the whole application, which is something I've thought about doing a bunch. IE, it would be full of delegates to all your contexts and then _every_ public function is available off of it.
I don't think this is a terrible idea but I like to draw boundaries and say "It's ok to reach one level deep from any point in the context tree".
ie App.Chat.send_message/2 is cool, but MyApp.Chat.Messages.send_message/2.
Yeah. Trying to teach specific practices confuses people. Naming and "context" should not matter (in a sense of consistency) much .. if at all. My take is "context" is just "module" and schema is just "type". I'd like to think in ML languages style.
> Also, why are we using the contexts directly in the controllers and keep the top-level module empty?
Because the context is the “public API” for the rest of your application to use. You can have as many layers below the context as you want, often purely functional, composable, and testable.
The contexts are boundaries, places where you use process machinery and might call other subsystems, or services, but they’re not the application’s public API. You might want to check Bruce Tate’s book “ Designing Elixir Systems with OTP”.
I get it--or maybe I don't deeply get it-- but I don't really find this a huge concern for most web apps. If I know that my app is never not going to have a db, then what is that extra step of that decoupling buying me? I still have fast tests due to transactional rollbacks, and I can even perform certain business logic in the db nicely with Ecto when it's a better fit than the BEAM.
I'm not really following this. The only problem the author has is with the name `create_changeset`. It may not be obvious to someone just starting with Elixir and Ecto what that means, but it is a naming convention that most (all?) projects use. You should be able to get the hang of it fairly quickly.
This may just be the simplicity of the example they have, but they also have an `update_changeset`, which I think is an anti-pattern. You should be creating a different changeset for each operation that can be applied to a given schema. In the case of a user, you may be able to change a password, or an email address separately from being able to change a screen name or something. You should be creating separate changeset functions for each of those operations.
The author also puts a foot note at the end stating that they would usually create a `base_changeset` function that can be reused for common parts of a changeset. Please don't do this. Just create multiple reusable functions for each check. For example, you may create private functions in the schema for `validate_email`, `validate_password`, etc. These are the reusable pieces that you most likely want to have and can be used from within any changeset. Then you will be able to mix and match them as needed instead of trying to come up with a truly base changeset that applies to everything and still have a lot of validation rules duplicated between various changesets.
> ...they also have an `update_changeset`, which I think is an anti-pattern.
I disagree that it's an anti-pattern. I'm already making a distinction between a "create" and an "update" changeset, as these may have different validation rules.
Of course, some situations may warrant separate changesets for operations on individual fields (updating an email address, updating a password, etc.), but in practise I rarely find this to be useful.
> Please don't do this. Just create multiple reusable functions for each check.
It's worth noting that these aren't mutually exclusive approaches; we regularly use both on my current project.
I will agree that not every situation will require multiple kinds of updates. For those situations an `update_changeset` may be considered okay. I now realize how definite my wording sounds. But I just meant that if you have multiple update operations, that should be exposed as multiple changeset functions. If you have multiple update operations but a single update_changeset function, it is possible to send the server data you did not intend to be sent together and now you are changing things that were not originally meant to be changed together. This has happened in previous projects I have worked on and now prefer the be explicit with multiple changeset functions.
25 comments
[ 4.8 ms ] story [ 69.9 ms ] threadI forgot to mention, whenever I see Ecto schema structs bubbling up to the views, I want to throw up.
How else would you do it? It would seem to me (admittedly, naively) that anything else would introduce an unnecessary extra layer between the context and the view. What am I missing?
If your application is a simple CRUD app, then yes, bubbling up Ecto schema structs to the vies is sufficient.
My understanding of contexts (from the docs) is that they are intended to "...encapsulate data access and data validation." They're intended to be the layer of your application that talks to the database. Do you have a different approach?
https://learn-elixir.dev/blogs/creating-reusable-ecto-code
Re: the sibling comment referring to queries, nothing you’re doing in the article prevents that.
Ecto’s changesets are often an unfamiliar concept, so I wonder if that’s where some confusion is coming from.
However, what you say about having different changesets for validation seems to make a ton of sense to me and has given me some ideas. Thanks.
I don't think this is a terrible idea but I like to draw boundaries and say "It's ok to reach one level deep from any point in the context tree".
ie App.Chat.send_message/2 is cool, but MyApp.Chat.Messages.send_message/2.
Because the context is the “public API” for the rest of your application to use. You can have as many layers below the context as you want, often purely functional, composable, and testable.
This may just be the simplicity of the example they have, but they also have an `update_changeset`, which I think is an anti-pattern. You should be creating a different changeset for each operation that can be applied to a given schema. In the case of a user, you may be able to change a password, or an email address separately from being able to change a screen name or something. You should be creating separate changeset functions for each of those operations.
The author also puts a foot note at the end stating that they would usually create a `base_changeset` function that can be reused for common parts of a changeset. Please don't do this. Just create multiple reusable functions for each check. For example, you may create private functions in the schema for `validate_email`, `validate_password`, etc. These are the reusable pieces that you most likely want to have and can be used from within any changeset. Then you will be able to mix and match them as needed instead of trying to come up with a truly base changeset that applies to everything and still have a lot of validation rules duplicated between various changesets.
I disagree that it's an anti-pattern. I'm already making a distinction between a "create" and an "update" changeset, as these may have different validation rules.
Of course, some situations may warrant separate changesets for operations on individual fields (updating an email address, updating a password, etc.), but in practise I rarely find this to be useful.
> Please don't do this. Just create multiple reusable functions for each check.
It's worth noting that these aren't mutually exclusive approaches; we regularly use both on my current project.