User Profile Sample: Couchbase Lite Query Introduction
Introduction
Couchbase Lite brings powerful querying and Full-Text-Search(FTS) capabilities to the edge.
Its query interface is based on N1QL, Couchbase’s declarative query language, which implements the emerging SQL++ standard.
The query API is designed using the Fluent API Design Pattern, and uses method cascading to read like a Domain Specific Language (DSL). This makes the interface very intuitive and easy to understand.
Couchbase Lite can be used as a standalone embedded database within your mobile app.
Prerequisites
This tutorial assumes familiarity with building Android apps using Java and with the basics of Couchbase Lite.
-
If you are unfamiliar with the basics of Couchbase Lite, it is recommended that you walk through the Standalone tutorial, covering the fundamentals of using Couchbase Lite as a standalone database
-
Android device or emulator running API level 22 or above
-
Android SDK 29+
-
Android Build Tools 29+
-
git (Optional) This is required if you would prefer to pull the source code from GitHub repo.
-
Create a free github account if you don’t already have one
-
git can be downloaded from git-scm.org
-
App Overview
We will be working with a very simple "User Profile" app. If you have walked through the Standalone tutorial, you will quickly realize that this version extends the functionality introduced in that tutorial.
This app does the following:
-
Allows users to log in and create or update his/her user profile information, which was also possible in the Standalone tutorial
-
As part of profile information, users can now select a
University
from a list of possible options.
This list of universities is queried (using the new Query API) from a local prebuilt "University" Couchbase Lite database that is bundled in the app. -
When saved, the user profile information is persisted as a
Document
in a local Couchbase Lite database. So, when the user logs back in again, the profile information is loaded from theDatabase
.
Installation
Clone the query branch of the User Profile Demo
solution from GitHub.
Assuming that you have installed
Git
you can use the following command to do this:
git clone -b query https://github.com/couchbaselabs/userprofile-couchbase-mobile-android.git
Installing Couchbase Lite
This sample project already contains the appropriate additions for downloading, and utilizing the Android Couchbase Lite dependency module. However, in the future, to include Couchbase Lite support within an Android app add the the following within app/build.gradle.
dependencies {
...
implementation 'com.couchbase.lite:couchbase-lite-android-ee:3.0.0'
}
Sample App Architecture
The sample app follows the MVP pattern, separating the internal data model, from a passive view through a presenter that handles the logic of our application and acts as the conduit between the model and the view.
In the Android Studio project, the code is structured by feature. You can select the Android option in the left navigator to view the files by package.
Each package comprises the following:
-
Activity: This is where all the view logic resides.
-
Presenter: This is where all the business logic resides to fetch and persist data to a web service or the embedded Couchbase Lite database.
-
Contract: An interface that the
Presenter
andActivity
implement.
Data Model
Couchbase Lite is a JSON Document Store.
A Document is a logical collection of named fields and values.
The values are any valid JSON types.
In addition to the standard JSON types, Couchbase Lite supports some special types like Date
and Blob
.
While it is not required or enforced, it is a recommended practice to include a "type" property that can serve as a namespace for related.
The User Profile Document
The app deals with a single Document
with a "type" property of "user".
The document ID is of the form "user::<email>".
An example of a document would be
{
"type":"user",
"name":"Jane Doe",
"email":"jame.doe@earth.org",
"address":"101 Main Street",
"image":CBLBlob (image/jpg),
"university":"Missouri State University"
}
UserProfile
For the purpose of this tutorial the "user" Document
is first stored within an Object
of type Map<String, Object>
.
Map<String, Object> profile = new HashMap<>();
profile.put("name", nameInput.getText().toString());
profile.put("email", emailInput.getText().toString());
profile.put("address", addressInput.getText().toString());
profile.put("university", universityText.getText().toString());
byte[] imageViewBytes = getImageViewBytes();
if (imageViewBytes != null) {
profile.put("imageData", new com.couchbase.lite.Blob("image/jpeg", imageViewBytes));
}
The Map<String, Object>
object functions used as a data storage mechanism between the app’s UI and the backing functionality of the Couchbase Lite Document
object.
The University Document
The app comes bundled with a collection of Documents of type "university". Each Document
represents a university.
{
"type":"university","web_pages": [
"http://www.missouristate.edu/"
],
"name": "Missouri State University",
"alpha_two_code": "US",
"state-province": MO,
"domains": [
"missouristate.edu"
],
"country": "United States"
}
The University Record
When "university" Document
is retrieved from the database it is stored within an Object
of type Map<String, Object>
.
Map<String, Object> properties = new HashMap<>(); (1)
properties.put("name", row.getDictionary("universities").getString("name")); (2)
properties.put("country", row.getDictionary("universities").getString("country")); (2)
properties.put("web_pages", row.getDictionary("universities").getArray("web_pages")); (3)
Using a Prebuilt Database
There are several reasons why you may want to bundle your app with a prebuilt database. This would be suited for data that does not change or change that often, so you can avoid the bandwidth and latency involved in fetching/syncing this data from a remote server. This also improves the overall user experience by reducing the start-up time.
In our app, the instance of Couchbase Lite that holds the pre-loaded "university" data is separate from the Couchbase Lite instance that holds "user" data hold the pre-loaded data. A separate Couchbase Lite instance is not required. However, in our case, since there can be many users potentially using the app on a given device, it makes more sense to keep it separate. This is to avoid duplication of pre-loaded data for every user.
Location of the cblite file
The pre-built database is in the form of a cblite
file.
It will be be in your app project bundle
-
In the
universities.zip
file within theAssets
folder.Note: The cblite folder will be extracted from the zip file.
Loading the Prebuilt Database
-
Open the DatabaseManager.java file and locate the
openPrebuiltDatabase()
function. The prebuilt database is common to all users of the app (on the device). So it will be loaded once and shared by all users on the device.public void openPrebuiltDatabase(Context context)
-
First, we create an instance of
DatabaseConfiguration
object and specify the path where the database would be locatedDatabaseConfiguration config = new DatabaseConfiguration(); config.setDirectory(context.getFilesDir().toString());
-
Then we determine if the "universities" database already exists at the specified location. It would not be present if this is the first time we are using the app, in which case, we locate the "universities.cblite" resource in the App’s main bundle and we copy it over to the Database folder.
If the database is already present at the specified Database location, we simply open the database.
if (!dbFile.exists()) { AssetManager assetManager = context.getAssets(); try { File path = new File(context.getFilesDir().toString()); unzip(assetManager.open("universities.zip"), path); universityDatabase = new Database("universities", config); createUniversityDatabaseIndexes(); } catch (IOException e) { e.printStackTrace(); } catch (CouchbaseLiteException e) { e.printStackTrace(); } } else { try { universityDatabase = new Database("universities", config); } catch (CouchbaseLiteException e) { e.printStackTrace(); } }
Indexing the Prebuilt Database
-
Creating indexes for non-FTS based queries is optional. However, in order to speed up queries, you can create indexes on the properties that you would query against. Indexing is handled eagerly.
-
In the DatabaseManager.java file, locate the
createUniversityDatabaseIndexes()
function. We create an index on thename
andlocation
properties of the documents in the university database.private void createUniversityDatabaseIndexes() { try { universityDatabase.createIndex("nameLocationIndex", IndexBuilder.valueIndex(ValueIndexItem.expression(Expression.property("name")), ValueIndexItem.expression(Expression.property("location")))); } catch (CouchbaseLiteException e) { e.printStackTrace(); } }
Closing the Database
When a user logs out, we close the Prebuilt Database along with other user-specific databases
-
In the DatabaseManager.java file, locate the
closePrebuiltDatabase()
function.public void closePrebuiltDatabase()
-
Closing the database is pretty straightforward
userprofileDatabase.close();
Exploring the Query API
The Query API in Couchbase Lite is extensive.
In our app, we will be using the QueryBuilder
API to make a simple pattern matching query using the like
operator.
Fetching University Document
From the "Your Profile" screen, when the user taps on the "University" cell, a search screen is displayed where the user can enter the search criteria (name and optionally, the location) for the university. When the search criteria is entered, the local "universities" Database is queried for the University documents that match the specified search criteria.
-
Open the UniversitiesPresenter.java file and locate the
fetchUniversities
function.public void fetchUniversities(String name) { fetchUniversities(name, null); } public void fetchUniversities(String name, String country)
-
We build the Query using the
QueryBuilder
API that will look for Documents that match the specified criteria.Expression whereQueryExpression = Function.lower(Expression.property("name")).like(Expression.string("%" + name.toLowerCase() + "%")); (1) if (country != null && !country.isEmpty()) { Expression countryQueryExpression = Function.lower(Expression.property("country")).like(Expression.string("%" + country.toLowerCase() + "%")); (2) whereQueryExpression = whereQueryExpression.and(countryQueryExpression); } Query query = QueryBuilder.select(SelectResult.all()) (3) .from(DataSource.database(database)) (4) .where(whereQueryExpression); (5)
1 Build a QueryExpression
that uses thelike
operator to look for the specified "name" string in the "name" property. Notice couple of things here:
(a) The use of wildcard "%" operator to denote that we are looking for the presence of the string anywhere in the "name" property
(b) The use ofFunction.lower()
to convert the search string into lowercase equivalent. This is because thelike
operator does case-sensitive matching, so we must convert the search string and the property value to lowercase equivalents and compare the two.2 If the location criteria was specified in the search, then Build a QueryExpression
that uses thelike
operator to look for the specified "location" string in the "location" property.3 The SelectResult.all()
specifiees that we are interested in all properties in Documents that match the specified criteria4 The DataSource.database(db)
specified the Data Source5 We include the where
clause that is the logical ANDing of the QueryExpression in <1> and <2> -
We run the Query by calling the
execute()
method on the Query that was constructed in the previous steptry { rows = query.execute(); } catch (CouchbaseLiteException e) { e.printStackTrace(); return; } List<Map<String, Object>> data = new ArrayList<>(); Result row; while((row = rows.next()) != null) { Map<String, Object> properties = new HashMap<>(); (1) properties.put("name", row.getDictionary("universities").getString("name")); (2) properties.put("country", row.getDictionary("universities").getString("country")); (2) properties.put("web_pages", row.getDictionary("universities").getArray("web_pages")); (3) data.add(properties); }
1 Create an instance of UniversityRecord (via HashMap
).2 Use specific type getters to fetch property values. The UniversityRecord instance is populated with these property values. 3 Getters also available for array
types. This returns a Couchbase LiteArrayObject
type.