4

Spring Data JPA: Emitting events on save() – Kotlin Edition

 2 years ago
source link: https://blog.davidvassallo.me/2019/09/06/spring-data-jpa-emitting-events-on-save-kotlin-edition/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Scenario

In a nutshell, we’d like to emit an event or take some custom action whenever a Spring Data JPA repository gets it’s “save(…)” method called. Our criteria is to have minimal constraints set on other developers using this. In other words, we wouldn’t like a solution where we create a new method or change the name of the method. Spring devs are used to simply auto-wiring a repository into their controllers and calling .save(…) to persist their data. We’d like to hook into that workflow and perform some action whenever that happens.

In this article, our Spring Data JPA repo is a run-of-the-mill CRUDRepository we’re calling “PlanRepo”:

interface PlanRepo : CrudRepository<PlanEntity, Long> {}

The “custom action” to take can be anything – sending a message to an event bus, or for the sake of simplicity here, just writing to console.

What was considered

Initially we thought of using a Kotlin Function Extension. However, this would have necessitated adding a new method to the existing CRUDRepository interface. For example, assuming the following code snippet for Spring REST controller:

@RestController
class DemoController(@Autowired val planRepo: PlanRepo) {

    fun PlanRepo.saveAndEmit( p0: PlanEntity ) : PlanEntity {
        println("=====CUSTOM ACTION=====")
        return this.save(p0)
    }

    @GetMapping("test")
    fun test() :Boolean{  
        val testPlanEntity = PlanEntity()
        planRepo.saveAndEmit(testPlanEntity)      
        return true
    }
}

Note how we use a function extension to define a new method named saveAndEmit on the CRUDRepository interface rather than on the instance (i.e. it’s fun PlanRepo.saveAndEmit, not fun planRepo.saveAndEmit), then we use this new method on the instance that we injected into the controller.

This works but violates one of our criteria. Devs need to remember to call the saveAndEmit method rather than calling their usual save method

Decorators for the win

Typically I hate using decorators in Java. You need to override all the methods of the original class you’re decorating, which can add quite a bit of boilerplate to your code. Using decorators in Kotlin by comparison is a pleasure because of the option to use Kotlin Delegated Properties. The documentation I’ve linked only shows examples of property-level delegation, but Kotlin also allows you to define class level delegates. So we can define a new class like so:

@Component
class PlanEmitRepo(@Autowired val planRepo: PlanRepo) : PlanRepo by planRepo {
    override fun <S : PlanEntity?> save(p0: S): S {
        println("=====CUSTOM ACTION=====")
        return planRepo.save(p0)
    }
}

Those first couple of lines are the most important:

The delegation takes care of any methods we don’t explicitly over-ride in the class body. We only really want to over-ride the “save” method, so that’s all the class body needs to contain.

With this delegate in hand, we can now change the controller to a very succint:

class UserController(@Autowired val emitRepo: PlanEmitRepo) {

    @PostMapping("/test")
    fun test() :Boolean{
        val testPlanEntity = PlanEntity()
        emitRepo.save(testPlanEntity)
        return true
    }
}

Note how nothing much changed from how a Dev would normally approach this – the only change is injecting the “PlanEmitRepo” class we just defined rather than the original “PlanRepo” interface we used before.


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK