View Source Indexed.Managed (Indexed v0.0.1)
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.: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.: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
.:subscribe
and:unsubscribe
- 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
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
.: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_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.
Preload associations recursively.
Invoke Indexed.put/3
with a wrapped state for convenience.
Link to this section Types
@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()
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.
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(), 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
.: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_wrapped(), atom(), id(), preloads() | true) :: 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.
Invoke Indexed.get_index/4
with a wrapped state for convenience.
@spec get_records(state_or_wrapped(), atom(), prefilter() | nil, order_hint() | nil) :: [record()] | nil
Invoke Indexed.get_records/4
with a wrapped state for convenience.
@spec get_uniques_list(state_or_wrapped(), 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_wrapped(), 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 managed_stat(state()) :: keyword()
@spec paginate(state_or_wrapped(), atom(), keyword()) :: Paginator.Page.t() | nil
@spec preload(map() | [map()] | nil, state_or_wrapped(), preloads()) :: [map()] | map() | nil
Preload associations recursively.
@spec put(state_or_wrapped(), atom(), record()) :: :ok
Invoke Indexed.put/3
with a wrapped state for convenience.