We are using a Spring Boot application 3.4.3.
These are the dependencies:
// include::build.gradle[tag=dependencies]
dependencies {
implementation 'org.apache.groovy:groovy'
implementation 'org.springframework.boot:spring-boot-starter-data-jdbc'
implementation 'org.springframework.boot:spring-boot-starter-data-rest'
implementation 'org.liquibase:liquibase-core'
implementation "io.vavr:vavr:${vavrVersion}"
runtimeOnly 'com.h2database:h2'
implementation "jakarta.validation:jakarta.validation-api:${jakartaValidationVersion}"
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation "org.spockframework:spock-core:${spockVersion}"
testImplementation "org.spockframework:spock-spring:${spockVersion}"
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}
This is the application configuration:
// include::src/main/resources/application.yml[]
spring:
application:
name: spring-data-jdbc-try
liquibase:
change-log: classpath:db/changelog/db.changelog-master.xml
datasource:
url: jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
driver-class-name: org.h2.Driver
username: sa
password:
#debug: true
Spring-data-jdbc custom repositories returning io.vavr.control.Try
are not working as expected (or as I am expecting). This is the custom repository fragment:
// include::src/main/java/com/example/spring_data_jdbc_try/order/repository/CustomOrderRepository.java[]
package com.example.spring_data_jdbc_try.order.repository;
import io.vavr.control.Try;
public interface CustomOrderRepository {
public Try<Boolean> tryUpdateStatus(Integer orderId, String newStatus);
public Boolean updateStatus(Integer orderId, String newStatus);
}
The contract of the method Try<Boolean> tryUpdateStatus
is to return a Try
object that contains the result of the update operation. However, when the method is called, it returns a Try<Try<Boolean>>
.
This is the implementation of the custom repository fragment:
// include::src/main/java/com/example/spring_data_jdbc_try/order/repository/CustomOrderRepositoryImpl.java[]
package com.example.spring_data_jdbc_try.order.repository;
import io.vavr.control.Try;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;
@Repository
class CustomOrderRepositoryImpl implements CustomOrderRepository {
private final JdbcTemplate jdbcTemplate;
public CustomOrderRepositoryImpl(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
public Boolean updateStatus(Integer orderId, String newStatus) {
String sql = "UPDATE orders SET status = ? WHERE id = ?";
var updated = jdbcTemplate.update(sql, newStatus, orderId);
if (updated == 0) {
throw new RuntimeException("Order " + orderId + " not found");
} else {
return true;
}
}
public Try<Boolean> tryUpdateStatus(Integer orderId, String newStatus) {
return Try.of(() -> updateStatus(orderId, newStatus));
}
}
This fragment provides 2 versions of the functionality:
-
tryUpdateStatus
- which returns aTry<Boolean>
object -
updateStatus
- which returns aBoolean
object
tryUpdateStatus
just invokes the updateStatus
method and wraps the result in a Try
object.
In contradiction with the behaviour of placing the method in the custom fragment, when we add the signature in the BASE repository, it works as expected. This is the signature of the repository method:
// include::src/main/java/com/example/spring_data_jdbc_try/order/repository/OrderRepository.java[]
package com.example.spring_data_jdbc_try.order.repository;
import com.example.spring_data_jdbc_try.order.data.Order;
import io.vavr.control.Try;
import org.springframework.data.jdbc.repository.query.Modifying;
import org.springframework.data.jdbc.repository.query.Query;
import org.springframework.data.repository.ListCrudRepository;
public interface OrderRepository extends ListCrudRepository<Order, Integer>, CustomOrderRepository {
@Modifying
@Query("UPDATE orders SET status = :newStatus WHERE id = :orderId")
public Try<Boolean> tryUpdateStatus2(Integer orderId, String newStatus);
}
It’s the same signature as the one in the custom fragment, but this time it is placed in the base repository. and uses the @Query
annotation so that spring-data-jdbc can generate the implementation for us.
I have provided an integration test that exposes the weird behaviour:
// include::src/test/groovy/com/example/spring_data_jdbc_try/order/repository/OrderRepositoryIntegrationSpec.groovy[]
package com.example.spring_data_jdbc_try.order.repository
import com.example.spring_data_jdbc_try.SpringDataJdbcTryApplication
import com.example.spring_data_jdbc_try.order.data.Order
import io.vavr.control.Try
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.transaction.annotation.Transactional
import spock.lang.Specification
import spock.lang.Subject
@Subject(OrderRepository)
@SpringBootTest(classes = [SpringDataJdbcTryApplication])
@Transactional
class OrderRepositoryIntegrationSpec extends Specification {
@Subject
@Autowired
private OrderRepository orderRepository
def "It succeeds to update an order's status if the custom repository method returns a Boolean"() {
given:
def order = new Order(null, anyString(), "NEW")
var saved = orderRepository.save(order)
when:
def newStatus = orderRepository.updateStatus(saved.id(), "OUT_OF_STOCK")
then:
newStatus
}
def "It fails to correctly return the type of a CUSTOM repository method that returns a Try instance. Instead returns a Try<Try<?>>"() {
given:
def order = new Order(null, anyString(), "NEW")
var saved = orderRepository.save(order)
when:
def attempt = orderRepository.tryUpdateStatus(saved.id(), "OUT_OF_STOCK")
then:
attempt.success
Try.isAssignableFrom(attempt.class)
def wrappedValueShouldBeBoolean = attempt.get() as Try// this should be a Boolean, but is a Try<Boolean>
Try.isAssignableFrom(wrappedValueShouldBeBoolean.class)
wrappedValueShouldBeBoolean.success
wrappedValueShouldBeBoolean.get() == true
}
def "It correctly returns the type of a BASE repository method that returns a Try instance."() {
given:
def order = new Order(null, anyString(), "NEW")
var saved = orderRepository.save(order)
when:
def attempt = orderRepository.tryUpdateStatus2(saved.id(), "OUT_OF_STOCK")
then:
attempt.success
Try.isAssignableFrom(attempt.class)
def wrappedValueShouldBeBoolean = attempt.get()
wrappedValueShouldBeBoolean.class == Boolean
wrappedValueShouldBeBoolean == true
}
static String anyString() {
UUID.randomUUID().toString()
}
}
Both methods have the same type signature, and should behave the same. However, the method in the custom repository fragment returns a Try<Try<Boolean>>
object, while the method in the base repository returns a Try<Boolean>
object.
This looks like a bug, or I have failed in finding any documentation explaining hwo to use it.