Django's SimpleLazyObject pattern in Phoenix

In Django’s authentication middleware the user object is assigned to the request using a SimpleLazyObject.

Using the SimpleLazyObject a database query to fetch the user from the database will only be executed if the user object is accessed during the request/response cycle.

Another way of achieving this pattern in Python is to cache method return values on the object. eg

class UserMiddleware:
    @property
    def user(self):
        if not hasattr(self, '_user'):
            self._user =  # Fetch user from database
        return self._user

This requires storing state on the UserMiddleware object and return a User object. I know in Elixir you can only return a modified value, you can’t store state like Python in a class object.

How do I achieve the same outcome in Elixir? Perhaps using a server process to encapsulate the state?

Ok, I think I’ve managed to prevent repeated database calls using an Agent

defmodule Auth do
    use Agent

    alias Foo.Accounts

    def start_link() do
        Agent.start_link(fn -> nil end, name: __MODULE__)
    end

    def user(user_id) do
        Agent.get_and_update(__MODULE__, fn state -> 
            case state do
                user ->
                    {user, user}
                nil ->
                    user = Accounts.get_user!(user_id)
                    {user, user}
            end
        end 
    end
end

Just need to figure out how to start the Agent on every incoming request.

Hi! This approach with a named agent won’t work because multiple requests are handled concurrently, bit you’re only storing a single global user.

The recommended approach is to fetch the user when needed and then to store it in conn.assigns.

3 Likes

Besides manually managing your conn.assigns there are quite a few data-caching libraries in Elixir that for instance sit between your web-application and your database. Cachex and Nebulex come to mind, but they are by no means the only ones.

1 Like

Implicit DB loads are generally seen as an anti-pattern in the Elixir world. For instance, loading data while rendering a page makes it impossible for Phoenix LiveView to figure out when a re-render is needed.

An interesting approach is the fetch_cookies function in Plug: if cookies have already been fetched, it’s a noop, and otherwise it fetches them and stores them in the conn. It uses a special struct %Unfetched{} to represent that the data isn’t loaded yet.

2 Likes

The same is done trying to preload already loaded associations in ecto.