What's the best approach to use Cachex with Ash framework for read query?

Is there an elegant way of using Cachex and Ash Framework?

I have a read query that I need to cache results for, because of the many relationships attached to it. I wonder if there’s a better way of caching results while reading in Ash Framework.

Here’s how I am doing it from the domain module.

  @doc """
  Get cached person if exists, otherwise fetch from the DB and cache for future fast-reading
  """
  def get_cached_person_first(person_id, context) do
    case Cachex.get(:person, person_id) do
      {:ok, %{} = person} ->
        person

      {:ok, nil} ->
        Hr.People.get_loaded_person!(person_id, context)
        |> cache_person()
    end
  end

There is no Cachex extension or anything like that at the moment.

A more Ash-centric approach would be to check the cache in the preparation of the read action, and if so, you can set the data on the query, and Ash won’t do the roundtrip to the database.

https://hexdocs.pm/ash/preparations.html
https://hexdocs.pm/ash/Ash.Query.html#set_result/2

You can add an after_action hook into the read action to populate the cache

I would create a specific read action for this.

You can also use a global change that runs on updates and destroys to invalidate the cache.

Caching involves a lot of complexities, make sure you really need it :wink:

1 Like

Every extra layer adds complexity and caching is one of those layers that tend to become complex. Most databases can be tweaked to cache more (they already do, but default settings are generally quite low) and if tweaking is enough, the complexity is completely handled by the database. Let the database do the heavy lifting for you (also research ‘materialized tables’)

That being said: let’s continue with the original question below.

1 Like

I am attaching a code, for the sake of reference. Combing through different resources. To come up with possible answer.

action

    read :read do
      primary? true
      pagination offset?: true, default_limit: 10
      argument :check_cache, :boolean do
        default true
      end
      prepare Preparations.CheckCache
    end


defmodule Preparations.CheckCache do
  @moduledoc """
  Checks a simple agent cache for libraries
  """
  use Ash.Resource.Preparation

  def prepare(query, _, _) when query.arguments.check_cache == true do
    CacheHelper.attach_cache(query)
  end

  def prepare(query, _, _) do
    query
  end
end
  def attach_cache(%Ash.Query{} = query) do
    query
    |> before_action()
    |> Ash.Query.after_action(fn query, records -> after_action(query, records) end)
  end

  def before_action(%Ash.Query{} = query) do
    cache_name = ReserveCache.name()

    key = query_filter_to_key(query)
    # take arguments to key as well

    updated_query =
      case Cachex.exists?(cache_name, key) do
        {:ok, true} ->
          {:ok, results} = Cachex.get(cache_name, key)
          Ash.Query.set_result(query, results)

        _ ->
          query
      end

    updated_query
  end

2 Likes