To run this prebuilt project, you will need:
Option 1: Couchbase Capella
Option 2: Couchbase Server
git clone https://github.com/couchbase-examples/scala-quickstart
All configuration for communication with the database and the three web servers is stored in the /src/main/resources/application.conf
file. This includes the connection string, username, and password for Couchbase, and the port numbers for Play/Akka http/htt4ps.
The default username for Couchbase server is assumed to be Administrator
and the default password is assumed to be password
. If these are different in your environment you will need to change them before running the application.
This section is only needed when you've opted for using a Couchbase Capella cloud instance for following the tutorial.
To enable the tutorial code to establish a connection to Capella, you will need to make a couple of changes:
capella = true
in application.conf
bucket-name
as the one defined in application.conf
username
in application.conf
is configured for access to this buckethost
in application.conf
to the Wide Area Network address in Capella -> Cluster -> your_cluster_name
-> Connect. See the image below:
Note that for simplicity, trust certificate checking has been disabled as part of the tutorial by setting this in the SecurityConfig
. If you want to learn more, see the security options as part of the SDK client settings.
Possible issues:
host
name in application.conf
, use bucket-name
insteadCouchbaseException: Failed to create bucket
,
please make sure you've created the bucket through the Capella user interface, and the name lines up with bucket-name
in application.conf
At this point the application is ready, and you can run it via your IDE or from the terminal:
sbt run
Note: When using Option 2: Couchbase Server, then Couchbase Server 7 must be installed and running on localhost (http://127.0.0.1:8091) prior to running the Scala application.
The application will keep running until you provide a line of input, after which it will shut down the web servers.
You can launch your browser and go to each web server's Swagger start page:
Simple REST APIs using the Scala Couchbase SDK with the following endpoints:
We will be setting up REST APIs with three of the commonly used frameworks with Scala: Play, Akka http and http4s. Endpoint descriptions and Swagger documentation is created through the tapir framework.
The REST APIs will be used manage user profile documents. Our profile document will have an auto-generated UUID for its key, first and last name of the user, an email, and hashed password. For this demo we will store all profile information in just one document in a collection named profile
:
{
"pid": "b181551f-071a-4539-96a5-8a3fe8717faf",
"firstName": "John",
"lastName": "Doe",
"email": "john.doe@couchbase.com",
"saltedPassword": "$2a$10$tZ23pbQ1sCX4BknkDIN6NekNo1p/Xo.Vfsttm.USwWYbLAAspeWsC"
}
As we can see, user's passwords will be encrypted in the database. This is achieved this through a bcrypt Scala wrapper.
To begin clone the repo and open it up in the IDE of your choice to learn about how to create, read, update and delete documents in your Couchbase Server.
For CRUD operations we will use the Key Value operations that are built into the Couchbase SDK to create, read, update, and delete a document. Every document will need an ID (similar to a primary key in other databases) in order to save it to the database.
If we look at the the ProfileController
trait, found in the controllers folder and navigate to the postProfile function, then we can see the following type signature:
def postProfile(profileInput: ProfileInput): F[Either[String, Profile]]
The abstract definitions in ProfileController
(and CouchbaseConnection
) are generalized over some effect F
, making it easier to switch between implementations such as Future
and IO
. This will help in defining our different REST APIs and also make it easier to test our code.
Within the F
type parameter, we can see that we return an Either[String, Profile]]
representing an error string, or a successful result with the Profile
case class.
The input for posting a Profile
is a ProfileInput
:
final case class ProfileInput(
firstName: String,
lastName: String,
email: String,
password: String
)
The implementation for postProfile
in CouchbaseProfileController
will salt the password and create a UUID
, which are returned as part of the Profile
datatype.
final case class Profile(
pid: UUID,
firstName: String,
lastName: String,
email: String,
saltedPassword: String
)
def fromProfileInput(profileInput: ProfileInput): Try[Profile] = {
profileInput match {
case ProfileInput(firstName, lastName, email, password) =>
for {
salted <- password.bcryptSafeBounded
} yield Profile(UUID.randomUUID(), firstName, lastName, email, salted)
}
}
Our Profile
document is ready to be persisted to the database. We create call to the collection
using the local variable profileCollection: Future[Collection]
and then call the insert
method and passing it the UUID from the Profile
as the key.
Note that the Scala SDK has support for various types of JSON commonly used within the Scala community. Here we use the circe JSON library to convert the Profile
with .asJson
and insert it.
Once the document is inserted we then return the document saved and the result all as part of the same object back to the user.
override def postProfile(profileInput: ProfileInput): Future[Either[String, Profile]] = {
for {
pc <- profileCollection
profile <- Profile.fromProfileInput(profileInput) match {
case Failure(exception) => Future.successful(Left(exception.toString))
case Success(p) =>
pc.insert[io.circe.Json](p.pid.toString, p.asJson) map (_ => Right(p))
}
} yield profile
}
from controllers/CouchbaseProfileController.scala
Navigate to the getProfile
function in the CouchbaseProfileController
file in the controllers folder. We only need the profile ID pid
from the user to retrieve a particular profile document using a basic key-value operation which is passed in the method signature as a string. Since we created the document with a unique key we can use that key to find the document in the scope and collection it is stored in.
override def getProfile(pid: UUID): Future[Either[String, Profile]] = {
for {
pc <- profileCollection
res <- pc.get(pid.toString)
} yield res.contentAsCirceJson[Profile]
}
from getProfile function in controllers/CouchbaseProfileController.scala and using the implicit class in models/CirceGetResult.
Now let's navigate to the putProfile
function of the CouchbaseProfileController
class. The entire document gets replaced except for the document key and the pid
field. We create a call to the collection
using the upsert
method and then return the document saved and the result just as we did in the previous endpoint.
The only difference in implementation with postProfile
) is the following line:
pc.upsert[io.circe.Json](p.pid.toString, p.asJson)
from update method of controllers/CouchbaseProfileController.scala
Navigate to the deleteProfile
function in the CouchbaseProfileController
class. We only need the Key
or id from the user to remove a document using a basic key-value operation.
pc.remove(pid.toString)
from deleteProfile method of controllers/CouchbaseProfileController.scala
SQL++ (N1QL) is a powerful query language based on SQL, but designed for structured and flexible JSON documents. We will use a SQL++ query to search for profiles with Skip, Limit, and Search options.
Navigate to the getProfiles
method in the CouchbaseProfileController
class. This endpoint is different from all of the others because it makes the SQL++ query rather than a key-value operation. This means more overhead because the query engine is involved. We did create an index specific for this query, so it should be performant.
The individual skip
(optional), limit
(optional), and search
values are obtained from their respective parameters. Then, we build our SQL++ query using the parameters that were passed in.
Finally, we pass that query
to the cluster.query
method and return the result.
Take notice of the SQL++ syntax and how it targets the bucket
.scope
.collection
.
override def profileListing(
limit: Option[Int],
skip: Option[Int],
search: String
): Future[Either[String, List[Profile]]] = {
val query = s"SELECT p.* FROM " +
s"`${quickstartConfig.couchbase.bucketName}`.`_default`.`${quickstartConfig.couchbase.collectionName}` p " +
s"WHERE lower(p.firstName) LIKE '%${search.toLowerCase}%' " +
s"OR lower(p.lastName) LIKE '%${search.toLowerCase}%' " +
s"LIMIT " + limit.getOrElse(5) + " OFFSET " + skip.getOrElse(0)
import cats.implicits._
for {
cluster <- couchbaseConnection.cluster
rows <- Future.fromTry(
cluster.query(
query,
QueryOptions(scanConsistency =
Some(QueryScanConsistency.RequestPlus())
)
)
)
profiles <- Future.fromTry(
rows
.rowsAs[io.circe.Json]
.map(_.map(json => json.as[Profile].left.map(_.getMessage())))
)
accumulatedProfiles = profiles.toList.sequence
} yield accumulatedProfiles
}
from getProfiles method of Controllers/ProfileController.cs
To run the standard unit and integration tests, use the following commands:
sbt test
Setting up a basic REST API in Spring Boot with Couchbase is fairly simple. This project when run with Couchbase Server 7 installed creates a bucket in Couchbase, an index for our parameterized SQL++ (N1QL) query, and showcases basic CRUD operations needed in most applications.