CacheDecorator behaviour (cache_decorator v0.2.0)
View SourceProvides a caching decorator mechanism for Elixir functions.
This module allows you to easily add caching behavior to functions by using
the @cache
and @invalidate
module attributes. It decorates functions to
automatically cache their results and invalidate cache entries.
Usage
Use CacheDecorator
in your module and specify a :cache_module
that
implements the caching backend behaviour.
use CacheDecorator, cache_module: YourCacheModule
Then decorate your functions with:
@cache key: "cache_key_template", on: <pattern or list_of_patterns>
- Caches the result of the function under the given key.
- The
:key
can contain placeholders like{arg_name}
that will be replaced by the string representation of the corresponding function argument. - The
:on
option allows specifying one or multiple patterns to match against the result of the function call; result will be cached only if it matches one of these patterns. - If
:on
is omitted, cache happens for any function call result. - You can provide additional options like
ttl
which will be passed tocache_module.put/4
asopts
@invalidate key: "cache_key_template", on: <pattern or list_of_patterns>
- Invalidates the cache entry for the given key.
- The
:key
can contain placeholders like{arg_name}
that will be replaced by the string representation of the corresponding function argument. - The
:on
option allows specifying one or multiple patterns to match against the result of the function call; cache invalidation only occurs if the result matches one of these patterns. - If
:on
is omitted, cache invalidation happens after every call.
Behaviour callbacks you need to implement in your cache module
Your cache module (provided via :cache_module
option) must implement the
following callbacks:
@callback get(decorator_opts :: Keyword.t(), key :: String.t()) ::
{:ok, nil} | {:ok, term()} | :error
@callback put(decorator_opts :: Keyword.t(), key :: String.t(), value :: term(), opts :: Keyword.t()) ::
:ok
@callback del(decorator_opts :: Keyword.t(), key :: String.t()) ::
:ok
Here:
decorator_opts
are the options passed touse CacheDecorator
in your module, such as the cache module and any other opts.In the
put/4
callback, theopts
argument is the keyword list of options provided in the@cache
decorator, for example thettl
option.
This allows plugging any caching backend you want, by implementing these functions.
Example
Cache provider module implementing CacheDecorator
behaviour (we use :cachex
as example,
but it can be any cache implementation):
defmodule MyCache do
@behaviour CacheDecorator
def get(_decorator_opts, key) do
case Cachex.get(:default, key) do
{:ok, value} ->
{:ok, value}
{:error, _reason} ->
:error
end
end
def put(_decorator_opts, key, value, opts) do
expire = Keyword.get(opts, :ttl)
_ = Cachex.put(:default, key, value, expire: expire)
:ok
end
def del(_decorator_opts, key) do
_ = Cachex.del(:default, key)
:ok
end
end
Module using cache decorators:
defmodule MyModule do
use CacheDecorator, cache_module: MyCache
@cache key: "cache_key_{user_id}"
def get_data(user_id) do
Storage.get_by_user_id(user_id)
end
@invalidate key: "cache_key_{user_id}", on: :ok
def update_data(%{user_id: user_id} = params) do
Storage.update(user_id, params)
end
end
Cache keys can reference function argument names wrapped in {}
that will
be dynamically interpolated from the actual arguments on call.
If the cache is unavailable (e.g., backend error), the original function will be called without caching as fallback.
This decorator uses Elixir's @on_definition
and @before_compile
hooks, and generates override functions that implement caching and
invalidation transparently.
Examples of decorator translation
Here are examples showing how functions decorated with caching decorators would look like without using the decorators.
Using @cache
Decorated function:
use CacheDecorator, cache_module: MyCache
@cache key: "prefix_{arg}"
def get(arg), do: expensive_computation(arg)
Equivalent function without decorator:
def get(arg) do
key = "prefix_#{arg}"
case MyCache.get([], key) do
{:ok, nil} ->
value = expensive_computation(arg)
:ok = MyCache.put([], key, value, key: "prefix_{arg}")
value
{:ok, value} ->
value
:error ->
expensive_computation(arg)
end
end
Using @invalidate
Decorated function:
use CacheDecorator, cache_module: MyCache
@invalidate key: "prefix_{arg}", on: :ok
def update(arg), do: do_something(arg)
Equivalent function without decorator:
def update(arg) do
result = do_something(arg)
case result do
:ok ->
:ok = MyCache.del([], "prefix_#{arg}")
result
_ ->
result
end
end
If the :on
option is omitted, invalidation occurs unconditionally:
@invalidate key: "prefix_{arg}"
def update(arg), do: do_something(arg)
Equivalent without decorator:
def update(arg) do
result = do_something(arg)
:ok = MyCache.del([], "prefix_#{arg}")
result
end
Summary
Types
Callbacks
@callback del(decorator_opts(), key()) :: :ok
@callback get(decorator_opts(), key()) :: {:ok, nil} | {:ok, value()} | :error
@callback put(decorator_opts(), key(), value(), opts()) :: :ok