Trying to learn some details: Why is the context part of the schema?

I am learning Live View and I am surprised by the way Contexts work.

I am updating the chat example from dwyl, and I want to create a context for the messages: Conversation. When I create it:

`mix phx.gen.context Conversation Message messages name:string message:string``

I was expecting a “Message” and a Conversation context. But I get the Message inside the namespace of the Conversation. So my LiveviewChat.Message becomes LiveviewChat.Conversation.Message.

That looks like against the reason to have a context. What happens if I want to have an admin Context that allows me to do different things? Or an Statistics context to know how many messages in average my users do, while the conversation only allows the user to see their own messages…

I understand that there is a reason for this, but I can’t think of any.

What I am missing? Why is the context part of the data? Would it be ok if I manually change the Conversation.Message into Message or I am missing something?

You will model it yourself, nothing is gonna blow up or give you a warning. The initial scaffolding simply uses modules as semantic namespaces e.g. MyApp.Users.Profile and MyApp.Users.Account etc.

Simply a reasonable default, nothing else.

Yes, do that if it helps you structure your code better to your thought process. Nothing will break.

4 Likes

Contexts are a design pattern, with the main idea to separate implementation logic from the endpoints.

You are free to define them however you see fit, for example if I have functions that include creation of a new user, password change, password reset, I would place them in the same context that I will most probably name Accounts, as I imply that all these functions are related to accounts concept.

2 Likes

Isn’t this still too strong? Contexts are just namespaces. They are not even a default because the user still has to choose them. I actually think the move to introduce “Contexts” as a concept, while understandable in terms of pushing good habits, is a bit unfortunate in the way it misleads new users to think there is something special going on, especially people new to Elixir as well.

2 Likes

Yes the current implementation is simple, however I am positive that the idea is based on a full-fledged pattern, there was a talk by Chris about this.

They’re based on Bounded Contexts from Domain Driven Design. But it’s not meant to be a strict implementation of it, just inspired by.

7 Likes

You’d then set up a separate schema for LiveviewChat.Admin.Message or LiveviewChat.Statistics.Message.

In Ecto, schemas are less tightly coupled to database tables than other ORMs like Rails’s ActiveRecord where one record generally gets mapped to one object. This separation is a bit more verbose but offers much more control by letting us explicitly set how objects are mapped for a given context based on how it will be used in that given context.

So if for privacy reasons, the statistics context should not analyze the message body, Statistics.Message schema could simply not map the message body and instead map only the fields relevant for analytics. Another example from an older version of the docs if memory serves was a users table that maps to Accounts.User and Blog.Author.

4 Likes

I mean you did choose to use the context generator, as the help says: “Generates a context with functions around an Ecto schema.”

Maybe you wanted to use the schema generator to create your standalone Message?

I agree, my question is not about the functions, but about the data.

The model is stored in the same context, but it should be independent of it, or is it not?

I mean, you create a context to access data from the model in a definite way. You access User through Accounts because you are managing the way you access the model in an specific context, that of accounts.

I feel that when your model is inside the context, you are tying the model to the context, and that is exactly the opposite of what you are trying to achieve when you create the context.

Again, I am trying to understand what is behind the best practice, because I get confused.

Actually, I was copying the dwyl/chat example, trying to update it to the newer Live View, and they use the schema generator, and then add the functions without a context.

I thought it did make sense to use a context. Because Message can be used in different ways (I am using this as the first step on my own personal project), and of course, the schema is LiveviewChat.Message, while the schema in the context is LiveviewChat.Conversation.Message.

For me, both are the same concept. I have a message, and I create a concept (the conversation), that will allow me to find those messages related to an specific conversation id, in order, etc… Or I could find the ones for one specific user, independent of the conversation, or … But the message is still the message, what is changing is the context where I use it, and I got confused.

As my train of thought is not aligned with the generator, I am trying to understand what is the original reason to do it this way so I can learn and design it properly next.

I would read the Contexts guides in the official docs if you want to fully understand. It’s a bit beefy, though you can also read a quick summary of bounded contexts here if you’re wondering why the generators act like they do. @codeanpeace’s answer also gives a very good summary.

Those are closest to “best practices” you are going to get, but Phoenix contexts are designed in a way that you can do whatever you want. You could even not use them at all like that chat project, but that makes it a particularly poor example when it comes to learning about contexts! Some people just make a large lib/schemas/ directory and store all their schemas there. You could also do something that is more classic OO modeling and have a 1-to-1 context-to-schema (which the generators are still helpful for). There are lots of options.

One thing I would advise which may seem like a nitpick is to not think of the schema as the “model”. It’s merely a schema describing the data you want in a certain context. The data itself is coming from the Repo. Again, it may seem nitpicky but it’s pretty key to understand that schema and repo are decoupled. Again I will point to @codeanpeace’s answer showing how the same concept is represented in different contexts as schemas. In that example there is Accounts.User and Blog.Author. Both those schemas represent some kind of entity the data of each type could well be coming from the same record in the users table!

1 Like