I read different opinion and approach on the forum about preload of assoc, but I cannot decide how to handle “cross context preload”.
Let’s say I have the typical Post and Comment schemas in the Blog context, and each comment and posts has many Likes and the Like schema is in the SocialMedia context, with things like like_post(post)like_comment(comment)…
@kuon A preload will not generate N+1 query. It’ll generate a 1+1 query (one query for the data and one query for the preloaded date that Ecto will join). So if this is your only concern, I’d go for it.
When faced with these sorts of questions, I settled on only ever passing query functions that exist in the same context as the function that will eventually interact with the Repo.
I’m not sure this is the right answer, but I did it for predictability, explicitness, and to protect against changes within the context. From outside the context, I don’t have to care if preload_post_likes directly specifies the query or if it delegates out to something in SocialMedia as that may change in the future.
Why not put Repo.all in the context? I feel this misses the point of contexts. A context should encapsulate your domain logic. Composing queries in a controller means the controller now understands concepts in your domain. Ideally, the controller would just call Blog.list_posts because that is all it cares about. Blog.list_posts and compose in the likes and fetch from the repo with the controller being none-the-wiser.
It’s totally ok for contexts to talk to each other so long as it’s happening through the “aggregate root” (ie, the context file, even if this isn’t the 100% true definition).
Your Blog.list_posts_with_likes/0 is totally acceptable. So long as you are only referencing SocialMedia in the Blog context and not Like directly, you’re fine.
There are other ways to get totally clean separation but they are complete madness outside of a very complex project and I don’t want to go down that rabbit hole here. Your non-N+1 solution is correct!
I’m working on a project where we have modules like SchemaQuery where we put “shared” query logic to be consumed from other contexts. For your use case I’d do something like this:
posts.ex
import MyApp.Posts.PostQuery
def list_posts() do
Post
|> preload_likes()
|> Repo.all()
end
posts/post_query.ex
def preload_likes(queryable) do
# do the preload logic here
end
“just cross the graph from the entity you are loading”.
But that means that the queried context has knowledge of the internal of the other contexts. But I guess it is a coupling we cannot avoid, as they are coupled in the DB anyway (foreign key…)
That’s incorrect. Repo.preload can receive a list, so you can just do Repo.preload(posts, :likes) and it will preload likes for all posts, with a single query. This is how we avoid n+1.
You are correct, but my example wasn’t reflecting the real situation. In my app, I have a place where I can either preload everything with a left lateral join, or afterward one by one, that’s why I used the Enum example.
It still seems to me that this could be a modeling problem. For instance, does it really make sense for your application to have separate contexts for Post and SocialMedia in the sense that likes are “completely” decoupled from posts? Remember that contexts are not exactly “bounded contexts” in the strict sense, even though they can certainly act like it if you want to - but in the end, there’s nothing preventing you from crossing those “concerns”, contexts are just modules that abstract shared logic.
Depending on the use case I don’t think I’d separate one thing from the other. But I’m making some assumptions here, could you share a little bit more about your project?
I cannot share details about the project. But your point make sense. In the app, the different context are more here to “organize API” that to separate really different concern.
I was thinking about this more and came back to answer and @thiagomajesk said what I wanted to say and more.
Alternatively, if the SocialMedia context is necessary and you don’t want to cross contexts, you can always make a PostLike and/or a CommentLike in the Blog context that reads from the same source as SocialMedia.Like.
It seems that our suggestions are very much aligned @sodapopcan. Oddly enough, I was about to suggest a similar solution to @kuon …
You could have something like a SocialPosts context that ties the concepts of both post and like working together. Both post and like could be a subset of the whole “post” and “like” schemas you already have.
Something like: social_posts/post and social_posts/like would abstract the domain “relationship” between posts that have social media requirements embedded into it.
It’s also important to remember that we can have multiple schemas representing distinct domain facets (loosely speaking). You are also not required to have schemas backed by a database (they could work similarly to value objects in DDD if necessary).