Skip to content

Instantly share code, notes, and snippets.

@thiagomajesk
Created February 17, 2022 23:59
Show Gist options
  • Save thiagomajesk/0d5f5432cccf44128a1f13180cffcb5d to your computer and use it in GitHub Desktop.
Save thiagomajesk/0d5f5432cccf44128a1f13180cffcb5d to your computer and use it in GitHub Desktop.
Elixir's Ecto Counter Cache Manager
defmodule CounterCache do
@moduledoc """
This module provides a way of generating embedded schemas that contain the definition of cached fields.
A module is automatically created for each and contains the definition values passed in the list.
By accessing the generated module, those fields can be queried from the schema that contains them.
# Usage
- Add `use CounterCache` to your module.
- Invoke `counter_cache_field` inside your schema definition:
```
schema "posts" do
has_many :reactions, Reaction
counter_cache_field :votes_cache, Reaction, votes: [:like, :dislike]
end
```
The function `counter_cache_field/3` accepts the name of the field which will be turned into a `embeds_one` call,
a queryable which is the source of the columns that should be counted/ cached, and a list definition of matching fields
that existing in the specified queryable. For instance, `Reaction` should have a field named `:votes` that accept the
values `:like` and `:dislike`.
## Example
It's possible to invoke the underlyning module and retrieve the values for those fields.
This is possible because the generated query evaluates each value against the provided field.
So the definition: `votes: [:like, :dislike]` generates something like:
```
SELECT
count(1) FILTER (WHERE votes = 'like'),
count(1) FILTER (WHERE votes = 'dislike')
FROM "reactions"
```
Which is then merged into a map that you can use to update the `VotesCache` embed field.
Post.VotesCache.query() |> Repo.one!()
#=> %{like: 1, dislike: 0}
"""
import Ecto.Query
defmacro __using__(_opts) do
quote do
import CounterCache
@counter_cache_fields []
@before_compile CounterCache
end
end
defmacro counter_cache_field(name, queryable, values) do
quote do
definition = {unquote(name), unquote(queryable), unquote(values)}
@counter_cache_fields [definition | @counter_cache_fields]
module_name = CounterCache.__module_name__(unquote(name), __MODULE__)
embeds_one unquote(name), module_name
end
end
defmacro __before_compile__(env) do
quote do
for {name, queryable, values} <- @counter_cache_fields do
module_name = CounterCache.__module_name__(name, unquote(env.module))
CounterCache.__module__(module_name, queryable, values)
end
end
end
@doc false
def __module_name__(name, namespace) do
name
|> Atom.to_string()
|> Macro.camelize()
|> String.to_atom()
|> then(&Module.concat(namespace, &1))
end
@doc false
def __module__(module_name, queryable, values) do
defmodule module_name do
use Ecto.Schema
@counter_cache_queryable queryable
@counter_cache_values values
def query() do
CounterCache.__query__(@counter_cache_queryable, @counter_cache_values)
end
@primary_key false
embedded_schema do
for {_, values} <- values, field <- values do
field(field, :integer, default: 0)
end
end
end
end
@doc false
def __query__(queryable, filters) do
query = from q in queryable, select: %{}
Enum.reduce(filters, query, fn {field, values}, query ->
values = Enum.map(values, &to_string/1)
Enum.reduce(values, query, fn value, query ->
select_merge(query, [q], %{^value => filter(count(1), field(q, ^field) == ^value)})
end)
end)
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment