DEV Community

Cover image for Your own custom Spring Data repository
Nicolas Fränkel
Nicolas Fränkel

Posted on • Originally published at blog.frankel.ch

Your own custom Spring Data repository

Frameworks promise to speed up one's development pace provided one follows the mainstream path.
The path may be more or less narrow. I'm a big fan of the Spring ecosystem because its design is extensible and customizable at different abstraction levels: thus, the path is as large as you need it to be.

Functional Programming is becoming more and more popular. Spring provides a couple of DSLs for the Kotlin language. For example, the Beans DSL and the Routes DSL allow for a more functional approach toward Spring configuration. On the type side, Vavr (previously Javaslang) is pretty popular in Java, while Kotlin has Arrow.

In this post, I'd like to describe how one can use Arrow's type system with Spring Data. Ultimately, you can benefit from the explanations to craft your custom Spring Data repository.

The starting architecture

The starting architecture for my application is pretty standard:

  1. A REST controller with two GET mappings
  2. A Spring Data JDBC repository interface

Because it's standard, Spring handles a lot of the plumbing, and we don't need to write a lot of code. With Kotlin, it's even more concise:

class Person(@Id val id: Long, var name: String, var birthdate: LocalDate?)

interface PersonRepository : CrudRepository<Person, Long>

@RestController
class PersonController(private val repository: PersonRepository) {

    @GetMapping
    fun getAll(): Iterable<Person> = repository.findAll()

    @GetMapping("/{id}")
    fun getOne(@PathVariable id: Long) = repository.findById(id)
}

@SpringBootApplication
class SpringDataArrowApplication

fun main(args: Array<String>) {
    runApplication<SpringDataArrowApplication>(*args)
}
Enter fullscreen mode Exit fullscreen mode

Toward a more functional approach

This step has nothing to do with Spring Data and is not required, but it fits the functional approach better. As mentioned above, we can benefit from using the Routes and Beans DSL. Let's refactor the above code to remove annotations as much as possible.

class PersonHandler(private val repository: PersonRepository) {                   // 1

  fun getAll(req: ServerRequest) = ServerResponse.ok().body(repository.findAll()) // 2
  fun getOne(req: ServerRequest): ServerResponse = repository
    .findById(req.pathVariable("id").toLong())
    .map { ServerResponse.ok().body(it) }
    .orElse(ServerResponse.notFound().build())                                    // 3
}

fun beans() = beans {                                                             // 4
  bean<PersonHandler>()
  bean {
    val handler = ref<PersonHandler>()                                           // 5
    router {
      GET("/", handler::getAll)
      GET("/{id}", handler::getOne)
    }
  }
}

fun main(args: Array<String>) {
  runApplication<SpringDataArrowApplication>(*args) {
    addInitializers(beans())                                                      // 6
  }
}
Enter fullscreen mode Exit fullscreen mode
  1. Create a handler class to organize the routing functions
  2. All routing functions should accept a ServerRequest parameter and return a ServerResponse
  3. Add an additional capability: if the entity is not found, return a 404
  4. Use the Routes DSL to map HTTP verbs and path to routing functions
  5. ref() retrieves bean with the configured type from Spring's application context
  6. Explicitly call the beans() function, no more magic!

Introducing Arrow

Functional companion to Kotlin's Standard Library

-- Arrow

Arrow comes with four different components:

  1. Core
  2. FX: Functional Effects Framework companion to KotlinX Coroutines
  3. Optics: Deep access and transformations over immutable data
  4. Meta: Metaprogramming library for Kotlin compiler plugins

The Core library offers the Either<E,T> type. Arrow advises using Either<Unit,T> to model an optional value. On the other side, Spring Data JDBC findById() returns a java.util.Optional<T>.

Bridging the gap

How do we bridge the gap between Optional and Either?

Here's a first attempt:

repository
    .findById(req.pathVariable("id").toLong())      // 1
    .map { Either.fromNullable(it) }                // 2
    .map { either ->
        either.fold(
            { ServerResponse.notFound().build() },  // 3
            { ServerResponse.ok().body(it) }        // 3
        )
    }.get()                                         // 4
Enter fullscreen mode Exit fullscreen mode
  1. Optional<Person>
  2. Optional<Either<Unit, Person>>
  3. Optional<ServerResponse>
  4. At this point, we can safely call get() to get a ServerResponse

I believe the usage of Optional<Either<Unit,Person>> is not great. However, Kotlin can help us in this regard with extension functions:

private fun <T> Optional<T>.toEither() =
    if (isPresent) Either.right(get())
    else Unit.left()
Enter fullscreen mode Exit fullscreen mode

With this function, we can improve the existing code:

repository
    .findById(req.pathVariable("id").toLong())    // 1
    .toEither()                                   // 2
    .fold(
        { ServerResponse.notFound().build() },    // 3
        { ServerResponse.ok().body(it) }          // 3
    )
Enter fullscreen mode Exit fullscreen mode
  1. Optional<Person>
  2. Either<Unit, Person>
  3. ServerResponse

It looks nicer this way, but it would be so much better to have the repository return an Either<Unit,Person> directly.

Spring Data customization

Let's check how we can customize Spring Data to achieve that.

By default, a Spring Data repository offers all generic functions you can expect, .e.g.:

Spring Data Repository class diagram

I believe that one comes to Spring Data for ease of use, but that one stays for its extensibility capabilities.

At the base level, one can add functions that follow a certain naming pattern, e.g., findByFirstNameAndLastNameOrderByLastName(). Spring Data will generate the implementing code without you needing to write a single line of SQL. When you hit the limits of this approach, you can annotate the function with the SQL that you want to run.

In both cases, you need to set the return type. While the number of possible return types is pretty huge, it's still limited. The framework cannot account for every possible type, and specifically, the list doesn't contain Either.

The next extensibility level is to add any function with the desired signature via a custom implementation. For that, we need:

  • An interface that declares the wanted function
  • A class that implements the interface
interface CustomPersonRepository {                                         // 1
    fun arrowFindById(id: Long): Either<Unit, Person>                      // 2
}

class CustomPersonRepositoryImpl(private val ops: JdbcAggregateOperations) // 3
    : CustomPersonRepository {                                             // 4

    override fun arrowFindById(id: Long) =
        Either.fromNullable(ops.findById(id, Person::class.java))           // 5
}

interface PersonRepository
    : CrudRepository<Person, Long>, CustomPersonRepository                 // 6
Enter fullscreen mode Exit fullscreen mode
  1. New custom interface
  2. Declare the wanted function
  3. New implementing class...
  4. ... that implements the parent interface
  5. Implement the function
  6. Just extend the custom interface

Now, we can call:

repository.arrowFindById(req.pathVariable("id").toLong())
    .fold(
        { ServerResponse.notFound().build() },
        { ServerResponse.ok().body(it) }
    )
Enter fullscreen mode Exit fullscreen mode

This approach works but has one major flaw. To avoid a clash in the functions' signature, we have to invent an original name for our function that returns Either i.e. arrowFindById().

Changing the default base repository

To overcome this limitation, we can leverage another extension point: change the default base repository.

Spring Data applications define interfaces, but the implementation needs to come from somewhere. The framework provides one by default, but it's possible to switch it with our own.

Here's an overview of the class diagram:

Spring Data JdbcRepositoryFactory class diagram overview

The detailed flow is pretty complex: the important part is the SimpleJdbcRepository class. Spring Data will find the class via the JdbcRepositoryFactoryBean bean, create a new instance of it and register the instance in the context.

Let's create a base repository that uses Either:

@NoRepositoryBean
interface ArrowRepository<T, ID> : Repository<T, ID> {         // 1
    fun findById(id: Long): Either<Unit, T>                    // 2
    fun findAll(): Iterable<T>                                 // 3
}

class SimpleArrowRepository<T, ID>(                            // 4
    private val ops: JdbcAggregateOperations,
    private val entity: PersistentEntity<T, *>
) : ArrowRepository<T, ID> {

    override fun findById(id: Long) = Either.fromNullable(
        ops.findById(id, entity.type)                          // 5
    )

    override fun findAll(): Iterable<T> = ops.findAll(entity.type)
}
Enter fullscreen mode Exit fullscreen mode
  1. Our new interface repository...
  2. ...with the signature we choose without any collision risk.
  3. I was too lazy to implement everything.
  4. The base implementation for the repository interface. The constructor needs to accept those two parameters.
  5. Don't reinvent the wheel; use the existing JdbcAggregateOperations instance.

We need to annotate the main application class with @EnableJdbcRepositories and configure the latter to switch to this base class.

@SpringBootApplication
@EnableJdbcRepositories(repositoryBaseClass = SimpleArrowRepository::class)
class SpringDataArrowApplication
Enter fullscreen mode Exit fullscreen mode

To ease the usage from the client code, we can create an annotation that overrides the default value:

@EnableJdbcRepositories(repositoryBaseClass = SimpleArrowRepository::class)
annotation class EnableArrowRepositories
Enter fullscreen mode Exit fullscreen mode

Now, the usage is straightforward:

@SpringBootApplication
@EnableArrowRepositories
class SpringDataArrowApplication
Enter fullscreen mode Exit fullscreen mode

At this point, we can move the Arrow repository code into its project and distribute it for other "client" projects to use. No further extension is necessary, though Spring Data offers much more, e.g., switching the factory bean.

Conclusion

Spring Data provides a ready-to-use repository implementation out-of-the-box. When it's not enough, its flexible design makes it possible to extend the code at different abstraction levels.

This post showed how to replace the default base repository with our own, which uses an Arrow type in the function signature.

Thanks to Mark Paluch for his review.

The complete source code for this post can be found on Github in Maven format.

To go further:

Orginally published at A Java Geek on April 11th, 2021

Top comments (0)