@@ -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.
0 commit comments