Caching in Phoenix/Elixir with Cachex
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
Setup child_spec for your cache:
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: "")
]
]
]}
}
endend
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_cache
method, 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
Retrieves an entry from a cache
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{...}}
Placing an entry in a cache
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!
If you find this helpful, I’d appreciate a coffee 😊
Happy Coding!!