Everything starts with creating an instance of RiakClient
. RiakClient
takes two core arguments, namely a reference to an Akka
ActorSystem
and the location of the Riak node you want to interact with.
If your app is not based on Akka and therefor has no access to an initialized actor system,
you can leave off the ActorSystem
argument and one will be created for you
internally. The location of a riak node can be specified as either an HTTP url pointing
to root of your Riak node or a combination of a hostname and a port number.
It is important to note that internally RiakClient
uses an
Akka extension to ensure there can
be only one instance of RiakClient
per actor system. This means that you can "create"
as many instances of RiakClient
as you want because under the surface they all point at one single
instance.
Before we can fetch and store values, it might be a good idea to talk about how those values are represented.
RiakValue
represents the raw, serialized value stored in Riak and all its associated meta data,
such as its content type, its vclock, its etag, its last modified timestamp its indexes, etc.
Most interactions with Riak involve dealing with instances of RiakValue
in one way or another.
Since dealing with raw, untyped data is usually not what you want when programming in Scala, any
RiakValue
can be transformed into an instance of
RiakMeta[T]
,
which represents the same Riak meta data as RiakValue
but now the raw data has been
deserialized into some type T
. This is accomplished by calling its asMeta[T]
method.
The reverse transformation is just as simple. Just call the toRiakValue
method
on any instance of RiakMeta
.
If you just want access to the deserialized T without any of the Riak meta data, you can call the
as[T]
method on RiakValue
:
This probably all seems pretty abstract to you. For a more integrated example of how to work with
RiakValue
and RiakMeta
, have a look at the Examples
section.
Of course, these transformations between raw serialized data and typed deserialized
data don't happen by themselves. These transformations require compatible instances of the
RiakSerializer[T]
and RiakDeserializer[T]
type classes to be available in implicit scope. Let's have a look at their definitions:
riak-scala-client uses String
as the lowest level
encoding, which means binary formats are not currently supported. This was a design choice
made to keep the API as simple as possible. Please let us know if you would like support
for binary formats!
riak-scala-client comes with out-of-the-box support for (de)serializing
raw Strings (i.e no serialization at all, using text/plain as the content type) and
any case classes with an associated spray-json
RootJsonFormat
(using application/json). These out-of-the-box implementations
will be used by default if you don't create your own serializers (and your classes fit the
criteria described above) See the Examples
section for examples using both builtin and custom serialization.
Just like in Riak, buckets in riak-scala-client are just a way to namespace your keys.
You can get a reference to a particular bucket simply by calling the bucket
method
of RiakClient
, resulting in an instance of
RiakBucket
.
Most functionality of riak-scala-client is exposed as methods on a RiakBucket
,
as you will see below.
As you would expect, fetching data from Riak is very simple indeed. Just call the fetch
method on any bucket with a key fo your choice:
As you can see, the return type of fetch
is Future[Option[RiakValue]]
.
All operations in riak-scala-client that interact with Riak are non-blocking so all of those
operations will result in some type of Future
being produced. The
Examples section has lots of examples of how to interact with Futures.
Obviously not all keys will be bound to a value in Riak so the Future
returned by fetch
wraps an Option[RiakValue]
.
Conflict resolution during fetch and fetching with secondary indexes will be discussed below.
Storing values in Riak is almost as easy as fetching them. The most basic store operation
is one that takes a (String
) key and a RiakValue
and returns
a Future[Unit]
.
This is basically a fire-and-forget operation but you can use the returned Future to handle any
errors that might occur while riak-scala-client communicates with Riak. If you want to
have access to the value you just stored, or more probably to its meta data (i.e. to make sure you
are working a value based on the latest vclock), you can use the storeAndFetch
method,
which will perform a Riak HTTP store operation using the returnbody=true
query parameter.
Working with raw RiakValue
instances is fine when you get them returned from a
fetch
operation but when you want to store data it is often more convenient to work
with your own domain classes. To that end you can also store
any type T for which
there is a RiakSerializer[T]
and a RiakIndexer[T]
in implicit scope.
As long as the value is either a String
, a class associated
with a spray-json RootJsonFormat[T]
, or a class associated with a custom
RiakSerializer[T]
, it will be automatically serialized and stored. The
Examples section has more details about how to use the builtin
serializers or how to define your own ones.
Don't worry about the indexer part for now since there is always a default indexer in scope that will not index anything. See the section on secondary indexes below for more information.
Lastly, you can also store any instance of RiakMeta[T]
directly without having
to convert it to a RiakValue
yourself.
Deleting values from Riak is probably the simplest operation you can perform. Just pass the key
to the delete
method.
As with the fire-and-forget version of store
, you can use the returned Future
to handle any possible errors gracefully.
If you decide to turn on support for siblings for one (or more) of your buckets, which you can do using
the allow_mult
bucket property (also available as allowSiblings
), then any
fetch
or storeAndFetch
can result in a conflict to be resolved.
riak-scala-client allows you to solve these conflicts yourself by specifying
a RiakConflictsResolver
when getting a reference to a bucket.
Conflict resolvers are usually implemented as (case) objects since they are stateless.
To create a custom resolver you will need to extend from the RiakConflictsResolver
trait and implement the resolve
method. RiakConflictsResolver
is
defined as follows:
Your custom conflict resolver will be presented with a Set
of RiakValue
instances
and its job is to produce one RiakValue
instance, which could be one from the set or a new one
created based on some combination of the values in the set.
If the bucket you are working with has sibling support turned on (i.e. allow_mult == true
), you
should always specify a conflict resolver. Failing to do so will activate the default no-op resolver, which will
throw an exception to remind you to specify a real resolver.
The Examples section contains at least one example of a custom resolver and you can find more examples in the riak-scala-client unit tests.
riak-scala-client fully supports Riak secondary indexes. Every RiakValue
has a set of
RiakIndex
instances representing the secondary indexes it should be stored with. There are also a number of extra
versions of the fetch
method for fetching data by either a single index or an index range.
The result of such index fetches is always a Future[List[RiakValue]]
where riak-scala-client
takes care of fetching the individual values based on the keys returned by the HTTP index fetch.
Using a Future[List[RiakValue]]
here is not ideal since the list of values might be very big
and might not even fit into available memory. You also have to wait (in a matter of speaking) until all
the values have been retrieved before you can do anything with them. The next version of riak-scala-client
will very probably use Play Iteratees to reimplement these methods (or to create streaming versions of them).
That being said, let's look at some examples:
riak-scala-client will add the appropriate type suffix (i.e. "_bin" or "_int") to the name you specify for the index so you don't have to. All index names and index values will also be properly URL encoded so your index names and values can contain non-ascii values.
Just like you can create a custom (de)serializer type class for your domain class, you can also
create a custom indexer by providing an implicit implementation of the RiakIndexer[T]
trait, which looks like this:
riak-scala-client provides a default implementation of RiakIndexer[T]
for any
T
that simply creates an empty set of indexes. This implementation is available from
the lowest possible implicit scopes so any custom implementation you make implicitly available will
always override it.
Next to the obvious methods for fetching, storing, and deleting values, RiakBucket
also
exposes methods for getting and setting common bucket properties. Some examples:
See the Scaladocs for
RiakBucket
and
RiakBucketProperties
for the full set of supported bucket properties.