coen.bakker

coen.bakker

How best to write context functions for more complex cases?

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

Most Liked

adw632

adw632

If it were me I would create my queries separately to my context functions and build out the semantic context functions using the queries and schema modules.

In my query module I would expose those preload functions and let the caller (the context module) decide what they need by chaining them together. I would allow specifying options to those query functions (like fields to return), and perhaps some sort and aggregate helpers also.

Context should be high level enough that for callers (eg LiveView or controller actions) it would not matter if your entire backend storage layer changed. Generally I think of context methods as orchestrating actions. If they involve multiple resources or a mix of ecto, external apis, sending email or pubsub notifications it shouldn’t matter.

LostKobrakai

LostKobrakai

I’m not sure there’s much consense of how exactly to write that portion of you codebase.

The best way to prepare for future requirements is make it easy to throw away the current code and replace it wholesale (https://www.youtube.com/watch?v=1FPsJ-if2RU). There’s no way to know what future requirements will be, so trying to cater to them is guesswork at best.

On a general note I’d always suggest to start with less abstraction (less complex parameters) and more distinct functions than the other way round. It leads to the above, but also means you discover useful abstractions rather than imagine them to be useful. Simpler more distinct functions should help against “just let this existing function do one more thing”.

Your example would lend itself to extacting common query manipulating functions into their own module. My suggestion for learning how to do layering in code (without necessarily buying into buzzword architecture) would be buying “grokking functional programming”. It has a few great chaptures on how to build larger stuff out of smaller pieces and how those layers should depend on each other (or not).

egze

egze

We are doing something similar at work, but we generate all the functions and they follow the same naming convention.

The problem with

  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

is that you can’t reuse it for other contexts, as it is too specific for the Post schema.

We have something like this in all contexts:

use MyApp.Context,
    queries: MyApp.Queries.ActionQueries,
    schema: MyApp.Schemas.Action

And it generates def get_action(action_id, opts \\ []) and def list_actions(opts \\ []) and some other stuff based on the schema name.

Where Next?

Popular in Questions Top

New
myronmarston
The Elixir Typespec docs show the following syntax for keyword lists in typespecs: # ... | [key: type] # keyword lists...
New
JulienCorb
I am trying to implement my new.html.eex file to create new posts on my website. new.html.eex: <h1>Create Post</h1> <%= ...
New
freewebwithme
Using vs code and installed ElixirLS: support and debugger. And I got an error popped up on start up says Failed to run ‘elixir’ comma...
New
nobody
Hi! In PHP: $_SERVER[‘SERVER_ADDR’] - in Elixir? Searched the docs for ip address and the web, no good results. Thanks!
New
nsuchy
Hi. I’ve noticed that Windows Powershell has it’s own IEX command and you cannot access Elixir’s IEX due to the conflict. This isn’t a cr...
New
Brian
What is the proper way to load a module from a file in to IEX? In the python world, doing something like this pretty standard: from ....
New
marick
I had some trouble figuring out how to make many-to-many associations work. Once I got it working, I wrote a blog post. Because I’m a nov...
New
openscript
Hello! Sorry for this astonishing simple question, but I’m really stuck. I try to set up the intellij-elixir plugin, but I don’t know ho...
New
jononomo
For some reason my phoenix channels are working for me in my local dev environment, but as soon as I deploy via Docker, I get a 403 error...
New

Other popular topics Top

vertexbuffer
Hello, can anybody help here..? I have a list of players and I what to delete an element, but every for loop the list is reverting to ori...
New
aesmail
Hello guys, I have finally made it. I created an admin interface for a framework. It’s been on my todo list for years and with the curre...
New
Emily
I have VueJS GUIs with the project generated using Webpack. I have Elixir modules that will need to be used by the VueJS GUIs. I forese...
New
freewebwithme
Using vs code and installed ElixirLS: support and debugger. And I got an error popped up on start up says Failed to run ‘elixir’ comma...
New
sergio_101
I am VERY much an elixir newbie. I have taken one elixir course and one phoenix course on Udemy. During that course, I saw the instructor...
New
fayddelight
I tried installing elixir 1.11.2 erlang 23.3.4 via asdf in my zsh shell. Enabled the versions locally and globally. When I list them ...
New
AstonJ
Please see the new poll here: Which code editor or IDE do you use? (Poll) (2022 Edition) It’s been a while since we first asked this, I...
208 31142 143
New
ashish173
I am using Ecto timestamps with postgres, I can see the timestamps() use the :naive_dateime but for my use case I wanted to store the ti...
New
hariharasudhan94
I would like to know what is the best IDE for elixir development?
New
dogweather
I wrote this comment on r/haskell, and it’s not popular there. :wink: But I think I’m on to something… Haskell reminds me of Java, and e...
New

We're in Beta

About us Mission Statement