最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

My Custom Repository returns a vavr Try. Why does Spring Data jdbc wrap it in another vavr.Try? - Stack Overflow

programmeradmin1浏览0评论

I'm using spring boot 3.4.3.

I am trying to figure out best practices on how to use Spring, and I want to be able to return errors as types instead of throwing exceptions. That is my premise. If it turns out it's impossible to do that with Spring, I will have to find a way to live with that. However, i have been able to read code in the actual framework that encourages that.

For example, the Transaction Advice does nicely catch transactional methods that return a Try and roll back the transaction if the returned Try instance is a Failure.

However I'm very surprised by the following behavior on the Custom repository fragments.

A little bit of code:

public interface OrderRepository extends ListCrudRepository<Order, Integer>, CustomOrderRepository {}

public interface CustomOrderRepository {
    public Try<String> tryUpdateStatus(Integer orderId, String newStatus);
    public String updateStatus(Integer orderId, String newStatus);
}

The implementation does not really matter for this. I can show the client code, though:

    public Try<String> outOfStock(Integer orderId) {
        return orderRepository.updateStatus(orderId, "OUT_OF_STOCK");
    }

When I run my tests, I find that my client code is receiving a Try<Try<String>>

It looks like Spring is wrapping my result in a Try.

Some of the code I have been able to find seems to confirm it:

In .springframework.data.repository.util.QueryExecutionConverters

    static {

        WRAPPER_TYPES.add(WrapperType.singleValue(Future.class));
        UNWRAPPER_TYPES.add(WrapperType.singleValue(Future.class));
        WRAPPER_TYPES.add(WrapperType.singleValue(ListenableFuture.class));
        WRAPPER_TYPES.add(WrapperType.singleValue(CompletableFuture.class));
        UNWRAPPER_TYPES.add(WrapperType.singleValue(ListenableFuture.class));
        UNWRAPPER_TYPES.add(WrapperType.singleValue(CompletableFuture.class));

        ALLOWED_PAGEABLE_TYPES.add(Slice.class);
        ALLOWED_PAGEABLE_TYPES.add(Page.class);
        ALLOWED_PAGEABLE_TYPES.add(List.class);
        ALLOWED_PAGEABLE_TYPES.add(Window.class);

        WRAPPER_TYPES.add(NullableWrapperToCompletableFutureConverter.getWrapperType());

        UNWRAPPERS.addAll(CustomCollections.getUnwrappers());

        CustomCollections.getCustomTypes().stream().map(WrapperType::multiValue).forEach(WRAPPER_TYPES::add);

        CustomCollections.getPaginationReturnTypes().forEach(ALLOWED_PAGEABLE_TYPES::add);

        if (VAVR_PRESENT) {

            // Try support
            WRAPPER_TYPES.add(WrapperType.singleValue(io.vavr.control.Try.class));
            EXECUTION_ADAPTER.put(io.vavr.control.Try.class, it -> io.vavr.control.Try.of(it::get));
        }
    }

If the VAVR library is detected, it will add an execution adapter to the .springframework.data.repository.core.support.QueryExecutionResultHandler by going through the conversionService. I find this extremely odd.

If I get it right, it will modify the interface of my custom method and change the type it returns?

Am I missing something. Maybe I'm misusing the Custom repository ?

Could an experienced spring-data developer help out a novice trying to get their bearings?

Thanks in advance!

NB: If you need more code I'll be happy to provide it.

Update

A commenter asked for the implementation of updateStatus:

@Repository
class CustomOrderRepositoryImpl implements CustomOrderRepository {
    private final JdbcTemplate jdbcTemplate;

    public CustomOrderRepositoryImpl(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }

    public String updateStatus(Integer orderId, String newStatus) {
        String sql = "UPDATE orders SET status = ? WHERE id = ?";
        var updated = jdbcTemplate.update(sql, newStatus, orderId);
        if (updated == 0) {
            throw ErrorContext.notFound("Order " + orderId + " not found");
        } else {
            return newStatus;
        }
    }

    public Try<String> tryUpdateStatus(Integer orderId, String newStatus) {
        return Try.of(() -> updateStatus(orderId, newStatus));
    }
}

Update 2 (Test code and test result)

Please not that this is not an issue with the test runtime, as I have the same problems in production code.

This is an integration test, running on a real database through and through.

@Subject(OrderRepository)
@SpringBootTest(classes = [SpringbootTemplateApplication])
@Transactional
class OrderRepositoryIntegrationSpec extends Specification{
    @Subject
    @Autowired
    private OrderRepository orderRepository


    def "It updates an order's status"() {
        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 == "OUT_OF_STOCK"
    }

    def "It updates an order's status using tryUpdateStatus"() {
        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.onFailure {
            assert false: "Unexpected failure: ${it}"
        }.onSuccess { status ->
            assert status == "OUT_OF_STOCK"
        }.success
    }
}

When this Specificaation class runs, The first test succeeds, but the second fails, with the following error:

Condition not satisfied:

status == "OUT_OF_STOCK"
|      |
|      false
Success(OUT_OF_STOCK)

Expected :OUT_OF_STOCK
Actual   :Success(OUT_OF_STOCK)

Update 3 - This is looking more and more like a bug

I have added a method in the base repository:

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);
}

And modified my test:


    def "It updates an order's status using tryUpdateStatus"() {
        given:
        def order = new Order(null, anyString(), "NEW", [])
        var saved = orderRepository.save(order)


        when:
        def attempt2 = orderRepository.tryUpdateStatus2(saved.id(), "OUT_OF_STOCK")

        then:
        attempt2.onFailure {
            assert false: "Unexpected failure: ${it}"
        }.onSuccess { success ->
            assert success
        }.success


        when:
        def attempt = orderRepository.tryUpdateStatus(saved.id, "OUT_OF_STOCK")

        then:
        attempt.onFailure {
            assert false: "Unexpected failure: ${it}"
        }.onSuccess { status ->
            assert status == "OUT_OF_STOCK" //FAILURE HERE
        }.success
    }

I'm not allowed to return Try from my custom repositories? If I do, I need to unwrap Try twice?

Update 4: Bug report and reproduction repo

I have created a reproduction github repo and a bug report in spring-data-commons.

Repo:

Issue report:

I'm using spring boot 3.4.3.

I am trying to figure out best practices on how to use Spring, and I want to be able to return errors as types instead of throwing exceptions. That is my premise. If it turns out it's impossible to do that with Spring, I will have to find a way to live with that. However, i have been able to read code in the actual framework that encourages that.

For example, the Transaction Advice does nicely catch transactional methods that return a Try and roll back the transaction if the returned Try instance is a Failure.

However I'm very surprised by the following behavior on the Custom repository fragments.

A little bit of code:

public interface OrderRepository extends ListCrudRepository<Order, Integer>, CustomOrderRepository {}

public interface CustomOrderRepository {
    public Try<String> tryUpdateStatus(Integer orderId, String newStatus);
    public String updateStatus(Integer orderId, String newStatus);
}

The implementation does not really matter for this. I can show the client code, though:

    public Try<String> outOfStock(Integer orderId) {
        return orderRepository.updateStatus(orderId, "OUT_OF_STOCK");
    }

When I run my tests, I find that my client code is receiving a Try<Try<String>>

It looks like Spring is wrapping my result in a Try.

Some of the code I have been able to find seems to confirm it:

In .springframework.data.repository.util.QueryExecutionConverters

    static {

        WRAPPER_TYPES.add(WrapperType.singleValue(Future.class));
        UNWRAPPER_TYPES.add(WrapperType.singleValue(Future.class));
        WRAPPER_TYPES.add(WrapperType.singleValue(ListenableFuture.class));
        WRAPPER_TYPES.add(WrapperType.singleValue(CompletableFuture.class));
        UNWRAPPER_TYPES.add(WrapperType.singleValue(ListenableFuture.class));
        UNWRAPPER_TYPES.add(WrapperType.singleValue(CompletableFuture.class));

        ALLOWED_PAGEABLE_TYPES.add(Slice.class);
        ALLOWED_PAGEABLE_TYPES.add(Page.class);
        ALLOWED_PAGEABLE_TYPES.add(List.class);
        ALLOWED_PAGEABLE_TYPES.add(Window.class);

        WRAPPER_TYPES.add(NullableWrapperToCompletableFutureConverter.getWrapperType());

        UNWRAPPERS.addAll(CustomCollections.getUnwrappers());

        CustomCollections.getCustomTypes().stream().map(WrapperType::multiValue).forEach(WRAPPER_TYPES::add);

        CustomCollections.getPaginationReturnTypes().forEach(ALLOWED_PAGEABLE_TYPES::add);

        if (VAVR_PRESENT) {

            // Try support
            WRAPPER_TYPES.add(WrapperType.singleValue(io.vavr.control.Try.class));
            EXECUTION_ADAPTER.put(io.vavr.control.Try.class, it -> io.vavr.control.Try.of(it::get));
        }
    }

If the VAVR library is detected, it will add an execution adapter to the .springframework.data.repository.core.support.QueryExecutionResultHandler by going through the conversionService. I find this extremely odd.

If I get it right, it will modify the interface of my custom method and change the type it returns?

Am I missing something. Maybe I'm misusing the Custom repository ?

Could an experienced spring-data developer help out a novice trying to get their bearings?

Thanks in advance!

NB: If you need more code I'll be happy to provide it.

Update

A commenter asked for the implementation of updateStatus:

@Repository
class CustomOrderRepositoryImpl implements CustomOrderRepository {
    private final JdbcTemplate jdbcTemplate;

    public CustomOrderRepositoryImpl(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }

    public String updateStatus(Integer orderId, String newStatus) {
        String sql = "UPDATE orders SET status = ? WHERE id = ?";
        var updated = jdbcTemplate.update(sql, newStatus, orderId);
        if (updated == 0) {
            throw ErrorContext.notFound("Order " + orderId + " not found");
        } else {
            return newStatus;
        }
    }

    public Try<String> tryUpdateStatus(Integer orderId, String newStatus) {
        return Try.of(() -> updateStatus(orderId, newStatus));
    }
}

Update 2 (Test code and test result)

Please not that this is not an issue with the test runtime, as I have the same problems in production code.

This is an integration test, running on a real database through and through.

@Subject(OrderRepository)
@SpringBootTest(classes = [SpringbootTemplateApplication])
@Transactional
class OrderRepositoryIntegrationSpec extends Specification{
    @Subject
    @Autowired
    private OrderRepository orderRepository


    def "It updates an order's status"() {
        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 == "OUT_OF_STOCK"
    }

    def "It updates an order's status using tryUpdateStatus"() {
        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.onFailure {
            assert false: "Unexpected failure: ${it}"
        }.onSuccess { status ->
            assert status == "OUT_OF_STOCK"
        }.success
    }
}

When this Specificaation class runs, The first test succeeds, but the second fails, with the following error:

Condition not satisfied:

status == "OUT_OF_STOCK"
|      |
|      false
Success(OUT_OF_STOCK)

Expected :OUT_OF_STOCK
Actual   :Success(OUT_OF_STOCK)

Update 3 - This is looking more and more like a bug

I have added a method in the base repository:

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);
}

And modified my test:


    def "It updates an order's status using tryUpdateStatus"() {
        given:
        def order = new Order(null, anyString(), "NEW", [])
        var saved = orderRepository.save(order)


        when:
        def attempt2 = orderRepository.tryUpdateStatus2(saved.id(), "OUT_OF_STOCK")

        then:
        attempt2.onFailure {
            assert false: "Unexpected failure: ${it}"
        }.onSuccess { success ->
            assert success
        }.success


        when:
        def attempt = orderRepository.tryUpdateStatus(saved.id, "OUT_OF_STOCK")

        then:
        attempt.onFailure {
            assert false: "Unexpected failure: ${it}"
        }.onSuccess { status ->
            assert status == "OUT_OF_STOCK" //FAILURE HERE
        }.success
    }

I'm not allowed to return Try from my custom repositories? If I do, I need to unwrap Try twice?

Update 4: Bug report and reproduction repo

I have created a reproduction github repo and a bug report in spring-data-commons.

Repo: https://github/luismunizsparkers/spring-data-jdbc-try

Issue report: https://github/spring-projects/spring-data-commons/issues/3257

Share Improve this question edited Mar 20 at 10:20 Luis Muñiz asked Mar 19 at 9:33 Luis MuñizLuis Muñiz 4,8491 gold badge30 silver badges46 bronze badges 12
  • 1 Where is the implementation of the updateStatus method. I doubt the converters in Spring are the problem, what they do is return the result of a query method wrapped in a Try if the response asks for it. It would also be interesting to see what your test does, do you use the actual repository or are you mocking things? – M. Deinum Commented Mar 19 at 10:51
  • OK; I can add the implementation but it's nothing special. – Luis Muñiz Commented Mar 19 at 13:14
  • Could you elaborate on what you mean by "the response asks for it"? I feel that there is a thing i'm missing there, that there is a protocol i'm not following. Obviously if me CustomOrderRepository interface returns a String, Spring can't be breaking interfaces willi-nilly. So maybe I'm not understanding some generated magic that Spring is doing. – Luis Muñiz Commented Mar 19 at 13:21
  • 1 I actually hoped you would add the full class that contains the updateStatus method. Spring Data uses reflection and default ways of executing a query. It will execute a query and based on the type requested Try in this case it will wrap the result of the query in a Try. So what I suspect what is happening is that your custom method is not being excluded due to some missing annotation like @NoRepositoryBean or something like that. – M. Deinum Commented Mar 19 at 14:08
  • 1 Looking at the reference documentation of Spring Data JDBC it only mentions support for Vavr collections not for other types, although that support is in the common part of Spring Data and I know for a fact that it works with JPA. So this might simply be a case of not actually being supported by Spring Data JDBC? – M. Deinum Commented Mar 20 at 6:53
 |  Show 7 more comments

1 Answer 1

Reset to default 1

I'm answering this myself with a workaround until the issue I created in the spring-data-commons project is closed. It has currently (2025-03-24) been assigned and has not yet been handled, one way or another.

This is the issue:

https://github/spring-projects/spring-data-commons/issues/3257

In the meantime, I have created a class that will unwrap calls to repositories if they are returning Try<Try<T>> :

public class QueryExecutionConverters {
    /**
     * Temporary workaround to issue https://github/spring-projects/spring-data-commons/issues/3257
     * (Anomalous behavior of spring-data custom repositories returning io.vavr.control.Try)
     *
     * @param resultFromSpringDataRepository potentially actually a Try<Try<T>>
     * @return unwraps the value if it is a Try
     * @param <T> The expected type of the value
     */
    public static <T> Try<T> fixbug(Try<T> resultFromSpringDataRepository) {
        if (resultFromSpringDataRepository.isSuccess()) {
            @SuppressWarnings("rawtypes")
            Try typeLess= resultFromSpringDataRepository;
            Object val = typeLess.get();
            if (Try.class.isAssignableFrom(val.getClass())) {
                //noinspection unchecked
                return (Try<T>) val;
            } else {
                return resultFromSpringDataRepository;
            }
        } else {
            return resultFromSpringDataRepository;
        }
    }
}

This is how you'd use it, in a transactional facade service accessing a repository:

    public Try<String> outOfStock(Long orderId) {
        return QueryExecutionConverters.fixbug(
           orderRepository.tryUpdateStatus(orderId, "OUT_OF_STOCK")
        );
    }

发布评论

评论列表(0)

  1. 暂无评论