#typebase-lite Typebase lite is a functional ORM and query language for Couchbase lite: free of boilerplate and runtime reflection.
##Table of contents
##What?
Typebase lite is a thin Scala wrapper for the Java and Android versions of Couchbase lite. It provides an automatic mapper between Couchbase lite's data and Scala's case classes and sealed trait hierarchies, free of boilerplate and runtime reflection. Moreover, many convenient functional combinators are also given to create and compose queries in a type-safe and functional manner a la LINQ.
Currently, it supports the following features: Map views (Reduce can be done using reduce
, foldLeft
, and foldRight
on a query), queries and live queries. It works on both Android and the standard JVM. Since it's just a thin wrapper, any unsupported feature can be done directly with Couchbase lite.
It's a work in progress. The api might change without any prior warning. There's much to be improved and you are very welcome to try out, give comments/suggestions, and contribute!
Note: We do not aim to wrap all features of Couchbase lite. Rather, our current goal is to provide a minimal and composable set of functionalities that improve the type-safety of Couchbase lite. Thus, if type-safety is not an issue for a feature X, X should be used directly via Couchbase lite's api.
##Why?
####Why Couchbase lite? The reason why we choose Couchbase lite is its support for NoSql and, more crucially, its powerful sync feature which makes writing apps with offline functionality pleasant.
####Why Typebase lite?
Type-safety is one of the main pain points of using a database. It comes in two places: querying and mapping between the database's and the language's data types. In the case of Couchbase lite, queried results are usually of the form (Java's) Map[String, AnyRef]
, and anything, for eg. an Array
or another nested Map
, might present in the AnyRef
part. This makes querying and persisting data a rather painful and error prone process.
Typebase lite automates all of those boring/error prone parts for you!
##Show me the code ####Quick demo
val query = for {
employee <- tblDb.typeView[Employee].filter(e => (e.age >= 30) && (e.address.city == "New York"))
} yield employee
will create a query of all employees living in New York, and are at least 30 years old.
query.foreach(println(_))
will execute the query and print out the results.
####What is happening behind the scene?
tblDb
is Typebase lite's database object. The code above will loop over all employees and filter out the ones that satisfy our conditions.
####Isn't that slow? How about indexing? Yes! In the above, we already used one, which indexes the types of the documents.
You can create custom indices, even composite indices are supported! In this case we want to create a composite index consisting of 2 keys: city and age:
val cityAgeIndex = tblDb.createIndex[String :: Int :: HNil]("city-age", "1.0", {
case e: Employee => Set(e.address.city :: e.age :: HNil)
case _ => Set()
})
Now, the same query as above, but now use the index
val query2 = cityAgeIndex.sQuery(startKey("New York" :: 30 :: HNil), endKey("New York" :: Last))
####More complex queries? Queries can be composed and reused in a functional manner, and can be mixed and matched seamlessly with Scala's collections as well.
Typebase lite is published on Maven central. To use it, inside the build.sbt
of your project, add the following line
libraryDependencies += "com.shalloui" %% "typebase-lite-java" % "0.1"
if you're using it in the standard JVM, or
libraryDependencies += "com.shalloui" %% "typebase-lite-android" % "0.1"
if you're using it on Android.
It works with both the standard storage option or ForestDb. To use the latter, just add
libraryDependencies += "com.couchbase.lite" % "couchbase-lite-java-forestdb" % "1.3.0"
or
libraryDependencies += "com.couchbase.lite" % "couchbase-lite-android-forestdb" % "1.3.0"
depending on whether you're writing for Android.
Many of the concepts here are related to (and influenced by) the ones in Couchbase lite, since we are, after all, a wrapper of that library. So, it might be instructive to first give a quick glance at their documentation.
The examples mentioned below runs outside of Android and could be found under the tbljavademo
subproject. The data schema is defined in
com.shalloui.tblite.demo.data
and the running example implemented in
com.shalloui.tblite.demo.launch.LaunchDemo
You can jump straight ahead to the code and run it.
The Android demo could be found here: https://github.com/a-reisberg/tbl-android-demo.
For this example, we need the following imports:
import com.couchbase.lite._
import com.shalloui.tblite.TblQuery._
import com.shalloui.tblite._
import shapeless._ // Only needed for composite indices
First, we initialize Couchbase lite as usual:
val manager = new Manager(new JavaContext("data"), Manager.DEFAULT_OPTIONS)
manager.setStorageType("ForestDB")
manager.getDatabase("my-database").delete()
val db = manager.getDatabase("my-database")
The only thing we need to do to initialize Typebase lite is to add the following line
val tblDb = TblDb[Company](db)
First we define the following classes which we want to persist in the database. The trait Company
will be the super type of everything stored in our database. This part is completely independent of Typebase lite.
sealed trait Company
case class Department(name: String, employeeIds: List[String]) extends Company
// _id is the primary key
case class Employee(_id: String, name: String, age: Int, address: Address) extends Company
case class Address(city: String, zip: String)
Let's first insert some random data for Employee
's
And now, we can insert the employees.
// Now add employees to the db. the _id field will be filled in automatically.
val john = tblDb.put(Employee(_, "John Doe", 35, Address("New York", "12345")))
val jamie = tblDb.put(Employee(_, "Jamie Saunders", 25, Address("New York", "13245")))
// ... more employees ...
We finish data initialization by creating and inserting the Department
's.
val saleDeptId = tblDb.put(Department("Sale", List(john._id, jamie._id)))
// ... more department ids ...
To retrieve, for example, the sale department, and the hr department, we just need to call tblDb.getType[Department](saleDeptId, hrId)
, which will return a Seq
of Department
's.
println(tblDb.getType[Department](saleDeptId, hrId))
Note that we could also do
tblDb.get(saleDeptId, hrId)
and everything still works, except that the return type would be Company
instead of the more precise type Department
. Of course, we could then also do a pattern matching, but the code wouldn't be as concise as using getType[T]
.
Typebase lite automatically creates an index based on the type of each entry. This index could be accessed by
tblDb.typeView
Querying all departments, for eg.
val deptQ = tblDb.typeView[Department]
deptQ
is just a description of the query, which only runs when deptQ.foreach(...)
is called
deptQ.foreach(println)
Thus, the query could be reused later, and moreover, it could also be combined with other queries using the provided combinators (more of this later).
For-comprehension also works
for (dept <- deptQ) println(dept)
Now, suppose we want to query all of the following pairs
(Department names, List of employees in that department)
Again, it's a two-step process
// First create the query
val deptEmployeeQ = for {
dept <- deptQ
employeeNames = tblDb.getType[Employee](dept.employeeIds: _*).map(_.name)
} yield (dept.name, employeeNames)
// Then execute the query and print out the results
deptEmployeeQ.foreach(println)
We see here that we seamlessly reused deptQ
from before.
As another example, we want to get everyone who lives in New York, and whose age is >= 30
val cityAndAgeQ = for {
employee <- tblDb.typeView[Employee].filter(e => (e.age >= 30) && (e.address.city == "New York"))
} yield employee
cityAndAgeQ.foreach(println)
The last query in the previous section just loops over all employees and filters out the ones that satisfy our condition. If it's something we run often, we can create a compound index, consisting of a pair
(String, Int)
where String
and Int
are for city names and ages respectively.
// Create the index
val cityAgeIndex = tblDb.createIndex[String :: Int :: HNil]("city-age", "1.0", {
case e: Employee => Set(e.address.city :: e.age :: HNil)
case _ => Set()
})
Now, we can do the same query, but using the index instead:
val cityAgeQ2 = cityAgeIndex.sQuery(startKey("New York" :: 30 :: HNil), endKey("New York" :: Last)).extractType[Employee]
cityAgeQ2.foreach(println)
Under the hood, Index
is created by Couchbase lite's View. More general MapViews can be created via createMapView
.
Since Couchbase lite's Reduce doesn't cache the data, Reduce can just be done by calling the usual combinator reduce
, foldLeft
, and foldRight
on a TblQuery
object.
Now, we want to be notified whenever the result returned by cityAgeQ2
changes (for eg. someone new from New York of age >= 30 starts at our company). A Live query will help with that.
First create the live query:
val liveQ = cityAgeIndex.sLiveQuery(startKey("New York" :: 30 :: HNil), endKey("New York" :: Last)).extractType[Employee]
Next, subscribe (in this case, we just dump the query's results to StdOut)
val subscription = liveQ.subscribe(_.foreach(println))
And then, start the monitoring
liveQ.start()
Suppose someone new enters the db:
tblDb.put(Employee("New Comer", 31, Address("New York", "99999")))
the query will be re-run automatically, and the results will be printed out.
The subscription
returned by liveQ.subscribe
could be used to unsubscribe when we are done:
subscription.dispose()
Finally, we can stop the live query by
liveQ.stop()
##Roadmap Currently, Typebase lite is essentially quite feature complete. Feel free to make a feature request if you feel like something is missing!
For the next version, we plan to add the following functionality:
- Replication