CouchDB-Scala
This is a purely functional Scala client for CouchDB. The design goals are compositionality, expressiveness, type-safety, and ease of use.
It's based on these awesome libraries: Scalaz, Http4s, uPickle, and Monocle.
Getting started
Add the following dependency to your SBT config:
libraryDependencies += "com.ibm" %% "couchdb-scala" % "0.7.2"
Tutorial
This Scala client tries to stay as close to the native CouchDB API as possible, while adding type-safety and automatic serialization/deserialization of Scala objects to and from JSON using uPickle. The best way to get up to speed with the client is to first obtain a good understanding of how CouchDB works and its native API. Some good resources to learn CouchDB are:
To get started, add the following import to your Scala file (or just start the SBT console with sbt console
, which automatically adds the required imports):
import com.ibm.couchdb._
Then, you need to create a client instance by passing in the IP address or host name of the CouchDB server, port number, and optionally the scheme (which defaults to http
):
val couch = CouchDb("127.0.0.1", 5984)
Through this object, you get access to the following CouchDB API sections:
- Server: server-level operations
- Databases: operations on databases
- Design: operations for creating and managing design documents
- Documents: creating, modifying, and querying documents and attachments
- Query: querying views, shows, and lists
Server API
The server API section provides only 3 operations: getting the server info, which is equivalent to making a GET request to the /
resource of the CouchDB server; generating a UUID; and generating a sequence of UUIDs. For example, to make a server info request using the client instance created above:
couch.server.info.run
The couch.server
property refers to an instance of the Server
class, which represents the server API section. Then, calling the info
method generates a scalaz.concurrent.Task, which describes an action of making a GET request to the server. At this point, an actual request is not yet made. Instead, Task
encapsulates a description of a computation, which can be executed later. This allows us to control side-effects and keep the functions pure. Tim Perrett has written a very nice blog post with more background and documentation on Scalaz's Task
.
The return type of couch.server.info
is Task[Res.ServerInfo]
, which means that when this task is executed, it may return a ServerInfo
object or fail. To execute a Task
, we need to call the run
method, which triggers the actual GET request to server, whose response is then automatically parsed and mapped onto the ServerInfo
case class that contains a few fields describing the server instance like the CouchDB version, etc. Ideally, instead of executing a Task
and causing side-effects in the middle of a program, we should delay the execution as much as possible to keep the core application logic pure. Rather then executing Task
s to obtain the query result, we can perform action on the query results and compose Task
s in a functional way using higher-order functions like map
and flatMap
, or for-comprehensions. We will see more examples of this later. In further code snippets, I will omit calls to the run
method assuming that the point where effectful computations are executed is externalized.
The other operations of the server API can be performed in a similar way. To generate a UUID, you just need to call couch.server.mkUuid
, which returns Task[String]
. To generate n
UUIDs, call couch.server.mkUuids(n)
, which returns Task[Seq[String]]
representing a task of generating a sequence of n
UUIDs. For more usage examples, please refer to ServerSpec.
Databases API
The databases API implements more useful functionality like creating, deleting, and getting information about databases. To create a database:
couch.dbs.create("awesome-database")
The couch.dbs
property refers to an instance of the Databases
class, which represents the databases API section. A call to the create
method returns a Task[Res.Ok]
, which represents a request returning an instance of the Res.Ok
case class if it succeeds, or a failure object if it fails. Failure handling is done using methods on Task
, part of which are covered in Tim Perrett's blog post. In two words the actual result of a Task
execution is Throwable \/ A
, which is either an exception or the desired type A
. In the case or dbs.create
, the desired result is of type Res.Ok
, which is a case class representing a response from the server in case of a succeeded request.
Other methods provided by the databases API are dbs.delete("awesome-database")
to delete a database, dbs.get("awesome-database")
to get information about a database returned as an instance of DbInfo
case class that includes such fields as data size, number of documents in the database, etc. For some examples of using the databases API, please refer to DatabasesSpec.
Design API
While the API sections described earlier operate at the level above databases, the Design, Documents, and Query APIs are applied within the context of a single database. Therefore, to obtain instances of these interfaces, the context needs to be specialized by specifying the name of a database of interest:
val db = couch.db("awesome-database", TypeMapping.empty)
This method call returns an instance of the CouchDbApi
case class representing the context of a single database, through which we can get access to the Design, Documents, and Query APIs. The db
method takes 2 arguments: the database name and an instance of TypeMapping
. We will discuss TypeMapping
later, for now we can just pass an empty mapping using TypeMapping.empty
. Through CouchDbApi
we can obtain an instance of the Design
class representing the Design API section for our database:
db.design
The Design API allows us to create, retrieve, update, delete, and manage attachments to design documents stored in the current database (you can get the name of the database from an instance of CouchDbApi
using db.name
).
Let's take a look at an example of a design document with a single view. First, assume we have a collection of people each corresponding to an object of a case class Person
with a name and age fields:
case class Person(name: String, age: Int)
Let's define a view with just a map function that emits person names as keys and ages as values. To do that, we are going to use a CouchView
case class:
val ageView = CouchView(map =
"""
|function(doc) {
| emit(doc.doc.name, doc.doc.age);
|}
""".stripMargin)
Basically, we define our map function in plain JavaScript and assign it to the map
field of a CouchView
object. This function maps each document to a pair of the person's name as the key and age as the value. Notice, that we need to use doc.doc
to get to the fields of the person object for reasons that will become clear later. To define a view that contains a reduce operation, specify the relevant Javascript function to the reduce
attribute of the CouchView
case class constructor like so:
val totalAgeView = CouchView(map =
"""
|function(doc) {
| emit(doc._id, doc.doc.age);
|}
""".stripMargin,
reduce =
"""
|function(key, values, rereduce) {
| return sum(values);
|}
""".stripMargin)
We can now create an instance of our design document using the defined ageView
and totalAgeView
:
val designDoc = CouchDesign(
name = "test-design",
views = Map("age-view" -> ageView, "total-age-view" -> totalAgeView))
CouchDesign
supports other fields like shows
and lists
, but for this simple example we only specify the design name
and views
as a Map
from view names to CouchView
objects. Proper management of complex design documents is a separate topic (e.g., JavaScript functions can be stored in separate .js
files and loaded dynamically). We can finally proceed to submitting the defined design document to our database:
db.design.create(designDoc)
This method call returns an object of type Task[Res.DocOk]
. The DocOk
case class represents a response from the server to a succeeded request involving creating, modifying, and deleting documents. Compared with Res.Ok
, it includes 2 extra fields: id
(the ID of the created/updated/deleted document) and rev
(the revision of the created/updated/deleted document). In the case of design documents, the ID is composed of the design name prefixed with _design/
. In other words, designDoc
will get the _design/test-design
ID. Each revision is a unique 32-character UUID string. We can now retrieve the design document from the database by name or by ID:
db.design.get("test-design")
db.design.getById("_design/test-design")
Once the returned Task
s are executed, each of these calls returns an instance of CouchDesign
corresponding to our design document with some extra fields, e.g., _id
, _rev
, _attachments
, etc. To update a design document, we must first retrieve it from the database to know the current revision and avoid conflicts, make changes to the content, and submit the updated version. Let's say we want to add another view, which emits ages as keys and names as values assigned to a nameView
variable, then our updated view Map
is:
val updatedViews = Map(
"age-view" -> ageView,
"name-view" -> nameView)
We can now submit the changes to the database as follows:
for {
initial <- db.design.get("test-design")
docOk <- db.design.update(initial.copy(views = updatedViews))
} yield docOk
Here, we use a for-comprehension to chain 2 monadic actions. If both actions succeed, we get a Res.DocOk
object as a result containing the new revision of the design document stored in the _rev
field. The Design API supports a few other operations, to see their usage examples please refer to DesignSpec.
Documents API
The Documents API implements operations for creating, querying, modifying, and deleting documents and their attachments. At this stage, it's time to discuss how Scala objects are represented in CouchDB and what TypeMapping
is used for. One of the design goals of CouchDB-Scala
is to make it as easy as possible to store and retrieve documents by automating the process of serialization and deserialization to and from JSON. This functionality is based on uPickle, which uses macros to automatically generate readers and writers for case classes. However, it also allows implementing custom readers and writers for your domain classes if they are not case classes. For example, these can be Thrift / Scrooge generated entities or your custom classes.
CouchDB automatically adds several fields to every document containing metadata about the document, such as _id
, _rev
, _attachments
, _conflicts
, etc. To take advantage of uPickle's support for case classes, a decision was made to have a case class called CouchDoc[D]
that has all the metadata fields generated by CouchDB and also includes 2 special fields: doc
for storing an instance of your domain class D
, and kind
for storing a string representation of the document type that can be used for filtering in views, shows, and lists (we use kind
instead of type
here, as type
is a reserved keyword in Scala). In other words, if your domain model is represented by a set of case classes, the serialization and deserialization will be handled completely transparently for you. TypeMapping
is used for defining a mapping from you domain model classes to a string representation of the corresponding document type. Continuing the previous example with the Person
case class, we can define a TypeMapping
, for example, as follows:
val typeMapping = TypeMapping(classOf[Person] -> "Person")
Here, we are specifying a mapping from the class name Person
to a document kind as a string. The TypeMapping
factory maps classes to their canonical names to preserve uniqueness. Whenever a document is submitted to the database, the kind
field is automatically populated based on the specified mapping. If the type mapping is not specified (as we did above by using TypeMapping.empty
), the kind
field is ignored. We can now provide the newly defined TypeMapping
to create a fully specified database context:
val db = couch.db("awesome-database", typeMapping)
Similarly to the other API sections, we can use the database context to get an instance of the Documents
class representing the Documents API section:
db.docs
Let's define some data:
val alice = Person("Alice", 25)
val bob = Person("Bob", 30)
val carl = Person("Carl", 20)
We can now store these objects in the database as follows:
db.docs.create(alice)
This method assigns a UUID generated with server.mkUuid
that we've seen above to the document being stored. Another option is to specify our own document ID if it's known to be unique:
db.docs.create(bob, "bob")
As another alternative, we can create multiple documents with auto-generated UUIDs at once using a batch request:
db.docs.createMany(Seq(alice, bob, carl))
We can retrieve a document from the database by ID:
db.docs.get[Person]("bob")
Here, we have to be explicit about the expected object type to allow uPickle to do its magic, that's why we specify the type parameter to the get
method. This method returns Task[CouchDoc[Person]]
, which basically means that we are getting back a task that after executing successfully will give us an instance of CouchDoc[Person]
. This object will contain an instance of Person
in the doc
field equivalent to the original Person("Bob", 30)
.
You can also retrieve a set of documents by IDs using:
db.docs.getMany.includeDocs[Person].withIds(Seq("id1", "id1")).build.query
A call to getMany
returns an instance of GetManyDocumentsQueryBuilder
, which is a class allowing you to build a query in a type-safe way. Under the hood, it makes a request to the /{db}/_all_docs endpoint. As you can see from the linked documentation on this endpoint, it has many optional parameters. The GetManyDocumentsQueryBuilder
class provides a fluent interface for constructing queries to this endpoint. For example, to limit the number of documents to the maximum of 10 and return them in the descending order:
db.docs.getMany.limit(10).descending.includeDocs[Person].withIds(Seq("id1", "id2")).build.query
This creates an instance of Task[CouchDocs[String, CouchDocRev, Person]]
, which looks complicated but just represents a task that returns basically a sequence of documents. The queryIncludeDocs
method serves as a way to complete the query construction process, which also sets the include_docs
option to include the full content of the documents mapped to Person
objects on arrival.
It's also possible to execute a query without including the document content using db.docs.getMany.build.query
, which is equivalent to keeping the include_docs
set to its default false
value. This query will only return metadata on the matching documents. In this case, we don't need to specify the type parameter as no mapping is required since the document content is not retrieved.
To retrieve all documents in the database of a given type without specifying ids, you could use one of the following approaches:
val allPeople1 = db.docs.getMany.byTypeUsingTemporaryView[Person].build.query
The first approach, byTypeUsingTemporaryView[T]
, uses a temporary view under the hood for type based filtering. While convenient for development purposes, it is inefficient and should not be used in production.
For efficiency you should instead use byType[K, V](view_name)
, or the simpler byType[V](view_name)
, which require that you first create a type filtering permanent view, and then pass its name as argument to one of these methods. Because a permanent view is used, these approaches are more efficient and are thus the recommended approach for type based document filtering.
Note in byType[K, V](view_name)
the parameters K
and V
represent the key and value types of the permanent view. The document's kind
attribute must be the first key of such a view, as in the type filter view function example shown below.
function(doc) {
emit([doc.kind, doc._id], doc._id);
}
The above function could then be used as follows:
val allPeople2 = db.docs.getMany.byType[(String, String), String](your_view_name).build.query
In the simpler byType[V](view_name)
, K
is implicitly assumed to be of type Tuple of two strings (String, String)
. Note the document's kind
attribute must be the first key of such a view, as in the type filter view function example defined above.
The above function could then be used as follows:
val allPeople3 = db.docs.getMany.byType[String](your_view_name).build.query
There is a similar query builder for retrieving single documents GetDocumentQueryBuilder
that makes GET requests to the /{db}/{docid} endpoint. This query builder can accessed through db.docs.get
.
There are other operations provided by the Documents API, such as updating documents, deleting documents, adding attachments, retrieving attachments, etc. For more usage examples, please refer to DocumentsSpec.
Query API
The Query API provides an interface for querying views, shows, and lists. Let's say we want to query our age-view
defined earlier. To do that, we first obtain an instance of ViewQueryBuilder
as follows:
val ageView = db.query.view[String, Int]("test-design", "age-view").get
val totalAgeView = db.query.view[String, Int]("test-design", "total-age-view").get
We need to specify 2 type parameters to the view
method representing the types of the key and value emitted by the view. In the case of age-view
and total-age-view
, it's String
for the key (person name) and Int
for the value (person age).
We can now use the ageView
query builder to retrieve all the documents from the view:
ageView.build.query
This method call returns an instance of Task[CouchKeyVals[String, Int]]
. Since we haven't specified the include_docs
option, this query only retrieves a sequence of document IDs, keys, and values emitted by the view's map function. This method makes a call to the /{db}/_design/{ddoc}/_view/{view} endpoint, and the builder supports all the relevant options.
Similarly, to query the total age of Persons in the document using the totalAgeView
builder we can do:
totalAgeView.reduce[Int].build.query
The type parameter T
specified to queryWithReduce[T]
, in this case Int
, is the expected return type of the view's reduce
function.
We can also make more complex queries. Let's say we want to get 10 people starting from the name Bob and include the document content:
ageView.startKey("Bob").limit(10).includeDocs[Person].build.query
This returns an instance of Task[CouchDocs[String, Int, Person]]
, which once executed results in a sequence of objects encapsulating the metadata about the documents (id
, key
, value
, offset
, total_rows
) and the corresponding Person
objects. Please follow the definitions of case classes in Model to fully understand the structure of the returned objects.
It's also possible to only get the documents from a view that match the specified keys. For example, we can use that to get only documents of Alice and Carl:
ageView.withIds(Seq("Alice", "Carl")).build.query
This return an instance of Task[CouchKeyVals[String, Int]]
. For other usage examples of the view Query API, please refer to QueryViewSpec.
The APIs for querying shows and lists are structured similarly to view querying and follow the official CouchDB specification. Please refer to QueryShowSpec and QueryListSpec for more details and examples.
Authentication
At the moment, the client supports only the basic authentication method. To use it, just pass your username and password to the CouchDb
factory:
val couch = CouchDb("127.0.0.1", 6984, https = true, "username", "password")
Please note that enabling HTTPS is recommended to avoid sending your credentials in plain text. The default CouchDB HTTPS port is 6984.
Complete example
Here is a basic example of an application that stores a set of case class instances in a database, retrieves them back, and prints out afterwards:
object Basic extends App {
// Define a simple case class to represent our data model
case class Person(name: String, age: Int)
// Define a type mapping used to transform class names into the doc kind
val typeMapping = TypeMapping(classOf[Person] -> "Person")
// Define some sample data
val alice = Person("Alice", 25)
val bob = Person("Bob", 30)
val carl = Person("Carl", 20)
// Create a CouchDB client instance
val couch = CouchDb("127.0.0.1", 5984)
// Define a database name
val dbName = "couchdb-scala-basic-example"
// Get an instance of the DB API by name and type mapping
val db = couch.db(dbName, typeMapping)
val actions = for {
// Delete the database or ignore the error if it doesn't exist
_ <- couch.dbs.delete(dbName).ignoreError
// Create a new database
_ <- couch.dbs.create(dbName)
// Insert documents into the database
_ <- db.docs.createMany(Seq(alice, bob, carl))
// Retrieve all documents from the database and unserialize to Person
docs <- db.docs.getMany.includeDocs[Person].build.query
} yield docs.getDocsData
// Execute the actions and process the result
actions.attemptRun match {
// In case of an error (left side of Either), print it
case -\/(e) => println(e)
// In case of a success (right side of Either), print each object
case \/-(a) => a.map(println(_))
}
}
You can run this example from the project directory using sbt
:
sbt "run-main com.ibm.couchdb.examples.Basic"
Mailing list
Please feel free to join our mailing list, we welcome all questions and suggestions: https://groups.google.com/forum/#!forum/couchdb-scala
Contributing
We welcome contributions, but request you follow these guidelines. Please raise any bug reports on the project's issue tracker.
In order for us to accept pull-requests, the contributor must first complete a Contributor License Agreement (CLA). This clarifies the intellectual property license granted with any contribution. It is for your protection as a Contributor as well as the protection of IBM and its customers; it does not change your rights to use your own Contributions for any other purpose.
You can download the CLAs here:
If you are an IBMer, please contact us directly as the contribution process is slightly different.
Contributors
- Anton Beloglazov (@beloglazov)
- Ermyas Abebe (@ermyas)
Copyright and license
© Copyright 2015 IBM Corporation, Google Inc. Distributed under the Apache 2.0 license.