Quickstart in Couchbase with C++
Quickstart app to build a REST API using Couchbase Capella in C++. Discover how to program interactions with Couchbase via the Data, Query, and Search services.
After you have navigated through signing up to Capella, if C++ is entered as your chosen language, you will be pointed to a clonable quickstart app on GitHub. If you were not, you can still find it here.
Often, the first step developers take after creating their database is to create a REST API that can perform Create, Read, Update, and Delete (CRUD) operations for that database. The quickstart project is designed to teach you and give you a starter project (in C++) to generate such a REST API. After you have loaded the travel-sample bucket in your database, you can run this application which is a REST API with Swagger documentation so that you can learn:
-
How to create, read, update, and delete documents using Key-Value operations. KV operations are unique to Couchbase and provide super fast (under millisecond) operations.
-
How to write simple parametrized SQL++ queries using the built-in travel-sample bucket.
This documentation — and a number of other useful developer tutorials — can be found on the Couchbase Developer Portal.
Prerequisites
To run this prebuilt project, you will need:
-
A Couchbase Capella cluster with the travel-sample bucket loaded.
To run this tutorial using a self-managed Couchbase cluster, refer to the Running Self-Managed Couchbase Cluster section.
-
The Travel Sample Bucket is pre-loaded in Capella Free Tier. If you need to load it manually, see the sample data impost page.
-
CMake 3.9 or higher installed
App Setup
We will walk through the different steps required to get the application running:
-
Cloning the Repo
$ git clone https://github.com/couchbase-examples/cxx-quickstart.git -
Navigate to the Project Directory
$ cd cxx-quickstart
Setup Database Configuration
To learn more about connecting to your Capella cluster, follow the instructions. Specifically, you need to do the following:
-
Create the database credentials to access the travel-sample bucket (Read and Write) used in the application.
-
Allow access to the Cluster from the IP on which the application is running.
All configuration for communication with the database is read from the environment variables.
We have provided a convenience feature in this quickstart to set the required environment variables using the shell script set_env_vars.sh.
Change the values of the following lines:
export DB_CONN_STR=<connection_string>
export DB_USERNAME=<username>
export DB_PASSWORD=<password>
export BUCKET_NAME=<bucket_name>
export SCOPE_NAME=<inventory>
export COL_NAME=<collection_name>
Note: The connection string expects the couchbases:// or couchbase:// part.
Run the command:
source set_env_vars.sh
This will set the environment variables for that session.
Install Dependencies and Build
This project makes use of CMake and CPM to install dependencies.
$ mkdir build
$ cd build
$ cmake ..
$ cmake --build .
This will download and install all of the dependencies required for the project to be built, and it will build the executable required to run the application.
Running The Application
|
Running from a Development Machine
Couchbase — including Capella — is designed to run in a LAN-like environment. For development, connecting to a remote cloud instance from a local laptop instead of an application server in the same region, may require you to adjust some timeouts. For more information, see the constrained network environments section of the docs. |
Directly on Your Local Machine
At this point, we have installed the dependencies, loaded the travel-sample data, and configured the application with the credentials.
The application is now ready and you can run it by executing the following command from the build directory:
$ cmake --build .
$ ./cxx_quickstart
Verifying the Application
Once you run the executable, your terminal should fill up with the results of the executed statements written in the main function of the main.cpp and should look something like this:
image::cli_output.png
Code Review
To begin this tutorial, clone the repo and open it up in the IDE of your choice. Now you can explore how to interact with Couchbase Server using the C++ SDK.
We have separated out the SDK code and the main function.
The db.h and db.cpp contain the declaration and the implementation of utility functions we will use to parse environment variables and create a connection to the cluster.
operations.h and operations.cpp contain all the functions that perform operations on the database.
Both db.cpp and operations.cpp are combined to make a static library.
The tests are similarly separated out in the tests folder which utilize the library created earlier.
The main.cpp is the executable which is also built by linking the library and contains code that demonstrates the usage of the functions we defined earlier to interact with the database.
Connecting to the Cluster
In db.h, we include the required header files to work with C++ SDK in order to implement the functions required to initialize the database.
In the db.cpp we implement the functions that help us connect to the database.
We begin by implementing a few utility functions that will help us later.
The parseEnvironmentVariables serves as a utility to get the values set for a list of environment variables.
This enables us to get the connection parameters and credentials set by running source set_env_vars.sh.
Following this, checkScopeAndColExists and checkSearchEnabled are implemented to check for the existence of a scope and collection of a given name and to verify if the Search Service is enabled respectively.
Finally we have the InitCluster function which returns the connection objects as a tuple.
...
std::vector<std::string> parseEnvironmentVariables(const std::vector<std::string>& keys);
bool checkScopeAndColExists(couchbase::bucket& bucket, const std::string& scope_name, const std::string& col_name);
bool checkSearchEnabled(couchbase::cluster& cluster, int min_nodes);
...
std::tuple<couchbase::cluster, couchbase::bucket, couchbase::scope, couchbase::collection> InitCluster();
We recommend creating a single Couchbase connection when your application starts up, and sharing this instance throughout your application.
You should always set the default BUCKET_NAME, SCOPE_NAME, COL_NAME environment variables, and use the InitCluster function to get the instances.
You should share and use these instances throughout your application.
The Couchbase connection is established in the connectCluster method defined in db.h and implemented in db.cpp.
There, we call the connect method defined in the SDK to create the Database connection.
If the connection is already established, we do not do anything.
Following connection to the cluster, get the reference to the bucket, scope and collection and return all the objects as a tuple.
...
auto [connect_err, cluster] = couchbase::cluster::connect(DB_CONN_STRING, options).get();
...
auto bucket = cluster.bucket(BUCKET_NAME);
...
auto scope = bucket.scope(SCOPE_NAME);
auto col = scope.collection(COL_NAME);
return {cluster, bucket, scope, col};
Operations
Operations for interacting with the database are defined and implemented in operations.h and operations.cpp.
Insert Document
Insert function is the equivalent of the POST request and can be used to insert new documents to the collection.
We can pass the document to be inserted as a JSON string or as a JSON file path, the function takes in file_flag which is used to differentiate between the two.
-
The value gets converted to the type
tao::json::valueand inserts it to the collection iffile_flag=false. -
If
file_flag=true, it reads the content from the provided file and then converts it totao::json::value. -
Performs an upsert operation on the collection using the
doc_idand the converted document content. -
If successful,
return 1. If an error occurs, prints an error message andreturn 0.
auto [in_error, in_res] = col.insert(doc_id, v).get();
auto insert_res = Insert(col, "quickstart_test", "{ \"test\": \"hello\"}", false);
auto insert_res2 = Insert(col, "quickstart_test2", "doc.json", true);
Upsert Document
The Upsert function is the equivalent of the PUT request.
It can be used to update any existing document or to insert a new document to the collection if the doc_id does not already exist.
Similar to Insert — we can pass the document to be inserted as a JSON string or as a JSON file path, the function takes in file_flag which is used to differentiate between the two.
-
The value gets converted to the type
tao::json::valueand inserts it to the collection iffile_flag=false. -
If
file_flag=true, it reads the content from the provided file and then converts it totao::json::value. -
Performs an insert operation on the collection using the
doc_idand the converted document content. -
If successful,
return 1. If an error occurs, prints an error message andreturn 0.
auto [up_error, up_res] = col.upsert(doc_id, v).get();
auto upsert_res = Upsert(col, "quickstart_test", "{ \"test\": \"hello\"}", false);
auto upsert_res2 = Upsert(col, "quickstart_test2", "doc.json", true);
Read
The Read function is equivalent to GET requests and can be used to fetch documents using the doc_id.
-
First checks if the document exists using
col.exists(doc_id). -
If the document exists, it retrieves the document’s content using
col.get(doc_id)and returns it after converting it totao::json::valuefor easier usage on return. -
If an error occurs (such as "document not found"), it prints an error message and returns an empty
tao::json::valueobject.
auto [ex_err, ex_res] = col.exists(doc_id).get();
...
auto [get_err, get_res] = col.get(doc_id).get();
...
auto doc = get_res.content_as<tao::json::value>();
return doc;
v = Read(col, "airline_10123");
std::cout << tao::json::to_string(v) << std::endl;
Delete
The Delete function attempts to remove a document with the given doc_id.
-
Attempts to remove the document with a given
doc_idfrom the collection. -
If the deletion is successful,
return 1and if an error occurs, it prints an error message andreturn 0.
auto [delete_err, delete_res] = col.remove(doc_id).get();
auto res = Delete(col, doc_id);
Query
We can use the Query function to execute any SQL++ query on a scope.
-
Executes the SQL++ query using the provided
scope.query(query, opts). -
Returns the result of the query if successful. The result is added to a
std::vector<std::string>object that contains theid,country,avg_rating,title. -
We can pass
optsparameter, which can be used to insert positional parameters in the query. -
If there is an error, it prints an error message and returns an empty result object.
std::string query{ R"(
SELECT META(h).id, h AS doc,
AVG(r.ratings.Overall) AS avg_rating
FROM hotel h
UNNEST h.reviews r
WHERE h.country IN $1 AND h.description LIKE "%cheap%"
GROUP BY META(h).id, h
ORDER BY avg_rating DESC
LIMIT 5;
)" };
auto [q_err, q_res] = scope.query(query, couchbase::query_options{}.positional_parameters(std::vector<std::string>{"United States", "United Kingdom"})).get();
for (auto& row : query_res) {
std::cout << row << std::endl;
}
Create a Search Index
Search indexes in Couchbase are used for full-text search and efficient querying of documents based on specific fields or attributes.
The CreateSearchIndex function helps in creating a new Search index which can then be used.
-
Reads the index configuration from the
index_file. -
Checks if an index with the same name already exists using the
searchIndexExistsfunction. -
If the index with same name exists, it returns the index name.
-
If the index does not exist, it constructs a new search index object and upserts it into the Couchbase scope.
-
Returns the name of the newly created index or an empty string if there was an error.
auto err = scope_index_manager.upsert_index(i).get();
std::string index_name = CreateSearchIndex(scope, "hotel_search_index.json");
Search By Name
The SearchByName function aims to demonstrate the usage of a Search index to search for documents in a scope. Params:
-
scope: The Couchbase scope to search in. -
index: The Search index name to use for the query. -
name: The name to search for in the documents. -
field: The field where the name should be searched. -
limit: The maximum number of results to return.
auto [s_err, s_res] = scope.search(index, searchQ, opts).get();
...
std::vector<std::string> rows_res{};
// Reference is important since the copy constructor is deleted
for(auto &row:s_res.rows()){
rows_res.push_back(row.id());
}
return rows_res;
auto search_res = SearchByName(scope, index_name, "swanky", "name", 50);
std::cout << "Search result contains:\t" << search_res.size() << std::endl;
Filter
The Filter function aims to demo the construction and execution of a conjunction_query which can be described as an AND operation on two or more types of filters.
This particular implementation performs a conjunction on couchbase::match_query("United States").field("country") and couchbase::term_query("San Diego").field("city").
auto query = couchbase::conjunction_query{
couchbase::match_query("United States").field("country"),
couchbase::term_query("San Diego").field("city")
};
...
auto [err,res] = scope.search(index_name, couchbase::search_request(query), opts).get();
for(auto &row:res.rows()){
auto fields = row.fields_as<couchbase::codec::tao_json_serializer>();
rows_res.push_back(fields["name"].as<std::string>());
}
return rows_res;
auto filter_res = Filter(scope, index_name, 50, 1);
std::cout << "Filter result contains:\t" << filter_res.size() << std::endl;
Running Self-Managed Couchbase Cluster
If you are running this quickstart with a self-managed Couchbase cluster, you need to load the travel-sample data bucket in your cluster and generate the credentials for the bucket.
You need to update the connection string and the credentials in the application.properties file in the src/main/resources folder.