Spring: 事务方法中未捕获的异常
Spring: Uncaught Exception in Transactional Method
我正在构建一个 RESTful API 并在我的 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;
}
}
可抛出的异常来自具有以下实现的 ProductService:
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 的调用无论如何都不会捕获异常,因此 DataIntegrityViolationException
冒泡到控制器。
我知道这是因为我将代码包装在事务中,因为删除了@Transactional "solves" 问题。我认为这与 Spring 使用代理将方法包装在事务中这一事实有关,因此控制器中的服务调用实际上并不是(直接)调用服务方法。尽管如此,我不明白为什么它在 ProductNotFoundException
.
工作正常时忽略 catch 分支以抛出 ProductAlreadyExistsException
我知道我还可以再次访问数据库,尝试按名称查找产品,如果找不到,我会尝试保存我的实体。但这会使一切变得更加低效。
我也可以在控制器层捕获 DataIntegrityViolationException
并将 ProductAlreadyExistsException
扔到那里,但我会在那里公开持久层的细节,这似乎不合适。
有没有办法像我现在尝试做的那样在服务层中处理所有这些?
P.S.: 将逻辑外包给一个新方法并在内部使用它 似乎 工作但只是因为调用 this 实际上不会被事务管理器代理拦截,因此实际上没有执行任何事务
要使其正常工作,您需要按如下方式更改 saveProduct
方法:
try {
return productRepository.saveAndFlush(product);
} catch (DataIntegrityViolationException ex) {
throw new ProductAlreadyExistsException(product.getName());
}
当您使用 save()
方法时,与保存操作关联的数据不会刷新到您的数据库,除非并且直到显式调用 flush()
或 commit()
方法制成。这会导致 DataIntegrityViolationException
比您预期的晚抛出,因此它不会被上面的 snipper 捕获。
另一方面,saveAndFlush()
方法会立即刷新数据。这应该触发预期的异常被立即捕获并重新抛出为 ProductAlreadyExistsException
.
我正在构建一个 RESTful API 并在我的 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;
}
}
可抛出的异常来自具有以下实现的 ProductService:
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 的调用无论如何都不会捕获异常,因此 DataIntegrityViolationException
冒泡到控制器。
我知道这是因为我将代码包装在事务中,因为删除了@Transactional "solves" 问题。我认为这与 Spring 使用代理将方法包装在事务中这一事实有关,因此控制器中的服务调用实际上并不是(直接)调用服务方法。尽管如此,我不明白为什么它在 ProductNotFoundException
.
ProductAlreadyExistsException
我知道我还可以再次访问数据库,尝试按名称查找产品,如果找不到,我会尝试保存我的实体。但这会使一切变得更加低效。
我也可以在控制器层捕获 DataIntegrityViolationException
并将 ProductAlreadyExistsException
扔到那里,但我会在那里公开持久层的细节,这似乎不合适。
有没有办法像我现在尝试做的那样在服务层中处理所有这些?
P.S.: 将逻辑外包给一个新方法并在内部使用它 似乎 工作但只是因为调用 this 实际上不会被事务管理器代理拦截,因此实际上没有执行任何事务
要使其正常工作,您需要按如下方式更改 saveProduct
方法:
try {
return productRepository.saveAndFlush(product);
} catch (DataIntegrityViolationException ex) {
throw new ProductAlreadyExistsException(product.getName());
}
当您使用 save()
方法时,与保存操作关联的数据不会刷新到您的数据库,除非并且直到显式调用 flush()
或 commit()
方法制成。这会导致 DataIntegrityViolationException
比您预期的晚抛出,因此它不会被上面的 snipper 捕获。
saveAndFlush()
方法会立即刷新数据。这应该触发预期的异常被立即捕获并重新抛出为 ProductAlreadyExistsException
.