SchemaCache provides four core operations: read, create, update, and delete. Each operation is Ecto-aware. SchemaCache inspects query results to find Ecto structs and automatically maintains a reverse index from schemas to cache keys.
read/4 checks the cache first. On a miss, it invokes your callback, caches the result, and records which schemas appear in it for later eviction.
{:ok, user} =
SchemaCache.read("find_user", %{id: 5}, :timer.minutes(15), fn ->
MyApp.Users.find(%{id: 5})
end)The first call executes the callback and caches the result. Subsequent calls with the same key and params return the cached value directly.
users =
SchemaCache.read("users", %{active: true}, :timer.minutes(5), fn ->
MyApp.Users.all(%{active: true})
end)The cache key is derived from the first two arguments: key and params. Params are deterministically serialized, so %{a: 1, b: 2} and %{b: 2, a: 1} produce the same cache key.
After inserting a new record, create/1 evicts all cached collections for that schema type. This ensures the next read includes the new record.
{:ok, new_user} =
SchemaCache.create(fn ->
%User{}
|> User.changeset(%{name: "Bob", email: "bob@example.com"})
|> Repo.insert()
end)On success, SchemaCache looks up all cache keys associated with the User schema type (collection references) and evicts them.
By default, update/2 evicts all cache keys that reference the mutated schema instance. The next read will fetch fresh data.
{:ok, updated_user} =
SchemaCache.update(fn ->
MyApp.Users.update_user(user, %{name: "Alice"})
end)With :write_through, SchemaCache updates cached values in place instead of evicting them. This avoids cache misses after writes.
{:ok, updated_user} =
SchemaCache.update(
fn -> MyApp.Users.update_user(user, %{name: "Alice"}) end,
strategy: :write_through
)Write-through handles both singular cached values and collections. For collections, SchemaCache finds the specific item by primary key and replaces it in the list.
After a deletion, delete/1 evicts all cache keys that reference the deleted schema instance.
{:ok, deleted_user} =
SchemaCache.delete(fn -> Repo.delete(user) end)flush/2 evicts all cache entries associated with a specific schema instance. Use this when you need to invalidate cache entries for a schema outside of a create/update/delete flow.
# Evict all cached queries that include User#5
SchemaCache.flush(user, :timer.minutes(15))The TTL argument is passed to the adapter when re-storing reference sets (if applicable).
Cache keys are composed from two parts: a name and params map. The name identifies the domain (e.g. "users", "jobs"), and the params are deterministically serialized into the key.
# Singular
SchemaCache.read("users", %{id: 5}, ttl, fn -> ... end)
# Collection
SchemaCache.read("users", %{active: true}, ttl, fn -> ... end)Write-through automatically distinguishes collections from singular values based on the cached data type. No naming convention required.
Params maps are serialized deterministically using sorted keys and Jason encoding. This means %{a: 1, b: 2} and %{b: 2, a: 1} produce the same cache key.
- Understand how invalidation works under the hood.
- Learn how to write a custom adapter.
- Integrate with ElixirCache for a production-ready backend.