thiagomajesk
Hi everyone, I was toying around with macros today and I reached a stagnation point. After trying multiple approaches I started to think that what I’m doing might not be possible, so I’m hoping for some guidance and/ or alternatives.
I’m trying to implement a module that will help me define some counter caches in a table. Here’s the general idea… I have a schema called reactions that stores various kinds of interactions a user might provide for a post:
schema "reactions" do
field :feeling, Ecto.Enum, values: [:like, :dislike]
# embeds_one :counter_caches, Cache
end
| post_id | user_id | feeling |
|---|---|---|
| 1 | 1 | like |
| 1 | 2 | dislike |
| 1 | 3 | like |
| 2 | 1 | like |
I expect that for those enum values, the following fields would be generated in the schema: feeling_like_count and feeling_dislike_count. Here’s what I came up with:
defmodule CounterCache do
import Ecto.Query
defmacro __using__(_opts) do
quote do
import CounterCache
Module.register_attribute(__MODULE__, :counter_cache_fields, accumulate: true)
end
end
defmacro counter_cache_field(field, opts \\ []) do
{group, opts} = Keyword.pop(opts, :group)
{suffix, _opts} = Keyword.pop(opts, :suffix, "count")
quote do
name =
"#{unquote(group)}_#{unquote(field)}_#{unquote(suffix)}"
|> String.trim_leading("_")
|> String.to_atom()
Module.put_attribute(__MODULE__, :counter_cache_fields, {unquote(group), name})
Ecto.Schema.field(name, :integer, default: 0)
end
end
defmacro counter_cache_field_enum(module, field) do
quote do
values = Ecto.Enum.values(unquote(module), unquote(field))
Enum.each(values, &counter_cache_field(&1, group: unquote(field)))
end
end
end
So, the part that I’m stuck at is generating and exposing the query that retrieves the counter cache fields. I expect the query to be something like this:
select
count(1) filter (where feeling = 'like') as likes
count(1) filter (where feeling = 'dislike') as dislikes
from reactions
| post_id | likes | dislikes |
|---|---|---|
| 1 | 2 | 1 |
| 2 | 1 | 0 |
Here’s what I’ve managed to do so far with this ancillary function:
def __query__(module, fields) do
quote bind_quoted: [module: module, fields: fields] do
Enum.reduce(fields, from(module), fn
{nil, field}, query ->
select(query, [m], filter(count(1), not is_nil(field(m, ^field))))
{field, value}, query ->
select(query, [m], filter(count(1), not is_nil(field(m, ^field) and field(m, ^field) == ^value)))
end)
end
end
I was hoping to be able to call a function where I’d pass the source module (where the query will fetch the information) and receive a query that I can use later to update the embed that holds the cached values in the posts table.
Repo.all(Post.counter_cache_query(Reactions))
#=> [
#=> %{post_id: 1, likes: 2, dislikes: 1},
#=> %{post_id: 2, likes: 1, dislikes: 0}
#=> ]
I had various problems trying to implement this function. I started defining it inside the __using__ macro, and had @counter_cache_fields be empty by the time I tried to generate the query. Also, tried to call the attribute outside the definition and received an error telling me that it cannot be invoked outside of the module.
So, I’m certainly missing something here, I also remembered that Ecto does something similar: ecto/lib/ecto/schema.ex at b69d1085cfd491a859f1be36463afcf4838e4891 · elixir-ecto/ecto · GitHub with the @changeset_fields attribute; so I’m not sure exactly what’s the problem. Is what I’m trying to achieve even possible?
Trending in Questions
Other Trending Topics
Categories:
Sub Categories:
Forums
Popular Tags
- #ecto
- #liveview
- #troubleshooting
- #learning-elixir
- #library
- #deployment
- #erlang
- #testing
- #genserver
- #mix
- #absinthe
- #remote-other
- #otp
- #plug
- #how-to-question
- #macros
- #postgres
- #elixirconf
- #channels
- #exunit
- #discussion
- #code-sync
- #podcasts
- #javascript
- #onsite
- #dialyzer
- #docker
- #authentication
- #umbrella
- #full-time-contract
- #podcasts-by-brainlid
- #ecto-query
- #elixirconf-us
- #ai
- #blog-post
- #elixir-ls
- #phoenix_html
- #iex
- #graphql
- #genstage
- #websockets
- #supervisor
- #advent-of-code
- #distillery
- #processes
- #api
- #forms
- #metaprogramming
- #hex
- #security











Showing Posts 1 to 10- Show Best Posts
- Show All (oldest first)
- Show All (newest first)
eksperimental
Not only outside the module, neither inside after the module has been compiled.
That is why you need a helper like Ecto does in the example defining
__changeset__/0Btw, where is the function that uses the stored values in
@counter_cache_fields?ityonemo
If you register the attribute with
persist: trueyou can get at those attributes after compilation using<Module>.__info__(:attributes)eksperimental
thank you @ityonemo TIL,
I guess I was assuming all attributes were acceded with
@, but I can see that was a wrong assumption.thiagomajesk
Hi @eksperimental, I’m not sure I understood what you mean, but the attribute
@counter_cache_fieldscould be passed to__query__/2as the fields param, for instance. I purposefully didn’t include the function because I didn’t know where exactly to put it, but if you care to leave an example I’d appreciate that.Interesting, I read the docs but it wasn’t immediately clear what this option did tbh. I’ll test that trick but I’d like to understand first if that’s really necessary (since Ecto doesn’t use this, I figure perhaps there’s another way).
eksperimental
Please share a gist with your code.
thiagomajesk
The code is in the post, that’s how far I got with it… The
CounterCachemodule is the one I intend to use inside schemas to expose the query function I mentioned (which is where we’re at).eksperimental
how do you call
__query__(module, fields)If you can share the module where you call
use?eksperimental
Just by reviewing your code (I haven’t run any code yet), i can tell a few things.:
:ok. So the second clause of yourcounter_cache_fieldmacro will always return:ok. You probably are looking forforand make sure it is valid the desired AST what you return.def __query__(module, fields) dois a function and you are returning a quoted expression. Are you sure about this? I don’t know how you are calling this. Usually you create these helpers and call them within the macro you are building.I fail to see how you read the stored
counter_cache_fieldsattributes.That is why I am asking you for the code
eksperimental
My advice is: Create a module that stores and read the attribute and does something similar to what you want to do, but without the Ecto layer.
Once you managed to properly register attributes and access them, port that code to interact with Ecto.
thiagomajesk
Hey @eksperimental, let me try to improve on the latest comment then…
I’m not calling it yet, I just left the example of the API I want to consume to make the use case a little clearer. It should be something like this:
Post.counter_cache_query(Reactions), wherecounter_cache_query/1is a function that is defined byCounterCache(perhaps) which is thenuse-d by a hypotheticalPostmodule… If you care to take a look at my previous examples, you’ll see that I mentioned trying to define this function on the__using__macro only to find that the attribute was actually empty.What you mentioned makes complete sense, but if you run the code I think it works (and now I’m curious about it as well, I’d have to make some more tests to confirm).
Like I mentioned previously, this is just “pseudo-code”, to make the spec clearer. I was defining
counter_cache_queryand using this function as a helper in my tests (hence the quoted expression).This is exactly the part I’m seeking help to accomplish
. What I’ve tried before was something like this if I recall properly (tried so many different approaches I won’t remember everything right now):
I think there’s a gap in my macro knowledge that I’m failing to transmit properly, and I believe that even though this makes complete sense to you, I’m not sure what you mean by that. However, I can tell you this: If you test the code, you’ll see that registering the module attribute and defining the Ecto fields in the schema works properly (more or less), so the difficulty is actually accessing the
@counter_cache_fieldsmodule attribute. A simple example to visualize it would be something like this (except the function should be injected inside the module that uses it):