View Source Indexed.Managed (Indexed v0.3.4)
Assists a GenServer in managing in-memory caches.
By annotating the entities to be managed, manage/5
can handle updating the
cache for the given record and its associated records. (If associations are
not preloaded, they will be automatically fetched.) In addition, entites with
:subscribe
and :unsubscribe
functions defined will be automatically
subscribed to and unusbscribed from as the first reference appears and the
last one is dropped.
example
Example
This module owns and is responsible for affecting changes on the Car with id 1. It subscribes to updates to Person records as they may be updated elsewhere.
defmodule MyApp.CarManager do
use GenServer
use Indexed.Managed, repo: MyApp.Repo
alias MyApp.{Car, Person, Repo}
managed :cars, Car, children: [:passengers], manage_path: :passengers
managed :people, Person,
subscribe: &MyApp.subscribe_to_person/1,
unsubscribe: &MyApp.unsubscribe_from_person/1
@impl GenServer
def init(_), do: {:ok, warm(:cars, Repo.get(Car, 1))}
@impl GenServer
def handle_call(:get, _from state) do
{:reply, get(state, :cars, 1)}
end
def handle_call({:update, params}, _from, state) do
case state |> get(:cars, 1) |> Car.changeset(params) |> Repo.update() do
{:ok, new_car} = ok -> {:reply, ok, manage(state, :cars, :update, new_car)}
{:error, _} = err -> {:reply, err, state}
end
end
@impl GenServer
def handle_info({MyApp, [:person, :update], person}, state) do
{:noreply, manage(state, :people, :update, person)}
end
end
managed-macro
Managed Macro
For each managed entity, the name (eg. :cars
) and module (eg. MyApp.Car
)
must be specified. If needed, a keyword list of options should follow.
children
: Keyword list with association fields as keys andassoc_spec/0
s as vals. This is used when recursing inmanage/5
as well as when resolving. If an undeclared association is resolved,Repo.get/2
will be used as a fallback. Will automatically include fields given inmanage_path
.fields
: List of fields which should be sortable (ascending and descending). SeeIndexed.Entity
.id_key
: Specifies how to find the id for a record. It can be an atom field name to access, a function, or a tuple in the form{module, function_name}
. In the latter two cases, the record will be passed in. Default:id
.manage_path
: Default associations to traverse formanage/5
. Also, this is the preload argument whentrue
is used.prefilters
- List of atoms indicating which fields should be prefiltered on. This means that separate indexes will be managed for each unique value for each of these fields, across all records of this entity type. Each two-element tuple has the field name atom and a keyword list of options.query_fn
: Optional function which takes a queryable and returns a queryable. This allows for extra query logic to be added such as populating virtual fields. Invoked bymanage/5
when the association is needed.:subscribe
andunsubscribe
: Functions which take a record's ID and manage the subscription. These must both be declared or neither.
tips
Tips
If you want to import Ecto.Query
, you'll find that its preload/3
conflicts
with Managed. Since Managed will use the repo as a fallback, you can exclude
it this way.
defmodule MyModule do
use Indexed.Managed
import Ecto.Query, except: [preload: 2, preload: 3]
end
Link to this section Summary
Types
An association spec defines an association to another entity. It is used to build the preload function among other things.
Assoc spec as provided in the managed declaration. See assoc_spec/0
.
A map of field names to assoc specs.
Used to explain the parent entity when processing its has_many relationship.
Either {:top, name} where name is the top-level entity name OR
nil
for a parent with a :one association OR a tuple with
Preload option as passed to functions such as get/4.
true
means to follow the default provided by the entity's :manage_path
.
Allows for a table namespace to be given when no more is needed.
For convenience, state is also accepted within a wrapping map.
children
: Map with assoc field name keysassoc_spec_opt/0
values. When this entity is managed, all children will also be managed and so on, recursively.fields
: Used to build the index. SeeManaged.Entity.t/0
.id_key
: Used to get a record id. SeeManaged.Entity.t/0
.lookups
: Seet:Managed.Entity.t/0
. Use these with "get_by" functions.query
: Optional function which takes a queryable and returns a queryable. This allows for extra query logic to be added such as populating virtual fields. Invoked bymanage/5
when the association is needed.manage_path
: Default associations to traverse formanage/5
.module
: The struct module which will be used for the records.name
: Atom name of the managed entity.prefilters
: Used to build the index. SeeManaged.Entity.t/0
.subscribe
: 1-arity function which subscribes to changes by id.tracked
: True if another entity has a :one assoc to this. Internal.unsubscribe
: 1-arity function which unsubscribes to changes by id.
Functions
Loads initial data into index.
Invoke Indexed.drop/3
with a wrapped state for convenience.
Invoke Indexed.get/3
. State may be wrapped in a map under :managed
key.
Invoke Indexed.get_index/4
with a wrapped state for convenience.
Invoke Indexed.get_lookup/3
with a wrapped state for convenience.
Invoke Indexed.get_records/4
with a wrapped state for convenience.
Invoke Indexed.get_view/3
with a wrapped state for convenience.
Add, remove or update one or more managed records.
Define a managed entity.
List name
records using the Paginator interface.
Preload associations recursively.
Create ETS tables, init fresh state, without loading data.
Invoke Indexed.put/3
with a wrapped state for convenience.
Link to this section Types
@type add_or_rm() :: :add | :rm
@type assoc_spec() :: {:one, entity_name :: atom(), id_key :: atom()} | {:many, entity_name :: atom(), pf_key :: atom() | nil, order_hint()} | {:repo, assoc_field :: atom(), managed :: t()}
An association spec defines an association to another entity. It is used to build the preload function among other things.
{:one, entity_name, id_key}
- Preload function should get a record ofentity_name
with id matching the id found underid_key
of the record.{:many, entity_name, pf_key, order_hint}
- Preload function should useIndexed.get_records/4
. Ifpf_key
is not null, it will be replaced with{pfkey, id}
whereid
is the record's id.{:repo, key, managed}
- Preload function should useRepo.get/2
with the assoc's module and the id in the foreign key field forkey
in the record. This is the default when a child/assoc_spec isn't defined for an assoc.
@type assoc_spec_opt() :: atom() | assoc_spec() | {:many, entity_name :: atom()} | {:many, entity_name :: atom(), pf_key :: atom() | nil}
Assoc spec as provided in the managed declaration. See assoc_spec/0
.
This is always normalized to assoc_spec/0
at compile time.
Missing pieces are filled via Ecto.Schema
reflection.
@type children() :: %{required(atom()) => assoc_spec()}
A map of field names to assoc specs.
@type data_opt() :: Indexed.Actions.Warm.data_opt()
@type id() :: Indexed.id()
@type order_hint() :: Indexed.order_hint()
Used to explain the parent entity when processing its has_many relationship.
Either {:top, name} where name is the top-level entity name OR
nil
for a parent with a :one association OR a tuple with
- Parent entity name.
- ID of the parent.
- Field name which would have the list of :many children if loaded.
@type prefilter() :: Indexed.prefilter()
@type preloads_option() :: preloads() | true
Preload option as passed to functions such as get/4.
true
means to follow the default provided by the entity's :manage_path
.
@type record() :: Indexed.record()
@type state() :: Indexed.Managed.State.t()
@type state_or_module() :: state_or_wrapped() | module()
Allows for a table namespace to be given when no more is needed.
For convenience, state is also accepted within a wrapping map.
@type t() :: %Indexed.Managed{ children: children(), fields: [atom() | Indexed.Entity.field()], id_key: id_key(), lookups: [Indexed.Entity.field_name()], manage_path: path(), module: module(), name: atom(), prefilters: [atom() | keyword()] | nil, query: (Ecto.Queryable.t() -> Ecto.Queryable.t()) | nil, subscribe: (Ecto.UUID.t() -> :ok | {:error, any()}) | nil, tracked: boolean(), unsubscribe: (Ecto.UUID.t() -> :ok | {:error, any()}) | nil }
children
: Map with assoc field name keysassoc_spec_opt/0
values. When this entity is managed, all children will also be managed and so on, recursively.fields
: Used to build the index. SeeManaged.Entity.t/0
.id_key
: Used to get a record id. SeeManaged.Entity.t/0
.lookups
: Seet:Managed.Entity.t/0
. Use these with "get_by" functions.query
: Optional function which takes a queryable and returns a queryable. This allows for extra query logic to be added such as populating virtual fields. Invoked bymanage/5
when the association is needed.manage_path
: Default associations to traverse formanage/5
.module
: The struct module which will be used for the records.name
: Atom name of the managed entity.prefilters
: Used to build the index. SeeManaged.Entity.t/0
.subscribe
: 1-arity function which subscribes to changes by id.tracked
: True if another entity has a :one assoc to this. Internal.unsubscribe
: 1-arity function which unsubscribes to changes by id.
Link to this section Functions
@spec create_view(state_or_wrapped(), atom(), Indexed.View.fingerprint(), keyword()) :: {:ok, Indexed.View.t()} | :error
Loads initial data into index.
@spec drop(state_or_wrapped(), atom(), id()) :: :ok | :error
Invoke Indexed.drop/3
with a wrapped state for convenience.
@spec get(state_or_module(), atom(), id(), preloads_option()) :: any()
Invoke Indexed.get/3
. State may be wrapped in a map under :managed
key.
If preloads
is true
, use the entity's default path.
@spec get_by(state_or_module(), atom(), atom(), any(), preloads_option()) :: [ record() ]
@spec get_ids_by(state_or_module(), atom(), atom(), any()) :: [record()]
Invoke Indexed.get_index/4
with a wrapped state for convenience.
@spec get_lookup(state_or_module(), atom(), atom()) :: Indexed.lookup() | nil
Invoke Indexed.get_lookup/3
with a wrapped state for convenience.
get_records(som, name, prefilter \\ nil, order_hint \\ nil, preloads \\ nil)
View Source@spec get_records( state_or_module(), atom(), prefilter(), order_hint() | nil, preloads() ) :: [record()]
Invoke Indexed.get_records/4
with a wrapped state for convenience.
@spec get_uniques_list(state_or_module(), atom(), prefilter(), atom()) :: list() | nil
Invoke Indexed.get_uniques_list/4
.
@spec get_uniques_map(state_or_wrapped(), atom(), prefilter(), atom()) :: Indexed.UniquesBundle.counts_map() | nil
Invoke Indexed.get_uniques_map/4
.
@spec get_view(state_or_module(), atom(), Indexed.View.fingerprint()) :: Indexed.View.t() | nil
Invoke Indexed.get_view/3
with a wrapped state for convenience.
@spec manage( state_or_wrapped(), managed_or_name(), :insert | :update | :delete | id() | record_or_list(), id() | record_or_list(), path() ) :: state_or_wrapped()
Add, remove or update one or more managed records.
The entity name
atom should be declared as managed
.
Arguments 3 and 4 can take one of the following forms:
:insert
and the new record: The given record and associations are added to the cache.:update
and the newly updated record or its ID: The given record and associations are updated in the cache. Raises if we don't hold the record.:upsert
and the newly updated record: The given record and associations are updated in the cache. If we don't hold the record, insert.:delete
and the record or ID to remove from cache. Raises if we don't hold the record.- If the original and new records are already known, they may also be supplied directly.
Records and their associations are added, removed or updated in the cache by ID.
path
is formatted the same as Ecto's preload option and it specifies which
fields and how deeply to traverse when updating the in-memory cache.
If path
is not supplied, the entity's :manage_path
will be used.
(Supply []
to override this and avoid managing associations.)
Define a managed entity.
@spec paginate(state_or_module(), atom(), keyword()) :: Paginator.Page.t() | nil
List name
records using the Paginator interface.
Preload associations recursively.
@spec prewarm(state_or_wrapped(), module()) :: state_or_wrapped()
Create ETS tables, init fresh state, without loading data.
@spec put(state_or_wrapped(), atom(), record()) :: :ok
Invoke Indexed.put/3
with a wrapped state for convenience.