Skip to content

Commit e8d27b2

Browse files
committed
[#245] Allow :key and :query together in cache_evict
1 parent 4ecbc0e commit e8d27b2

File tree

5 files changed

+205
-39
lines changed

5 files changed

+205
-39
lines changed

CHANGELOG.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,13 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
1919
`:query` option for bulk eviction based on adapter-specific queries.
2020
For example: `@decorate cache_evict(query: my_query)`. The query can be
2121
provided directly or as a function that returns the query at runtime.
22-
When present, the `:query` option overrides the `:key` option.
22+
Additionally, both `:query` and `:key` options can be used together to
23+
evict specific entries and entries matching a query pattern in a single
24+
operation. When both are provided, query-based eviction executes first,
25+
followed by key-based eviction.
2326
For more information, see:
2427
[#243](https://github.com/elixir-nebulex/nebulex/issues/243).
28+
[#245](https://github.com/elixir-nebulex/nebulex/issues/245).
2529
- [Nebulex.Caching.Decorator] Add support for evicting external references in
2630
`cache_evict` decorator. For more information, see:
2731
[#244](https://github.com/elixir-nebulex/nebulex/issues/244)

lib/nebulex/caching/decorators.ex

Lines changed: 121 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1170,6 +1170,17 @@ if Code.ensure_loaded?(Decorator.Define) do
11701170
def delete_all do
11711171
# your logic (maybe write/delete data to the SoR)
11721172
end
1173+
1174+
@decorate cache_evict(key: user_id, query: &__MODULE__.query_for_user_sessions/1)
1175+
def logout_user(user_id) do
1176+
# Evicts both the user entry and all their session entries
1177+
# your logic (maybe write/delete data to the SoR)
1178+
end
1179+
1180+
def query_for_user_sessions(%{args: [user_id]}) do
1181+
# Return a query that matches user sessions
1182+
# (implementation depends on your adapter)
1183+
end
11731184
end
11741185
11751186
## Eviction with a query
@@ -1178,6 +1189,14 @@ if Code.ensure_loaded?(Decorator.Define) do
11781189
returns a query at runtime) to evict multiple cache entries based on
11791190
specific criteria. The query must be supported by the cache adapter.
11801191
1192+
> #### Query syntax varies by adapter {: .warning}
1193+
>
1194+
> The query syntax and format depend on the cache adapter being used.
1195+
> The examples in this section assume you are using the
1196+
> `Nebulex.Adapters.Local` adapter, which uses ETS match specifications.
1197+
> If you're using a different adapter (e.g., Redis, Partitioned, etc.),
1198+
> consult that adapter's documentation for the appropriate query format.
1199+
11811200
To explain how this works, let's use an example. Imagine you have a function
11821201
`delete_objects_by_tag` that receives a `tag` as an argument and deletes
11831202
all records matching the given tag from the system of record (SoR). You want
@@ -1188,12 +1207,12 @@ if Code.ensure_loaded?(Decorator.Define) do
11881207
receives the decorator context and returns the required query to evict all
11891208
cached entries matching the given tag.
11901209
1191-
@decorate cache_evict(query: &__MODULE__.query_for_tag/1)
1210+
@decorate cache_evict(query: &query_for_tag/1)
11921211
def delete_objects_by_tag(tag) do
11931212
# your logic to delete data from the SoR
11941213
end
11951214
1196-
def query_for_tag(%{args: [tag]} = _context) do
1215+
defp query_for_tag(%{args: [tag]} = _context) do
11971216
# Assuming we are using the `Nebulex.Adapters.Local` adapter and the
11981217
# cached entry value is a map with a `tag` field, the match spec
11991218
# would look like this:
@@ -1212,12 +1231,12 @@ if Code.ensure_loaded?(Decorator.Define) do
12121231
12131232
If you need to evict all cached entries associated with a specific user:
12141233
1215-
@decorate cache_evict(query: &__MODULE__.query_for_user/1)
1234+
@decorate cache_evict(query: &query_for_user/1)
12161235
def delete_user_data(user_id) do
12171236
# your logic to delete user data from the SoR
12181237
end
12191238
1220-
def query_for_user(%{args: [user_id]} = _context) do
1239+
defp query_for_user(%{args: [user_id]} = _context) do
12211240
[
12221241
{
12231242
{:entry, :"$1", %{user_id: :"$2"}, :_, :_},
@@ -1231,12 +1250,12 @@ if Code.ensure_loaded?(Decorator.Define) do
12311250
12321251
You can build more complex queries that match multiple conditions:
12331252
1234-
@decorate cache_evict(query: &__MODULE__.query_for_category_and_status/1)
1253+
@decorate cache_evict(query: &query_for_category_and_status/1)
12351254
def delete_products(category, status) do
12361255
# your logic to delete products from the SoR
12371256
end
12381257
1239-
def query_for_category_and_status(%{args: [category, status]} = _context) do
1258+
defp query_for_category_and_status(%{args: [category, status]}) do
12401259
[
12411260
{
12421261
{:entry, :"$1", %{category: :"$2", status: :"$3"}, :_, :_},
@@ -1266,6 +1285,60 @@ if Code.ensure_loaded?(Decorator.Define) do
12661285
However, using a function is recommended for better readability and
12671286
maintainability, especially when the query depends on function arguments.
12681287
1288+
### Combining `:key` and `:query`
1289+
1290+
You can use both `:key` and `:query` options together to evict both specific
1291+
entries and entries matching a query pattern. When both options are provided,
1292+
the decorator executes the eviction in the following order:
1293+
1294+
1. **Query-based eviction** - First, entries matching the query are evicted.
1295+
2. **Key-based eviction** - Then, the specific key(s) are evicted.
1296+
1297+
This is useful when you need to evict a primary entry along with related
1298+
entries. For example, when logging out a user, you might want to evict the
1299+
user's cache entry and all their active session entries (the examples below
1300+
assume you are using the `Nebulex.Adapters.Local` adapter):
1301+
1302+
@decorate cache_evict(key: user_id, query: &query_for_user_sessions/1)
1303+
def logout_user(user_id) do
1304+
# Evicts the user entry and all session entries for this user
1305+
UserSessions.delete_all_for_user(user_id)
1306+
end
1307+
1308+
defp query_for_user_sessions(%{args: [user_id]} = _context) do
1309+
# Return a query that matches all session entries for the given user
1310+
[
1311+
{
1312+
{:entry, :"$1", %{user_id: :"$2", type: "session"}, :_, :_},
1313+
[{:"=:=", :"$2", user_id}],
1314+
[true]
1315+
}
1316+
]
1317+
end
1318+
1319+
Another common use case is evicting multiple specific keys along with a
1320+
query:
1321+
1322+
@decorate cache_evict(
1323+
key: {:in, [category.id, category.slug]},
1324+
query: &query_for_category_products/1
1325+
)
1326+
def delete_category(category) do
1327+
# Evicts both category cache entries (by id and slug) and all
1328+
# product entries within that category
1329+
Products.delete_all_for_category(category.id)
1330+
end
1331+
1332+
defp query_for_category_products(%{args: [category]} = _context) do
1333+
[
1334+
{
1335+
{:entry, :"$1", %{category_id: :"$2"}, :_, :_},
1336+
[{:"=:=", :"$2", category.id}],
1337+
[true]
1338+
}
1339+
]
1340+
end
1341+
12691342
### Best Practices
12701343
12711344
- **Keep query logic in separate functions** for reusability and cleaner
@@ -1276,6 +1349,8 @@ if Code.ensure_loaded?(Decorator.Define) do
12761349
maintainability.
12771350
- **Consider performance implications** when querying large datasets, as
12781351
some queries may require full cache scans.
1352+
- **Use both `:key` and `:query` when evicting hierarchical data** where
1353+
you need to remove both a parent entry and its related child entries.
12791354
12801355
## Eviction of external references
12811356
@@ -1290,14 +1365,16 @@ if Code.ensure_loaded?(Decorator.Define) do
12901365
12911366
[ext-refs]: #cacheable/3-external-referenced-keys
12921367
1293-
@decorate cache_evict(key: {:in, [user.id, keyref(user.email, cache: AnotherCache)]})
1368+
@decorate cache_evict(
1369+
key: {:in, [user.id, keyref(user.email, cache: ExtCache)]}
1370+
)
12941371
def delete_user(user) do
12951372
# your logic to delete data from the SoR
12961373
end
12971374
1298-
Assuming there is a reference under the key `user.email` in `AnotherCache`,
1299-
the decorator will remove the cached value under the key `user.id` and the
1300-
reference under the key `user.email` from `AnotherCache`.
1375+
Assuming there is a reference under the key `user.email` in `ExtCache`,
1376+
the decorator will remove the cached value under the key `user.id` and
1377+
the reference under the key `user.email` from `ExtCache`.
13011378
"""
13021379
@doc group: "Decorator API"
13031380
def cache_evict(attrs \\ [], block, context) do
@@ -1607,9 +1684,10 @@ if Code.ensure_loaded?(Decorator.Define) do
16071684
all_entries? = get_boolean(attrs, :all_entries)
16081685

16091686
key =
1610-
case Keyword.fetch(attrs, :query) do
1611-
{:ok, q} -> {:"$nbx_query", q}
1612-
:error -> keygen
1687+
case {Keyword.fetch(attrs, :query), Keyword.fetch(attrs, :key)} do
1688+
{{:ok, q}, {:ok, _k}} -> quote(do: {:"$nbx_query", unquote(q), unquote(keygen)})
1689+
{{:ok, q}, :error} -> quote(do: {:"$nbx_query", unquote(q)})
1690+
_else -> keygen
16131691
end
16141692

16151693
quote do
@@ -1800,6 +1878,12 @@ if Code.ensure_loaded?(Decorator.Define) do
18001878
run_cmd(cache, :delete_all, [[q]], on_error)
18011879
end
18021880

1881+
defp do_evict(false, cache, {:query, q, k}, on_error) do
1882+
_ = run_cmd(cache, :delete_all, [[{:query, q}]], on_error)
1883+
1884+
do_evict(false, cache, k, on_error)
1885+
end
1886+
18031887
defp do_evict(false, cache, key, on_error) do
18041888
run_cmd(cache, :delete, [key, []], on_error)
18051889
end
@@ -1922,10 +2006,30 @@ if Code.ensure_loaded?(Decorator.Define) do
19222006
@spec eval_key(any(), context()) :: any()
19232007
def eval_key(key, ctx)
19242008

1925-
def eval_key(key, ctx) when is_function(key, 1), do: key.(ctx)
1926-
def eval_key(key, _ctx) when is_function(key, 0), do: key.()
1927-
def eval_key({:"$nbx_query", q}, ctx), do: {:query, eval_key(q, ctx)}
1928-
def eval_key(key, _ctx), do: key
2009+
# cache_evict: only a query is provided
2010+
def eval_key({:"$nbx_query", q}, ctx) do
2011+
{:query, eval_key(q, ctx)}
2012+
end
2013+
2014+
# cache_evict: a query and a key are provided
2015+
def eval_key({:"$nbx_query", q, k}, ctx) do
2016+
{:query, eval_key(q, ctx), eval_key(k, ctx)}
2017+
end
2018+
2019+
# The key is a function that expects the context
2020+
def eval_key(key, ctx) when is_function(key, 1) do
2021+
key.(ctx)
2022+
end
2023+
2024+
# The key is a function that expects no arguments
2025+
def eval_key(key, _ctx) when is_function(key, 0) do
2026+
key.()
2027+
end
2028+
2029+
# The key is a term
2030+
def eval_key(key, _ctx) do
2031+
key
2032+
end
19292033

19302034
@doc """
19312035
Convenience function for running a cache command.

lib/nebulex/caching/options.ex

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -234,9 +234,16 @@ defmodule Nebulex.Caching.Options do
234234
type_doc: "`t:query/0`",
235235
required: false,
236236
doc: """
237-
The query to use for evicting cache entries. When present, this option
238-
overrides the `:key` option and allows you to evict multiple entries
239-
based on specific criteria.
237+
The query to use for evicting cache entries based on specific criteria.
238+
This option allows you to evict multiple entries that match the query.
239+
240+
When both `:query` and `:key` are provided, the decorator will:
241+
1. First execute the query-based eviction to remove matching entries.
242+
2. Then execute the key-based eviction to remove the specified key(s).
243+
244+
This is useful when you need to evict both a specific entry and related
245+
entries. For example, when deleting a user, you might want to evict both
246+
the user's cache entry and all their session entries.
240247
241248
The `:query` option accepts the following values:
242249

mix.lock

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"benchee_html": {:hex, :benchee_html, "1.0.1", "1e247c0886c3fdb0d3f4b184b653a8d6fb96e4ad0d0389267fe4f36968772e24", [:mix], [{:benchee, ">= 0.99.0 and < 2.0.0", [hex: :benchee, repo: "hexpm", optional: false]}, {:benchee_json, "~> 1.0", [hex: :benchee_json, repo: "hexpm", optional: false]}], "hexpm", "b00a181af7152431901e08f3fc9f7197ed43ff50421a8347b0c80bf45d5b3fef"},
44
"benchee_json": {:hex, :benchee_json, "1.0.0", "cc661f4454d5995c08fe10dd1f2f72f229c8f0fb1c96f6b327a8c8fc96a91fe5", [:mix], [{:benchee, ">= 0.99.0 and < 2.0.0", [hex: :benchee, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "da05d813f9123505f870344d68fb7c86a4f0f9074df7d7b7e2bb011a63ec231c"},
55
"bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"},
6-
"credo": {:hex, :credo, "1.7.12", "9e3c20463de4b5f3f23721527fcaf16722ec815e70ff6c60b86412c695d426c1", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "8493d45c656c5427d9c729235b99d498bd133421f3e0a683e5c1b561471291e5"},
6+
"credo": {:hex, :credo, "1.7.13", "126a0697df6b7b71cd18c81bc92335297839a806b6f62b61d417500d1070ff4e", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "47641e6d2bbff1e241e87695b29f617f1a8f912adea34296fb10ecc3d7e9e84f"},
77
"decimal": {:hex, :decimal, "2.3.0", "3ad6255aa77b4a3c4f818171b12d237500e63525c2fd056699967a3e7ea20f62", [:mix], [], "hexpm", "a4d66355cb29cb47c3cf30e71329e58361cfcb37c34235ef3bf1d7bf3773aeac"},
88
"decorator": {:hex, :decorator, "1.4.0", "a57ac32c823ea7e4e67f5af56412d12b33274661bb7640ec7fc882f8d23ac419", [:mix], [], "hexpm", "0a07cedd9083da875c7418dea95b78361197cf2bf3211d743f6f7ce39656597f"},
99
"deep_merge": {:hex, :deep_merge, "1.0.0", "b4aa1a0d1acac393bdf38b2291af38cb1d4a52806cf7a4906f718e1feb5ee961", [:mix], [], "hexpm", "ce708e5f094b9cd4e8f2be4f00d2f4250c4095be93f8cd6d018c753894885430"},
@@ -23,7 +23,7 @@
2323
"mimic": {:hex, :mimic, "2.1.1", "29008b71c842b652b065d6f9a24e05d84a2fac7181c34627e1ef5229659702e1", [:mix], [{:ham, "~> 0.3", [hex: :ham, repo: "hexpm", optional: false]}], "hexpm", "a3c330c8840feb29ab43b2375ac023073b936429e5320dd5ca1c95a7322a0da7"},
2424
"nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"},
2525
"nimble_parsec": {:hex, :nimble_parsec, "1.4.2", "8efba0122db06df95bfaa78f791344a89352ba04baedd3849593bfce4d0dc1c6", [:mix], [], "hexpm", "4b21398942dda052b403bbe1da991ccd03a053668d147d53fb8c4e0efe09c973"},
26-
"sobelow": {:hex, :sobelow, "0.14.0", "dd82aae8f72503f924fe9dd97ffe4ca694d2f17ec463dcfd365987c9752af6ee", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "7ecf91e298acfd9b24f5d761f19e8f6e6ac585b9387fb6301023f1f2cd5eed5f"},
26+
"sobelow": {:hex, :sobelow, "0.14.1", "2f81e8632f15574cba2402bcddff5497b413c01e6f094bc0ab94e83c2f74db81", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "8fac9a2bd90fdc4b15d6fca6e1608efb7f7c600fa75800813b794ee9364c87f2"},
2727
"statistex": {:hex, :statistex, "1.1.0", "7fec1eb2f580a0d2c1a05ed27396a084ab064a40cfc84246dbfb0c72a5c761e5", [:mix], [], "hexpm", "f5950ea26ad43246ba2cce54324ac394a4e7408fdcf98b8e230f503a0cba9cf5"},
2828
"stream_data": {:hex, :stream_data, "1.2.0", "58dd3f9e88afe27dc38bef26fce0c84a9e7a96772b2925c7b32cd2435697a52b", [:mix], [], "hexpm", "eb5c546ee3466920314643edf68943a5b14b32d1da9fe01698dc92b73f89a9ed"},
2929
"telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"},

0 commit comments

Comments
 (0)