This is a simple/basic in-memory Key/Value Store written in Elixir and using ExShards as default adapter.
Again, KVX is a simple library, most of the work is done by ExShards, and its typical use case might be as a Cache.
Add kvx
to your Mix dependencies:
defp deps do
[{:kvx, "~> 0.1"}]
end
In an existing or new module:
defmodule MyTestMod do
use KVX.Bucket
end
Let's try it out, compile your project and start an interactive console:
$ mix deps.get
$ mix compile
$ iex -S mix
Now let's play with kvx
:
> MyTestMod.new(:mybucket)
:mybucket
> MyTestMod.set(:mybucket, :fruit, "banana")
:mybucket
> MyTestMod.mset(:mybucket, male_users: 200, female_users: 150)
:mybucket
> MyTestMod.get(:mybucket, :female_users)
150
> MyTestMod.mget(:mybucket, [:male_users, :female_users])
[200, 150]
> MyTestMod.find_all(:mybucket)
[fruit: "banana", male_users: 200, female_users: 150]
> MyTestMod.delete(:mybucket, :male_users)
:mybucket
> MyTestMod.get(:mybucket, :male_users)
nil
> MyTestMod.flush!(:mybucket)
:mybucket
> MyTestMod.find_all(:mybucket)
[]
Most of the configuration that goes into the config
is specific to the adapter.
But there are some common/shared options such as: :adapter
and :ttl
. E.g.:
config :kvx,
adapter: KVX.Bucket.ExShards,
ttl: 1 # the ttl in seconds
Now, in case of the adapter KVX.Bucket.ExShards
, it has some extra options
like module
. E.g.:
config :kvx,
adapter: KVX.Bucket.ExShards,
ttl: 1, # the ttl in seconds
module: ExShards.Local
Besides, you can define bucket options in the config:
config :kvx,
adapter: KVX.Bucket.ExShards,
ttl: 43200, # the ttl in seconds
module: ExShards,
buckets: [
mybucket1: [
n_shards: 4
],
mybucket2: [
n_shards: 8
]
]
In case of ExShards adapter, run-time options when calling new/2
function, are
the same as ExShards.new/2
. E.g.:
MyModule.new(:mybucket, [n_shards: 4])
NOTE: For more information check KVX.Bucket.ExShards.
$ mix test
$ mix coveralls
NOTE: For more coverage options check excoveralls.
As we mentioned before, one of the most typical use case might be
use KVX as a Cache. Now, let's suppose you're working with
Ecto, and you want to be
able to cache data when you call Ecto.Repo.get/3
, and on other hand,
be able to handle eviction, remove/update cached data when they
change or mutate – typically when you call Ecto.Repo.insert/2
,
Ecto.Repo.update/2
, etc.
To do so, let's implement our own CacheableRepo
to encapsulate
data access and caching logic. First let's create our bucket and
the Ecto.Repo
in two separated modules:
defmodule MyApp.Bucket do
use KVX.Bucket
end
defmodule MyApp.Repo do
use Ecto.Repo, otp_app: :myapp
end
Now, let's code our CacheableRepo
, re-implementing some Ecto.Repo
functions but adding caching. It is as simple as this:
defmodule MyApp.CacheableRepo do
alias MyApp.Repo
alias MyApp.Bucket
require Logger
def get(queryable, id, opts \\ []) do
get(&Repo.get/3, queryable, id, opts)
end
def get!(queryable, id, opts \\ []) do
get(&Repo.get!/3, queryable, id, opts)
end
def get_by(queryable, clauses, opts \\ []) do
get(&Repo.get_by/3, queryable, clauses, opts)
end
def get_by!(queryable, clauses, opts \\ []) do
get(&Repo.get_by!/3, queryable, clauses, opts)
end
defp get(fun, queryable, key, opts) do
b = bucket(queryable)
case Bucket.get(b, key) do
nil ->
value = fun.(queryable, key, opts)
if value != nil do
Logger.debug "CACHING <get>: #{inspect key} => #{inspect value}"
Bucket.set(b, key, value)
end
value
value ->
Logger.debug "CACHED <get>: #{inspect key} => #{inspect value}"
value
end
end
def insert(struct, opts \\ []) do
case Repo.insert(struct, opts) do
{:ok, schema} = rs ->
schema
|> bucket
|> Bucket.delete(schema.id)
rs
error ->
error
end
end
def insert!(struct, opts \\ []) do
rs = Repo.insert!(struct, opts)
rs
|> bucket
|> Bucket.delete(rs.id)
rs
end
def update(struct, opts \\ []) do
case Repo.update(struct, opts) do
{:ok, schema} = rs ->
schema
|> bucket
|> Bucket.set(schema.id, schema)
rs
error ->
error
end
end
def update!(struct, opts \\ []) do
rs = Repo.update!(struct, opts)
rs
|> bucket
|> Bucket.set(rs.id, rs)
rs
end
def delete(struct, opts \\ []) do
case Repo.delete(struct, opts) do
{:ok, schema} = rs ->
schema
|> bucket
|> Bucket.delete(schema.id)
rs
error ->
error
end
end
def delete!(struct, opts \\ []) do
rs = Repo.delete!(struct, opts)
rs
|> bucket
|> Bucket.delete(rs.id)
rs
end
# function to resolve what bucket depending on the given schema
defp bucket(%{__struct__: struct}), do: Bucket.new(struct)
defp bucket(struct) when is_atom(struct), do: Bucket.new(struct)
defp bucket(_), do: Bucket.new(:default)
end
Now that we have our CacheableRepo
, it can be used instead of Ecto.Repo
(since it is a wrapper on top of it, but it adds caching) for data you
consider can be cached, for example, you can use it from your
Phoenix Controllers – in case you're using Phoenix.
Copyright (c) 2016 Carlos Andres Bolaños R.A.
KVX source code is licensed under the MIT License.