Reputation: 4238
As I'm aware spring transaction automatically rolls back for all unchecked exceptions. Adding to that fact that Kotlin has only unchecked exceptions I expected that transaction should be rolled back for all of them.
Unfortunately this is not how it works. Below there is complete example showing what I'm working on. Why is this not working?
package example1
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.data.jpa.repository.Modifying
import org.springframework.data.jpa.repository.Query
import org.springframework.data.jpa.repository.config.EnableJpaRepositories
import org.springframework.data.repository.PagingAndSortingRepository
import org.springframework.data.repository.query.Param
import org.springframework.stereotype.Repository
import org.springframework.stereotype.Service
import org.springframework.test.context.junit.jupiter.SpringJUnitJupiterConfig
import org.springframework.transaction.annotation.EnableTransactionManagement
import org.springframework.transaction.annotation.Transactional
import javax.persistence.*
// EXCEPTIONS
class MyCustomException(message: String) : Exception(message)
// ENTITIES
@Entity
@Table(name = "posts")
class Post(
@Id
@Column(name = "id")
@GeneratedValue(strategy = GenerationType.SEQUENCE)
var id: Long? = null,
@Version
@Column(name = "version")
var version: Long? = null,
@Column(name = "score")
var score: Int = 0
)
// REPOSITORIES
@Repository
interface PostRepository : PagingAndSortingRepository<Post, Long> {
@Modifying
@Query(value = "update Post p set p.score = p.score + 1 where p.id = :postId")
fun upvote(@Param("postId") postId: Long)
@Query(value = "select p.score from Post p where p.id = :postId")
fun getScore(@Param("postId") postId: Long): Int?
}
// SERVICES
interface PostService {
fun upvote(postId: Long)
}
@Service
open class PostServiceImpl(
@Autowired
val postRepository: PostRepository
) : PostService {
@Transactional//(rollbackFor = [MyCustomException::class])
override fun upvote(postId: Long) {
postRepository.upvote(postId)
throw MyCustomException("Something wrong happend!")
}
}
// CONFIGURATION
@EnableJpaRepositories(basePackages = ["example1"])
@EnableTransactionManagement
@SpringBootApplication(scanBasePackages = ["example1"])
open class FrameworkApplication
// TESTS
@SpringJUnitJupiterConfig(classes = [FrameworkApplication::class])
@DisplayName("Rollback test")
class TestClass(
@Autowired
val postService: PostService,
@Autowired
val postRepository: PostRepository
) {
@AfterEach
fun cleanUp() {
postRepository.deleteAll()
}
@Test
@DisplayName("Should rollback after exception")
fun testUpvote() {
//given
var post = Post()
post = postRepository.save(post)
val postId = post.id!!
//then
Assertions.assertThrows(Exception::class.java) {
postService.upvote(postId)
}
//then
Assertions.assertEquals(0, postRepository.getScore(postId))
}
}
Upvotes: 2
Views: 3656
Reputation: 4238
Although Kotlin does not have checked exceptions, it still has RuntimeException class. As of Spring you can read in it's documentation regarding transactions management that:
In its default configuration, the Spring Framework’s transaction infrastructure code only marks a transaction for rollback in the case of runtime, unchecked exceptions; that is, when the thrown exception is an instance or subclass of RuntimeException.
Knowing that we can try to change base class of our exception from Exception
to RuntimeException
to see that then everything works as expected.
It may not be what we expect but we have to remember that after compilation, difference between both exception types comes up to nothing more than class hierarchy. So Spring can only check if our class extends RuntimeException
or not. Theoretically it could rollback transaction for all exception types, but this has its drawbacks. First, as Spring provides only one version of binaries for both Java and Kotlin it would have to decide whether to support rollback for subclasses of RuntimeException
(more common in Java) or subclasses of Exception
(more common in Kotlin). Additionally it would automatically rollback transaction in case of any exception, even if we catch it and handle the failure inside transaction.
Upvotes: 3