View Source ActiveMemory.ActiveRepo (ActiveMemory v0.8.1)
The ActiveRepo
An ActiveRepo manages multiple ActiveMemory.Tables from a single process. It
is the multi-table counterpart to ActiveMemory.Store (which manages a single
table), giving you one supervised entry point and a unified API over many tables.
It is named
ActiveReporather thanReposo it does not collide with an application'sEcto.Repowhile keeping the familiar "repo" terminology.
defmodule MyApp.ActiveRepo do
use ActiveMemory.ActiveRepo,
tables: [
MyApp.People.Person,
{MyApp.Dogs.Dog, seed_file: Path.expand("dog_seeds.exs", __DIR__), before_init: [{:warm, []}]}
]
endAdd the ActiveRepo to your supervision tree like any other process:
children = [MyApp.ActiveRepo]Tables may freely mix :ets and :mnesia adapters; each operation dispatches to
the adapter configured on the given table.
activerepo-api
ActiveRepo API
Every operation an ActiveMemory.Store offers is available here, with the same
behavior; only the arities differ. Reads and withdraw take the table module as
the first argument, while write and delete infer the table from the struct (or
from a changeset's data).
ActiveRepo.all/2Get all records stored in a table, optionally ordered and pagedActiveRepo.count/2Count the records in a table, without reading themActiveRepo.delete/1Delete the record provided, matched in full (see Deleting a record)ActiveRepo.delete_all/1Delete all records stored in a tableActiveRepo.exists?/3Whether any record in a table matches an attributes search ormatchqueryActiveRepo.get/2Get the record with the given primary key, or{:error, :not_found}ActiveRepo.get!/2Likeget/2but raisesActiveMemory.NotFoundErrorActiveRepo.get_by/2Get the single record in a table matching an attributes searchActiveRepo.get_by!/2Likeget_by/2but raisesActiveMemory.NotFoundErrorActiveRepo.one/2Get one record from a table matching either an attributes search ormatchquery. RaisesActiveMemory.MultipleResultsErrorwhen several matchActiveRepo.one!/2Likeone/2but raisesActiveMemory.NotFoundErrorActiveRepo.reload/1Re-read a record by its primary key, inferring the tableActiveRepo.reload!/1Likereload/1but raisesActiveMemory.NotFoundErrorActiveRepo.select/3Get all records from a table matching either an attributes search ormatchquery, optionally ordered and pagedActiveRepo.withdraw/2Atomically get one record from a table matching either an attributes search ormatchquery, delete the record and return it — exactly one concurrent caller wins, making it safe for take-once workloadsActiveRepo.write/1Write a record into its table, from a struct or anEcto.Changeset. An invalid changeset is returned as{:error, changeset}with itsactionset to:insert, exactly likeEcto.Repo.insert/2
An operation for a struct or table that is not part of the ActiveRepo returns
{:error, :unknown_table}.
reading-counting-ordering
Reading, counting, ordering
These behave exactly as they do on a ActiveMemory.Store, which documents them in
full: reading a single record, counting, and ordering with
:order_by/:limit/:offset. get/2 reads by the table's primary key (its first
field), count/2 is O(1) and takes sweep: true on a ttl table, and reload/1
infers its table from the struct as write/1 and delete/1 do.
deleting-a-record
Deleting a record
delete/1 removes an exact record match: the struct you pass is compared
field for field against what is stored. A struct that has diverged from the
stored copy — a stale read, or one modified in memory — removes nothing and
still returns :ok, the same answer given for a record that was never there.
That is the only correct behavior for a :bag table, and on a :set table it
keeps a delete from clobbering a newer version of the record.
When you hold an identifier rather than a record you know is current, use
withdraw/2: it matches on a query, is atomic, and reports whether anything was
removed with {:ok, record} or {:error, :not_found}.
concurrency
Concurrency
Like a Store, an ActiveRepo is a GenServer, but the data functions above are
not routed through that process and are not serialized by it. They run in
the caller's process and delegate straight to each table's adapter, so reads
and writes execute with ETS/Mnesia concurrency — the single GenServer is not a
bottleneck. Only lifecycle and metadata operations (init, state/0,
reload_seeds/1) use the GenServer.
These functions live on the GenServer module purely for organization: the
ActiveRepo is the single place responsible for how the application talks to its
tables, following the Single Responsibility Principle. See the
S.T.O.N.E principles for
the broader design philosophy.
tables-and-per-table-options
Tables and per-table options
Each entry of tables: is either a table module or a {table, opts} tuple. The
supported per-table options mirror the single-table ActiveMemory.Store:
seed_filea path to a seed file auto loaded when the table is first createdbefore_initmethods (defined on theActiveRepo) run during the table's setup
expiry-ttl
Expiry (TTL)
Any table whose ActiveMemory.Table declares a ttl expires its records
automatically: reads never return an expired record, and the ActiveRepo
periodically sweeps expired records from every ttl table it owns to reclaim
memory. The sweep cadence defaults to one minute and can be set with the
sweep_interval option (milliseconds). Tables without a ttl are left untouched,
and the sweep is only scheduled when at least one table uses a ttl.
initial-state
Initial State
Like a Store, an ActiveRepo is a GenServer with state. The default state is:
%{started_at: "date time when first started", tables: [MyApp.People.Person, ...]}Supply a {method, args} tuple to the initial_state keyword to override it; the
method must return {:ok, new_state}.
resilience
Resilience
ETS tables created by an ActiveRepo get the same ActiveMemory.TableHeir
protection as a Store: they survive an ActiveRepo crash and are reclaimed on
restart, and seed files are not re-run on recovery. See ActiveMemory.Store for
the before_init recovery caveat, which applies here as well.