DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Please enter at least three characters to search
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Zones

Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks

Last call! Secure your stack and shape the future! Help dev teams across the globe navigate their software supply chain security challenges.

Modernize your data layer. Learn how to design cloud-native database architectures to meet the evolving demands of AI and GenAI workloads.

Releasing software shouldn't be stressful or risky. Learn how to leverage progressive delivery techniques to ensure safer deployments.

Avoid machine learning mistakes and boost model performance! Discover key ML patterns, anti-patterns, data strategies, and more.

Related

  • Testcontainers With Kotlin and Spring Data R2DBC
  • The First Annual Recap From JPA Buddy
  • Advanced Search and Filtering API Using Spring Data and MongoDB
  • Manage Hierarchical Data in MongoDB With Spring

Trending

  • Java's Quiet Revolution: Thriving in the Serverless Kubernetes Era
  • Scalability 101: How to Build, Measure, and Improve It
  • Virtual Threads: A Game-Changer for Concurrency
  • Enhancing Avro With Semantic Metadata Using Logical Types
  1. DZone
  2. Data Engineering
  3. Databases
  4. Couchbase with Kotlin, Spring Boot, and Spring Data

Couchbase with Kotlin, Spring Boot, and Spring Data

Read this article in order to learn more about Kotlin and how to convert a Java application.

By 
Denis W S Rosa user avatar
Denis W S Rosa
·
Jun. 12, 18 · Tutorial
Likes (4)
Comment
Save
Tweet
Share
8.0K Views

Join the DZone community and get the full member experience.

Join For Free

Last year, I started learning Kotlin, and I was surprised at how easy it was to convert a Java application. IntelliJ and a few other IDEs offer nice tools for automatic conversion, and with a few adjustments, you can end up with a much more concise and less error-prone code.

So, I decided to create a sample application to show my new favorite combination: Kotlin, Spring Boot, Spring Data, and Couchbase:

Creating a User Profile Service

You can clone the whole project here.

Let’s start by creating our main class:

@SpringBootApplication
open class KotlinDemoApplication

fun main(args: Array<String>) {
SpringApplication.run(KotlinDemoApplication::class.java, *args)
}

Note: You class must be open otherwise you will end up with the following error:

org.springframework.beans.factory.parsing.BeanDefinitionParsingException: Configuration problem: @Configuration class 'KotlinDemoApplication' may not be final. Remove the final modifier to continue.
Offending resource: com.couchbase.KotlinDemoApplication
at org.springframework.beans.factory.parsing.FailFastProblemReporter.error(FailFastProblemReporter.java:70) ~[spring-beans-4.3.13.RELEASE.jar:4.3.13.RELEASE]
at org.springframework.context.annotation.ConfigurationClass.validate(ConfigurationClass.java:214) ~[spring-context-4.3.13.RELEASE.jar:4.3.13.RELEASE]

Here is our User entity, it is very similar to the Java one:

@Document
class User(): BasicEntity() {

    constructor(id: String,
                name: String,
                address: Address,
                preferences: List<Preference>,
                securityRoles: List<String>): this(){

        this.id = id;
        this.name = name;
        this.address = address;
        this.preferences = preferences;
        this.securityRoles = securityRoles;
    }

    @Id
    var id: String? = null

    @NotNull
    var name: String? = null

    @Field
    var address: Address? = null

    @Field
    var preferences: List<Preference> = emptyList()

    @Field
    var securityRoles: List<String> = emptyList()
}
  • @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.
  • @Id: The document’s key
  • @Field: Couchbase’s annotations, similar to JPA’s @Column. It is not mandatory, but we do recommend using it.

Mapping attributes in Couchbase are really simple. They will be directly mapped to the correspondent structure in JSON:

  • Simple Properties: Straightforward mapping to JSON:
{
"id": "user::1",
"name": "Denis Rosa"
}

Arrays: As you might expect, arrays like securityRoles will be converted to JSON arrays:

{
"securityRoles": ["admin", "user"]
}

Nested Entities: Do you hate to map @ManyToOne relationships? Me too. As we are using a document database, there is no need to write these relationships anymore, nested entities are also directly translated to JSON.

{  
   "id":"user::1",
   "name":"Denis Rosa",
   "address":{  
      "streetName":"A Street Somewhere",
      "houseNumber":"42",
      "postalCode":"81234",
      "city":"Munich",
      "country":"DE"
   },
   "preferences":[  
      {  
         "name":"lang",
         "value":"EN"
      }
   ],
   "securityRoles":[  
      "admin",
      "user"
   ]
}

Repositories

Now, let’s take a look at how our repository will look like:

@N1qlPrimaryIndexed
@ViewIndexed(designDoc = "user")
interface UserRepository : CouchbasePagingAndSortingRepository<User, String> {

    fun findByName(name: String): List<User>

    @Query("#{#n1ql.selectEntity} where #{#n1ql.filter} and ANY preference IN " + " preferences SATISFIES preference.name = $1 END")
    fun findUsersByPreferenceName(name: String): List<User>

    @Query("#{#n1ql.selectEntity} where #{#n1ql.filter} and meta().id = $1 and ARRAY_CONTAINS(securityRoles, $2)")
    fun hasRole(userId: String, role: String): User
}

@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.

As you can see below, you can leverage all Spring Data keywords to query the database, such as FindBy, Between, IsGreaterThan, Like, Exists, etc.

fun findByName(name: String): List<User>

The repository is extending CouchbasePagingAndSortingRepository, which allows you to paginate your queries by simply adding a Pageable param at the end of your method definition. If you need to write more powerful queries, you can also use N1QL:

@Query("#{#n1ql.selectEntity} where #{#n1ql.filter} and ANY preference IN " + " preferences SATISFIES preference.name = $1 END")
    fun findUsersByPreferenceName(name: String): List<User>

    @Query("#{#n1ql.selectEntity} where #{#n1ql.filter} and meta().id = $1 and ARRAY_CONTAINS(securityRoles, $2)")
    fun hasRole(userId: String, role: String): User

The queries above have a few syntax-sugars to make it smaller:

  • #(#n1ql.bucket): Use this syntax avoids hard-coding the 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 to 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.
  • #{#n1ql.delete} will be replaced by the delete from statement.
  • #{#n1ql.returning} will be replaced by returning clause needed for reconstructing entity.

Services

Our service is basically forwarding requests to our repository, but if you need to write ad-hoc queries, here is the right place:

@Service
class UserService {

    @Autowired
    lateinit var userRepository: UserRepository;

    fun findByName(name: String): List<User> = userRepository.findByName(name)

    fun findById(userId: String) = userRepository.findOne(userId)

    fun save(@Valid user: User) = userRepository.save(user)

    fun findUsersByPreferenceName(name: String): List<User> = userRepository.findUsersByPreferenceName(name)

    fun hasRole(userId: String, role: String): Boolean {
        return userRepository.hasRole(userId, role) != null
    }

    /**
     * Example of ad hoc queries
     */
    fun findUserByAddress(streetName: String = "", number: String = "", postalCode: String = "",
                          city: String = "", country: String = ""): List<User> {

        var query = "SELECT meta(b).id as id, b.* FROM " + getBucketName() + " b WHERE  b._class = '" + User::class.java.getName() + "' "

        if (!streetName.isNullOrBlank()) query += " and b.address.streetName = '$streetName' "

        if (!number.isNullOrBlank()) query += " and b.address.houseNumber = '$number' "

        if (!postalCode.isNullOrBlank()) query += " and b.address.postalCode = '$postalCode' "

        if (!city.isNullOrBlank()) query += " and b.address.city = '$city' "

        if (!country.isNullOrBlank()) query += " and b.address.country = '$country' "

        val params = N1qlParams.build().consistency(ScanConsistency.REQUEST_PLUS).adhoc(true)
        val paramQuery = N1qlQuery.parameterized(query, JsonObject.create(), params)
        return userRepository.getCouchbaseOperations().findByN1QLProjection(paramQuery, User::class.java)
    }

    fun getBucketName() = userRepository.getCouchbaseOperations().getCouchbaseBucket().bucketManager().info().name()
}

Controllers

Finally, let’s also add a controller to test our services via rest:

@RestController
@RequestMapping("/api/user")
class UserController {

    @Autowired
    lateinit var userService: UserService

    @GetMapping(value = "/{id}")
    fun findById(@PathParam("id") id: String) = userService.findById(id)


    @GetMapping(value = "/preference")
    fun findPreference(@RequestParam("name") name: String): List<User> {
        return userService.findUsersByPreferenceName(name)
    }

    @GetMapping(value = "/find")
    fun findUserByName(@RequestParam("name") name: String): List<User> {
        return userService.findByName(name)
    }

    @PostMapping(value = "/save")
    fun findUserByName(@RequestBody user: User) = userService.save(user)

    @GetMapping(value = "/findByAddress")
    fun findByAddress(@RequestParam("streetName", defaultValue = "") streetName: String,
                      @RequestParam("number", defaultValue = "") number: String,
                      @RequestParam("postalCode", defaultValue = "") postalCode: String,
                      @RequestParam("city", defaultValue = "") city: String,
                      @RequestParam("country", defaultValue = "") country: String): List<User> {
        return userService.findUserByAddress(streetName, number, postalCode, city, country);
    }

}

Writing Integration Tests with Kotlin

To run the integrations tests correctly, don’t forget to configure the credentials of your database in the application.properties file:

spring.couchbase.bootstrap-hosts=localhost
spring.couchbase.bucket.name=test
spring.couchbase.bucket.password=somePassword
spring.data.couchbase.auto-index=true

Here, you can see what our tests look like:

@Test
    fun testComposedAddress() {
        val address1 = Address("street1", "1", "0000", "santo andre", "br")
        val address2 = Address("street1", "2", "0000", "santo andre", "br")
        val address3 = Address("street2", "12", "1111", "munich", "de")

        userService.save(User(USER_1, "user1", address1, emptyList(), emptyList()))
        userService.save(User("user::2", "user2", address2, emptyList(), emptyList()))
        userService.save(User("user::3", "user3", address3, emptyList(), emptyList()))

        var users = userService.findUserByAddress(streetName = "street1")
        assertThat(users, hasSize<Any>(2))

        users = userService.findUserByAddress(streetName = "street1", number=  "1")
        assertThat(users, hasSize<Any>(1))

        users = userService.findUserByAddress(country = "de")
        assertThat(users, hasSize<Any>(1))
    }

Kotlin and Maven Dependencies

Kotlin is moving quickly, so be aware to use the most recent versions of each dependency:

<dependencies>
        <dependency>
            <groupId>org.jetbrains.kotlin</groupId>
            <artifactId>kotlin-stdlib-jdk8</artifactId>
            <version>1.2.41</version>
        </dependency>
        <dependency>
            <groupId>org.jetbrains.kotlin</groupId>
            <artifactId>kotlin-reflect</artifactId>
            <version>1.2.41</version>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.module</groupId>
            <artifactId>jackson-module-kotlin</artifactId>
            <version>2.9.5</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-couchbase</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-rest</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.hamcrest</groupId>
            <artifactId>hamcrest-library</artifactId>
            <version>1.3</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.data</groupId>
            <artifactId>spring-data-couchbase</artifactId>
        </dependency>
        <dependency>
            <groupId>org.jetbrains.kotlin</groupId>
            <artifactId>kotlin-stdlib-jdk8</artifactId>
            <version>${kotlin.version}</version>
        </dependency>
        <dependency>
            <groupId>org.jetbrains.kotlin</groupId>
            <artifactId>kotlin-test</artifactId>
            <version>${kotlin.version}</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.jetbrains.kotlin</groupId>
            <artifactId>kotlin-maven-allopen</artifactId>
            <version>1.2.41</version>
        </dependency>
    </dependencies>

You can view the whole pom.xml here.

Spring Data Database Kotlin (programming language) Spring Framework Data (computing)

Published at DZone with permission of Denis W S Rosa, DZone MVB. See the original article here.

Opinions expressed by DZone contributors are their own.

Related

  • Testcontainers With Kotlin and Spring Data R2DBC
  • The First Annual Recap From JPA Buddy
  • Advanced Search and Filtering API Using Spring Data and MongoDB
  • Manage Hierarchical Data in MongoDB With Spring

Partner Resources

×

Comments
Oops! Something Went Wrong

The likes didn't load as expected. Please refresh the page and try again.

ABOUT US

  • About DZone
  • Support and feedback
  • Community research
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 100
  • Nashville, TN 37211
  • support@dzone.com

Let's be friends:

Likes
There are no likes...yet! 👀
Be the first to like this post!
It looks like you're not logged in.
Sign in to see who liked this post!