One of the biggest barriers for anyone who wants to start using new technologies is usually the learning curve. Often while starting a new project, we end up choosing to use what we already know to avoid any friction right at the beginning of the project.

I have spent most of my career working as a Java developer, and in the last few years I fell in love with the JPA + Spring-Boot + Lombok + Spring Data combination, but the one thing that still annoyed me was mapping relationships.

JPA is known for loading unnecessary data from the database, and over time you are forced to revisit some of your entities to change a few relationships from EAGER to LAZY. It can improve significantly your performance as you will avoid a lot of unnecessary JOINS but it does not come for free. You will be required to do a lot of refactoring to load those new lazy objects whenever they are required.

This common pattern always bothered me and I was really happy when I found that Couchbase has a connector for Spring Data (full doc here). It is simply the best part of two worlds, I can program like I would in a relational database but still leveraging all the speed of Couchbase and the power of N1QL. Let’s see how to setup a simple project:

Setting-up Couchbase with Spring Boot and Spring Data

Prerequisites:

I will assume that you already have Couchbase installed, if you don’t, please download it here

I am also using Lombok, so you might need to install Lombok’s plugin on your IDE: E clipse and IntelliJ IDEA

First, you can clone my project:

git clone https://github.com/deniswsrosa/couchbase-spring-data-sample.git 1 git clone https : //github.com/deniswsrosa/couchbase-spring-data-sample.git

or simply go to Spring-Boot Initialzr and add Couchbase and Lombok as dependencies:

Note: Lombok is not a required dependency, but it helps to reduce significantly your code base.

Now, let’s define your bucket configuration in the application.properties file:

spring.couchbase.bootstrap-hosts=localhost spring.couchbase.bucket.name=test spring.couchbase.bucket.password=couchbase spring.data.couchbase.auto-index=true 1 2 3 4 spring . couchbase . bootstrap - hosts = localhost spring . couchbase . bucket . name = test spring . couchbase . bucket . password = couchbase spring . data . couchbase . auto - index = true

And that’s it! You are already able to start up your project using:

mvn spring-boot:run 1 mvn spring - boot : run

Mapping an Entity

So far, our project does not do anything. Let’s create and map our first entity:

@Document @Data @AllArgsConstructor @NoArgsConstructor @EqualsAndHashCode public class Building { @NotNull @Id private String id; @NotNull @Field private String name; @NotNull @Field private String companyId; @Field private List<Area> areas = new ArrayList<>(); @Field private List<String> phoneNumbers = new ArrayList<>(); } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 @Document @Data @AllArgsConstructor @NoArgsConstructor @EqualsAndHashCode public class Building { @NotNull @Id private String id ; @NotNull @Field private String name ; @NotNull @Field private String companyId ; @Field private List <Area> areas = new ArrayList < > ( ) ; @Field private List <String> phoneNumbers = new ArrayList < > ( ) ; }

@Document: Couchbase’s annotation which defines an entity, similar to @Entity in JPA. Couchbase will automatically add a property called _class in the document to use it as the document type. @Data: Lombok’s annotation, auto-generate getters and setters @AllArgsConstructor: Lombok’s annotation, auto-generate a constructor using all fields of the class, this constructor is used in our tests @NoArgsConstructor: Lombok’s annotation, auto-generate a constructor with no args (required by Spring Data) @EqualsAndHashCode: Lombok’s annotation, auto-generate equals and hashcode methods, also used in our tests. @NotNull: Yes! You can use javax.validation with Couchbase. @Id: The document’s key @Field: Couchbase’s annotations, similar to @Column



Mapping entities in Couchbase is really simple and straightforward, the biggest difference here is the @Field entity which is used in 3 different ways:

Simple property: In cases like id , name and companyId, the @Field acts pretty much like the @Column in JPA. It will result in a simple property in the document:

{ "id": "building::1", "name": "Couchbase's Building", "companyId": "company::1" } 1 2 3 4 5 { "id" : "building::1" , "name" : "Couchbase's Building" , "companyId" : "company::1" }

Arrays : In the phoneNumbers’s case it will result in an array inside the document:

{ "phoneNumbers": ["phoneNumber1", "phoneNumber2"] } 1 2 3 { "phoneNumbers" : [ "phoneNumber1" , "phoneNumber2" ] }

Entities : Finally, in the areas’s case, @Field acts like a @ManyToOne relationship, the main difference is that you are not required to map anything in the Area entity:

@EqualsAndHashCode @AllArgsConstructor @NoArgsConstructor @Data public class Area { private String id; private String name; private List<Area> areas = new ArrayList<>(); } 1 2 3 4 5 6 7 8 9 10 11 12 @ EqualsAndHashCode @ AllArgsConstructor @ NoArgsConstructor @ Data public class Area { private String id ; private String name ; private List < Area > areas = new ArrayList < > ( ) ; }

Repositories

Your repositories will look very similar to standard Spring Data repositories but with a few extra annotations:

@N1qlPrimaryIndexed @ViewIndexed(designDoc = "building") public interface BuildingRepository extends CouchbasePagingAndSortingRepository<Building, String> { List<Building> findByCompanyId(String companyId); Page<Building> findByCompanyIdAndNameLikeOrderByName(String companyId, String name, Pageable pageable); @Query("#{#n1ql.selectEntity} where #{#n1ql.filter} and companyId = $1 and $2 within #{#n1ql.bucket}") Building findByCompanyAndAreaId(String companyId, String areaId); @Query("#{#n1ql.selectEntity} where #{#n1ql.filter} AND ANY phone IN phoneNumbers SATISFIES phone = $1 END") List<Building> findByPhoneNumber(String telephoneNumber); @Query("SELECT COUNT(*) AS count FROM #{#n1ql.bucket} WHERE #{#n1ql.filter} and companyId = $1") Long countBuildings(String companyId); } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @ N1qlPrimaryIndexed @ ViewIndexed ( designDoc = "building" ) public interface BuildingRepository extends CouchbasePagingAndSortingRepository < Building , String > { List < Building > findByCompanyId ( String companyId ) ; Page < Building > findByCompanyIdAndNameLikeOrderByName ( String companyId , String name , Pageable pageable ) ; @ Query ( "#{#n1ql.selectEntity} where #{#n1ql.filter} and companyId = $1 and $2 within #{#n1ql.bucket}" ) Building findByCompanyAndAreaId ( String companyId , String areaId ) ; @ Query ( "#{#n1ql.selectEntity} where #{#n1ql.filter} AND ANY phone IN phoneNumbers SATISFIES phone = $1 END" ) List < Building > findByPhoneNumber ( String telephoneNumber ) ; @ Query ( "SELECT COUNT(*) AS count FROM #{#n1ql.bucket} WHERE #{#n1ql.filter} and companyId = $1" ) Long countBuildings ( String companyId ) ; }

@N1qlPrimaryIndexed : This annotation makes sure that the bucket associated with the current repository will have a N1QL primary index

@ViewIndexed: This annotation lets you define the name of the design document and View name as well as a custom map and reduce function.

In the repository above, we are extending CouchbasePagingAndSortingRepository, which allows you to paginate your queries by simply adding a Pageable param at the end of your method definition

As it is essentially a repository, you can leverage all Spring Data keywords like FindBy, Between, IsGreaterThan, Like, Exists, etc. So, you can start using Couchbase with almost no previous knowledge and still be very productive.

As you might have noticed, you can create full N1QL queries but with a few syntax-sugars:

#(#n1ql.bucket): Use this syntax avoids hard-coding your bucket name in your query

#{#n1ql.selectEntity}: syntax-sugar to SELECT * FROM #(#n1ql.bucket) :

#{#n1ql.filter}: syntax-sugar to filter the document by type, technically it means class = ‘myPackage.MyClassName’ ( _class is the attribute automatically added in the document to define its type when you are working with Couchbase on Spring Data )

#{ #n1ql.fields} will be replaced by the list of fields (eg. for a SELECT clause) necessary to reconstruct the entity.

will be replaced by the list of fields (eg. for a SELECT clause) necessary to reconstruct the entity. #{ #n1ql.delete} will be replaced by the delete from statement.

#{#n1ql.returning} will be replaced by returning clause needed for reconstructing entity.

To demonstrate some of the cool capabilities of N1QL, let’s go a little deeper in two methods of our repository: findByPhoneNumber and findByCompanyAndAreaId:

findByPhoneNumber

@Query("#{#n1ql.selectEntity} where #{#n1ql.filter} AND ANY phone IN phoneNumbers SATISFIES phone = $1 END") List<Building> findByPhoneNumber(String telephoneNumber); 1 2 @ Query ( "#{#n1ql.selectEntity} where #{#n1ql.filter} AND ANY phone IN phoneNumbers SATISFIES phone = $1 END" ) List < Building > findByPhoneNumber ( String telephoneNumber ) ;

In the case above we are simply searching for buildings by telephone numbers. In a relational world, you would normally need 2 tables to accomplish nearly the same thing. With Couchbase we can store everything in a single document, which makes loading a “building” much faster than what you would get using any RDBMS.

Additionally, you can speed up your query performance even more by adding an index on the phoneNumbers attribute.

findByCompanyAndAreaId

@Query("#{#n1ql.selectEntity} where #{#n1ql.filter} and companyId = $1 and $2 within #{#n1ql.bucket}") Building findByCompanyAndAreaId(String companyId, String areaId); 1 2 @ Query ( "#{#n1ql.selectEntity} where #{#n1ql.filter} and companyId = $1 and $2 within #{#n1ql.bucket}" ) Building findByCompanyAndAreaId ( String companyId , String areaId ) ;

In the query above we are basically trying to find the root node (Building) giving a random child node (Area). Our data is structured as a tree because an Area could also have a list of other areas:

@EqualsAndHashCode @AllArgsConstructor @NoArgsConstructor @Data public class Area { private String id; private String name; private List<Area> areas = new ArrayList<>(); } 1 2 3 4 5 6 7 8 9 10 11 12 @ EqualsAndHashCode @ AllArgsConstructor @ NoArgsConstructor @ Data public class Area { private String id ; private String name ; private List < Area > areas = new ArrayList < > ( ) ; }

This type of querying is one of the most expensive and complex operations when you are working with a relational database, in most of the cases you either find the root node by hand or using some big fat query with UNIONs and CONNECTED BYs.

Here you can solve this problem by using a magical keyword called WITHIN.

Services

By default, you will inject and use your repositories in your Services like you normally would, but you can additionally access Couchbase’s specific repositories capabilities using the method getCouchbaseOperations()

Everything in Action

Using services are to exactly what you would expect:

buildingService.findById("building::1") 1 buildingService . findById ( "building::1" )

Building bulding = new Building("bulding::1", "Couchbase Building", "company::1", new ArrayList<>(), new ArrayList<>()); buildingService.save(building); 1 2 3 4 Building bulding = new Building ( "bulding::1" , "Couchbase Building" , "company::1" , new ArrayList < > ( ) , new ArrayList < > ( ) ) ; buildingService . save ( building ) ;

buildingService.findByCompanyIdAndNameLike("company::1", "Cou%", 0); 1 buildingService . findByCompanyIdAndNameLike ( "company::1" , "Cou%" , 0 ) ;

Check out the integration test class BuildingServiceIntegrationTest to see everything in action.

If you have any questions, tweet me at @deniswsrosa or ask a question on our forum