Caching in Phoenix/Elixir with Cachex

GET IT? 😅

A quick and dumb-down explanation of how to set up cachex in phoenix/elixir and an optional cache table/service management for awesomeness 😊 . No more chitchat on why caching is important blah blah blah, let’s go straight to coding.

Cachex Installation:

Add cachex to your list of dependencies:mix.exs

def deps do
[{:cachex, "~> 3.3"}]
end

Ensure cachex is started before your application:mix.exs

def application do
[applications: [:cachex]]
end

Starting your cache:

In your application.ex

def start(_type, _args) do
import Supervisor.Spec
children = [
MyApp.Cache
]
end

Requirements:
- must be of type :supervisor , not a :worker
- must have a unique id

defmodule MyApp.Cache do
@moduledoc """
Cache
"""
@cache_table :my_app_cache def child_spec(_init_arg) do
%{
id: @cache_table,
type: :supervisor,
start:
{Cachex, :start_link,
[
@cache_table,
[
warmers: [
warmer(module: MyApp.CacheWarmer, state: "")
]
]
]}
}
end
end

Setting up cache warmer:

defmodule MyApp.CacheWarmer do
@moduledoc """
Cache Warmer
"""

use Cachex.Warmer
alias MyApp.CacheModule

def interval, do: :timer.minutes(60)
def execute(_args) do
with {:ok, data} <- CacheModule.get_data_somewhere() do
{:ok, data, [ttl: :timer.minutes(60)]} end
end
end

interval : Returns the interval this warmer will execute on. (Basically when you want the warmer to rerun)

This must be an integer representing a count of milliseconds to wait before the next execution of the warmer. Anything else will cause either invalidation errors on cache startup, or crashes at runtime.

execute : Executes actions to warm a cache instance on interval.

This can either return values to set in the cache or an atom :ignore to signal that there's nothing to be set at this point in time.

ttl : Time to live, expiration for an entry in a cache.

This is a millisecond value (if set) representing the time a cache entry has left to live in a cache. It can return nil if the entry does not have a set expiration.

Handling cache on one table (optional):

If you want all your services to share just one cache table, you should parse your data before returning it to the warmer.

defmodule MyApp.CacheModule do
@moduledoc """
Getting Cache Data
"""
alias MyApp.Repo
alias MyApp.Schemas.{
User,
Movie
}
def get_data_somewhere do
with users <- get_data(User, :user),
movies <- get_data(Movie, :movie),
data <- users ++ movies do
{:ok, data}
end
end
defp get_data(struct, type) do
struct
|> Repo.all()
|> parse_cache(type)
end
defp parse_cache(data, type) do
data
|> Enum.map(fn f ->
{{type, f.id}, f}
end)
end
end

In the parse_cachemethod, you want to structure the data like this {type, f.id}, f , you’ll see its use later on in the example.

type: is the cache type, in our example, the :user or :movie

f.id : is the cache key

f : is the actual cache data

Examples (if you set up one cache table):

Since we set our application to only have one cache table for all our services/data, using cachex functions will be a little different, but I’m sure you can get it easily

In cachex documentation:

Cachex.get(:my_cache, "key")

In our app:

Cachex.get(:my_cache, {type, "key"})

Example:

Cachex.get(:my_cache, {:user, user.id}){:ok, %User{...}}

In cachex documentation:

Cachex.put(:my_cache, "key", "value")

In our app:

Cachex.put(:my_cache, {type, "key"}, "value")

Example:

Cachex.put(:my_cache, {:user, user.id}, user){ :ok, true }

And that is it, a quite short blog, but I hope you get the idea, you can have a look at cachex documentation for more details here: https://hexdocs.pm/cachex/getting-started.html, Thanks!

Happy Coding!!

I drink(☕️) and I code things — www.alvinrapada.com