TLDR; Is this a good way to writing context functions? Especially when preloads are nested, or for some other reason there is some complexity. If not, what is the way to go?
I was reading the topic Preloading, some of the time, all of the time, none of the time? from some years ago. In the topic a number of different approaches to implementing context functions are mentioned. It also covers when to preload and why, as the title of the referenced topic suggests.
I rewrote a get_post/2
function of mine, because after have read the mentioned topic, among others, I felt it needed improvement.
How close is this to what could be considered good practice? Am I missing something still? For example, will this approach bite me later on, when requirements shift?
One thing I noticed myself is that my context module now has a lot more private functions in it than before. Those distract a bit from the top level functions that are actually the ones that I would call from the web layer. Do you put these private functions somewhere else? For example, under the post schema in Post.ex
?
Quick background: A post has many post replies. And a post reply has many post subreplies. The post and post reply schema’s each have a virtual field for the (sub)reply count.
@doc """
Returns the post with the given `id`.
## Options
* `:preload_user` - preload the user association of the post, its replies, and its subreplies (:all), or a keyword list of fields to select from the user table
* `:preload_replies` - a boolean indicating whether to preload the replies association (default: false)
* `:preload_subreplies` - a boolean indicating whether to preload the subreplies association (default: false)
* `:with_reply_count` - a boolean indicating whether to preload `:reply_count` and `:subreply_count` virtual fields (default: false)
## Example
%Post{} = Posts.get_post(
post_id,
preload_user: [:id, :avatar, :username],
preload_replies: true,
preload_subreplies: true,
with_reply_count: true
)
"""
def get_post(id, opts \\ []) do
from(p in Post, where: p.id == ^id)
|> add_reply_count(opts)
|> preload_user(opts)
|> preload_replies(opts)
|> preload_subreplies(opts)
|> Repo.one()
end
defp add_reply_count(query, opts) do
case Keyword.get(opts, :with_reply_count, false) do
true ->
from post in query,
left_join: reply in assoc(post, :replies),
left_join: subreply in assoc(reply, :subreplies),
group_by: [post.id],
select_merge: %{reply_count: count(reply.id, :distinct) + count(subreply.id)}
_ ->
query
end
end
defp preload_user(query, opts) do
preload_user = Keyword.get(opts, :preload_user)
case preload_user do
:all ->
from q in query,
preload: [:user]
nil ->
query
fields ->
user_query =
from u in User,
select: ^fields
from q in query,
preload: [user: ^user_query]
end
end
defp preload_replies(query, opts) do
case Keyword.get(opts, :preload_replies) do
true ->
replies_query =
from(PostReply)
|> sort_by_inserted_at()
|> add_subreply_count(opts)
|> preload_user(opts)
from post in query,
preload: [replies: ^replies_query]
_ ->
query
end
end
defp sort_by_inserted_at(query) do
from q in query,
order_by: [asc: q.inserted_at]
end
defp add_subreply_count(query, opts) do
case Keyword.get(opts, :with_reply_count, false) do
true ->
from reply in query,
left_join: subreply in assoc(reply, :subreplies),
group_by: [reply.id],
select_merge: %{subreply_count: count(subreply.id)}
_ ->
query
end
end
defp preload_subreplies(query, opts) do
case Keyword.get(opts, :preload_subreplies) do
true ->
subreplies_query =
from(PostSubreply)
|> sort_by_inserted_at()
|> preload_user(opts)
from subreply in query,
preload: [replies: [subreplies: ^subreplies_query]]
_ ->
query
end
end