Couchbase is one of the most popular NoSQL databases currently in use. It’s fast and elastic, and it’s a great alternative for relational databases, especially when dealing with large datasets. For mobile devices, we have Couchbase Lite which offers us this NoSQL approach inside the app and allows us to use some great features, like automatic conflict resolution or real-time data synchronization via Sync Gateway.
Couchbase Lite is out there for a while, but recently, with an update to version 2.0, it went through a big redesign. In this article, we’d like to point out some differences between versions 1.4 and 2.0, and help you with migrating your code.
[Note: this article does not cover migrating your data replication code.]
All examples you’ll see in this article are based on the official sample Android Couchbase Lite Todo app.
Internal database format
Couchbase Lite is a document database that stores data in JSON documents, however internally it uses an SQLite database. We will look into this database a few times in this article. The first two tables we’re interested in are docs and revs. The first one contains a list of all documents internal ids, and the ids you gave them:
The second table contains info about all documents current revisions:
Up to version 1.4 Couchbase Lite was using a map-reduce approach, which bases on creating views (which work more or less like SQL indexes). It requires you to create a map (and optional reduce) function, which given a JSON document input emits a number of key-value pairs to be indexed in the view. In code it would look like this:
val view = db.getView("list/listByName")
view.setMap({ document, emitter ->
if (document["type"] == "task-list") {
emitter.emit(document["name"], null)
}
}, "1")
Executing this code will result in inserting a new row in the internal database in views table:
and creating a new table maps_1 which will include all emitted values. In this case:
A new table maps_x will be created for every view.
The first thing that changed in 2.0 version is the format of the internal SQLite database. Now the table we’re interested in is named kv_default.
It contains all the info about the documents, but JSON body is stored in a binary form, so we’re no longer able to read it! The only way to read the data is to use an official cblite tool. More info about the new db schema can be found here.
The important thing is that if in your app you have a database in version 1.4, when you update the library version in your code, your database will be automatically updated to the new format on the first open.
Database creation
After changing your gradle dependency from implementation "com.couchbase.lite:couchbase-lite-android:1.4.*"
to implementation "com.couchbase.lite:couchbase-lite-android:2.0.0"
and compiling the project, your code will light up with dozens of error messages. Let’s try to fix some of them step by step.
First things first. In 1.4 to create and open your database you probably used code like this:
val manager = Manager(AndroidContext(context.applicationContext), Manager.DEFAULT_OPTIONS)
database = manager.getExistingDatabase("dbName")
if (database == null) {
ZipUtils.unzip(context.assets.open("dbName.zip"), context.filesDir)
database = manager.getDatabase("dbName")
}
If you needed to have a preloaded database, you stored it in a zip file in assets and then unzipped it using ZipUtils, which was a utility class from the couchbase-lite library. Now you need to change it to
val config = DatabaseConfiguration(context.applicationContext)
if (!Database.exists("dbName", context.filesDir)) {
ZipUtils.unzip(context.assets.open("dbName.zip"), context.filesDir)
}
database = Database("dbName", config)
and remember that ZipUtils class is no longer included in the library, so you need to write your own (or copy the old implementation from here).
CRUD operations
As in the 2.0 version Couchbase Lite changed data manipulation approach from map-reduce to N1QL-based Query API, the CRUD operations changed a lot. Say goodbye to getView() methods, and say hello to QueryBuilder.
Creating a single document didn’t change that much. From
val docId = username + "." + UUID.randomUUID()
val data = mapOf("type" to "task-list", "name" to title, "owner" to username)
val document = Document(db, docId)
document.putProperties(data)
we get to
val docId = username + "." + UUID.randomUUID()
val data = mapOf("type" to "task-list", "name" to title, "owner" to username)
val mDoc = MutableDocument(docId, data)
db.save(mDoc)
Updating existing document looks pretty much the same. You just need to set an existing document id. To create a MutableDocument
from Document
just call document.toMutable()
.
Reading a single document also looks the same
val document = db.getDocument(mDoc.id)
but to get the properties from the document instead of calling document.properties
you need to call document.toMap()
.
Deleting a single document goes from
document.delete() or document.purge()
to
db.delete(document) or db.purge()
The real fun begins when we need to create a query to the database. Instead of calling db.getView(viewName).createQuery()
, we build our query step by step using QueryBuilder.
Before:
val query = db.getView("list/listByName").createQuery()
query.isDescending = false
query.limit = limit
query.run().forEach { result ->
val doc = result.document /* we can get document or properties */
/* mapping and other stuff */
}
After:
val query = QueryBuilder.select(SelectResult.expression(Meta.id),
SelectResult.all())
.from(DataSource.database(db))
.where(Expression.property("type").equalTo(Expression.string("task-list")))
.orderBy(Ordering.property("name").ascending())
.limit(Expression.intValue(limit))
query.execute().forEach { result ->
val properties = result.toMap() /* we cannot access the document itself */
/* mapping and other stuff */
}
This may seem longer at first sight, but remember that in the first example we omitted creating the view.
The semantics of the query builder is really easy to understand, as it reminds good old SQL SELECT * FROM table_name WHERE id=id_number ORDER BY name ASC LIMIT limit
.
However, there are a few things that need to be pointed out.
In select()
method you’d probably use mostly these three expressions:
• SelectResult.expression(Meta.id)
- which will result in returning document id (key from the database, not the id inside your JSON, if you have one) under the name id.
• SelectResult.all()
- which will result in returning entire JSON document in a form of a Dictionary under the name of your database.
• SelectResult.property("name")
- which will result in returning a property name from your JSON document.
Of course you can build more complex expressions using comparators like Expression.greaterThan()
or functions like Function.count()
etc.
Where
expressions can be chained using operators like Expression.and()
or Expression.or()
. You can also chain QueryBuilder with join()
or groupBy()
components.
For example, the above query will result in a structure like this:
[
{
"id": "todo.123",
"todo": {
"name": "Groceries",
"owner": "todo",
"type": "task-list"
}
}
]
One more confusing thing is how Couchbase Lite handles numbers now. If you want to maintain a Double field, it can be serialized (and as a result deserialized) to different data types. If the decimal part is zero, it will be deserialized as Long
. If a number fits in the Float
type, it will be deserialized asFloat
. Therefore, if you need a Double
, it would be best to read it from the document map as Number
and then call toDouble()
.
You also should note that there are no more options to run multiple operations in the transaction. Instead, you can use db.inBatch()
- although it works a bit differently.
The last thing you need to take into account is that for today (May 2019) Couchbase Lite versions 2.x do not support devices with architecture x86_64. That means they won’t work on these devices, but it also means that you will not be able to write a unit test using eg. Robolectric to test your db code. Instrumentation test, however, will work fine.
Now, hopefully, you will be able to migrate your database and CRUD operations from Couchbase Lite 1.4 to 2.0. For other examples and features you can look in the official documentation and dive into some official tutorials.
We hope you find this article useful, and as always: if you have any comments, feel free to let us know!
About the Author
Aleksandra Krzemień has profound experience in building all sorts of mobile apps, including those connected with external devices. She is a natural mentor and loves to share her knowledge. Throughout her career, she has been actively engaged in many mentoring programs and IT-related organizations, such as Women in Technology or Google Developers Group. She likes to spend her free time singing and playing board games.