Created
February 17, 2022 23:59
-
-
Save thiagomajesk/0d5f5432cccf44128a1f13180cffcb5d to your computer and use it in GitHub Desktop.
Elixir's Ecto Counter Cache Manager
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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