春季:事务方法中未捕获的异常 [英] Spring: Uncaught Exception in Transactional Method

查看:69
本文介绍了春季:事务方法中未捕获的异常的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我正在构建RESTful API,并且在ProductController中具有以下更新方法:

I'm building a RESTful API and have the following update method in my ProductController:

@Slf4j
@RestController
@RequiredArgsConstructor
public class ProductController implements ProductAPI {

    private final ProductService productService;

    @Override
    public Product updateProduct(Integer id, @Valid UpdateProductDto productDto) throws ProductNotFoundException,
            ProductAlreadyExistsException {

        log.info("Updating product {}", id);
        log.debug("Update Product DTO: {}", productDto);

        Product product = productService.updateProduct(id, productDto);

        log.info("Updated product {}", id);
        log.debug("Updated Product: {}", product);

        return product;
    }

}

throwable异常来自具有以下实现的ProductService:

The throwable exceptions come from the ProductService which has the following implementation:

package com.example.ordersapi.product.service.impl;

import com.example.ordersapi.product.api.dto.CreateProductDto;
import com.example.ordersapi.product.api.dto.UpdateProductDto;
import com.example.ordersapi.product.entity.Product;
import com.example.ordersapi.product.exception.ProductAlreadyExistsException;
import com.example.ordersapi.product.exception.ProductNotFoundException;
import com.example.ordersapi.product.mapper.ProductMapper;
import com.example.ordersapi.product.repository.ProductRepository;
import com.example.ordersapi.product.service.ProductService;
import lombok.RequiredArgsConstructor;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;

@Service
@RequiredArgsConstructor
public class ProductServiceImpl implements ProductService {

    private final ProductRepository productRepository;
    private final ProductMapper productMapper;

    @Override
    public Set<Product> getAllProducts() {
        return StreamSupport.stream(productRepository.findAll().spliterator(), false)
                .collect(Collectors.toSet());
    }

    @Override
    public Product getOneProduct(Integer id) throws ProductNotFoundException {
        return productRepository.findById(id)
                .orElseThrow(() -> new ProductNotFoundException(id));
    }

    @Override
    public Product createProduct(CreateProductDto productDto) throws ProductAlreadyExistsException {
        Product product = productMapper.createProductDtoToProduct(productDto);
        Product savedProduct = saveProduct(product);

        return savedProduct;
    }

    private Product saveProduct(Product product) throws ProductAlreadyExistsException {
        try {
            return productRepository.save(product);
        } catch (DataIntegrityViolationException ex) {
            throw new ProductAlreadyExistsException(product.getName());
        }
    }

    /**
     * Method needs to be wrapped in a transaction because we are making two database queries:
     *  1. Finding the Product by id (read)
     *  2. Updating found product (write)
     *
     *  Other database clients might perform a write operation over the same entity between our read and write,
     *  which would cause inconsistencies in the system. Thus, we have to operate over a snapshot of the database and
     *  commit or rollback (and probably re-attempt the operation?) depending if its state has changed meanwhile.
     */
    @Override
    @Transactional
    public Product updateProduct(Integer id, UpdateProductDto productDto) throws ProductNotFoundException,
            ProductAlreadyExistsException {

        Product foundProduct = getOneProduct(id);
        boolean productWasUpdated = false;

        if (productDto.getName() != null && !productDto.getName().equals(foundProduct.getName())) {
            foundProduct.setName(productDto.getName());
            productWasUpdated = true;
        }

        if (productDto.getDescription() != null && !productDto.getDescription().equals(foundProduct.getDescription())) {
            foundProduct.setDescription(productDto.getDescription());
            productWasUpdated = true;
        }

        if (productDto.getImageUrl() != null && !productDto.getImageUrl().equals(foundProduct.getImageUrl())) {
            foundProduct.setImageUrl(productDto.getImageUrl());
            productWasUpdated = true;
        }

        if (productDto.getPrice() != null && !productDto.getPrice().equals(foundProduct.getPrice())) {
            foundProduct.setPrice(productDto.getPrice());
            productWasUpdated = true;
        }

        Product updateProduct = productWasUpdated ? saveProduct(foundProduct) : foundProduct;

        return updateProduct;
    }

}

因为我在数据库中将NAME列设置为UNIQUE,所以当使用现有名称发布更新时,存储库保存方法将抛出 DataIntegrityViolationException .在createProduct方法中,它工作正常,但在updateProduct中,无论如何,对私有方法saveProduct的调用都不会捕获到Exception,因此 DataIntegrityViolationException 会冒泡到Controller.

Because I've set the NAME column as UNIQUE in my database, when an update is issued with an already existing name, the repository save method will throw a DataIntegrityViolationException. In the createProduct method it works fine, but in the updateProduct, the call to the private method saveProduct doesn't catch the Exception no matter what and thus the DataIntegrityViolationException bubbles to the Controller.

我知道这是因为我将代码包装在事务中,因为删除了@Transactional可以解决"问题.我认为这与Spring使用Proxy将方法包装在事务中有关,因此控制器中的服务调用实际上并未(直接)调用service方法.即使如此,我也不明白为什么它对 ProductNotFoundException 正常工作时会忽略catch分支以抛出 ProductAlreadyExistsException .

I know it is because I'm wrapping the code in a transaction because removing the @Transactional "solves" the problem. I think it has something to do with the fact that Spring uses a Proxy to wrap the method in a transaction and thus the service call in the controller isn't actually calling the service method (directly). Even though, I don't understand why does it ignore the catch branch to throw the ProductAlreadyExistsException when it works fine for the ProductNotFoundException.

我知道我也可以再次访问数据库,尝试按名称查找产品,如果不提供产品,我将尝试将实体保存回去.但这会使所有事情变得效率低下.

I know I could also make another trip to the database, try to find a product by name and in case of absence would I try to save my entity back. But that would make everything even more inefficient.

我还可以在控制器层中捕获 DataIntegrityViolationException 并在其中抛出 ProductAlreadyExistsException ,但是我将从那里的持久层公开细节,这似乎不合适

I can also catch the DataIntegrityViolationException in the controller layer and throw the ProductAlreadyExistsException there but I would be exposing details from the persistence layer there, which doesn't seem appropriate.

就像我现在正在尝试的那样,是否可以在服务层中处理所有这些问题?

Is there a way to handle it all in the service layer, as I'm trying to do now?

PS:将逻辑外包给新方法并在内部使用它似乎可以正常工作,但这仅仅是因为对 this 的调用实际上不会被事务管理器截获代理,因此实际上未执行任何交易

P.S.: Outsourcing the logic to a new method and use it internally seems to work but only because calls to the this won't actually be intercepted by the Transaction Manager proxy and thus no transaction is actually performed

推荐答案

要使其正常运行,您需要更改 saveProduct 方法,如下所示:

To make it work you would need to change saveProduct method as follows:

        try {
            return productRepository.saveAndFlush(product);
        } catch (DataIntegrityViolationException ex) {
            throw new ProductAlreadyExistsException(product.getName());
        }

当您使用 save()方法时,与保存操作关联的数据将不会刷新到您的数据库中,除非并且直到显式调用 flush()为止或 commit()方法.这会导致 DataIntegrityViolationException 在您期望的以后被抛出,因此它不会在上面的快照程序中被捕获.

When you use the save() method, the data associated with the save operation will not be flushed to your database unless and until an explicit call to flush() or commit() method is made. That causes DataIntegrityViolationException being thrown later that you would expect and therefore it isn't beeing catched in the snipper above.

saveAndFlush()方法立即刷新数据.那应该立即触发预期的异常,然后将其重新抛出为 ProductAlreadyExistsException .

saveAndFlush() method on the other hand, flushes the data immediately. That should trigger expected exception being catched immidietely and re-throwed as ProductAlreadyExistsException.

这篇关于春季:事务方法中未捕获的异常的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆