基本上,当使用Jax-RS和Spring开发REST服务时,我试图理解如何编写正确的(或"正确编写"?)事务代码。此外,我们还使用JOOQ进行数据访问。但这不应该是非常相关的…
考虑一个简单的模型,我们有一些组织,有这些领域:"id", "name", "code"
。所有的都必须是唯一的。还有一个status
域
组织可能会在某个时候被移除。但是我们不希望完全删除数据,因为我们希望保存它以供分析/维护。所以我们将组织的status字段设置为'REMOVED'
。
因为我们没有从表中删除组织行,所以我们不能简单地在"name"列上添加唯一约束,因为我们可能会删除组织,然后创建一个具有相同名称的新组织。但是让我们假设代码必须是全局唯一的,所以我们在code
列上有一个唯一的约束。
那么,让我们看看这个简单的例子,它创建组织,执行一些检查。
资源:
@Component
@Path("/api/organizations/{organizationId: [0-9]+}")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaTypeEx.APPLICATION_JSON_UTF_8)
public class OrganizationResource {
@Autowired
private OrganizationService organizationService;
@Autowired
private DtoConverter dtoConverter;
@POST
public OrganizationResponse createOrganization(@Auth Person person, CreateOrganizationRequest request) {
if (organizationService.checkOrganizationWithNameExists(request.name())) {
// this throws special Exception which is intercepted and translated to response with 409 status code
throw Responses.abortConflict("organization.nameExist", ImmutableMap.of("name", request.name()));
}
if (organizationService.checkOrganizationWithCodeExists(request.code())) {
throw Responses.abortConflict("organization.codeExists", ImmutableMap.of("code", request.code()));
}
long organizationId = organizationService.create(person.user().id(), request.name(), request.code());
return dtoConverter.from(organization.findById(organizationId));
}
}
DAO服务是这样的:
@Transactional(DBConstants.SOME_TRANSACTION_MANAGER)
public class OrganizationServiceImpl implements OrganizationService {
@Autowired
@Qualifier(DBConstants.SOME_DSL)
protected DSLContext context;
@Override
public long create(long userId, String name, String code) {
Organization organization = new Organization(null, userId, name, code, OrganizationStatus.ACTIVE);
OrganizationRecord organizationRecord = JooqUtil.insert(context, organization, ORGANIZATION);
return organizationRecord.getId();
}
@Override
public boolean checkOrganizationWithNameExists(String name) {
return checkOrganizationExists(Tables.ORGANIZATION.NAME, name);
}
@Override
public boolean checkOrganizationWithCodeExists(String code) {
return checkOrganizationExists(Tables.ORGANIZATION.CODE, code);
}
private boolean checkOrganizationExists(TableField<OrganizationRecord, String> checkField, String checkValue) {
return context.selectCount()
.from(Tables.ORGANIZATION)
.where(checkField.eq(checkValue))
.and(Tables.ORGANIZATION.ORGANIZATION_STATUS.ne(OrganizationStatus.REMOVED))
.fetchOne(DSL.count()) > 0;
}
}
这带来了一些问题:
- 我应该把
@Transactional
注释资源的createOrganization
方法?或者我应该再创建一个与DAO对话的服务,并将@Transactional注释放到它的方法中?别的吗? - 如果两个用户同时发送具有相同
"code"
字段的请求会发生什么?在提交第一个事务之前,检查已成功通过,因此不会发送409响应。第一个事务将被正确提交,但第二个事务将违反数据库约束。这将抛出SQLException。如何优雅地处理这种情况?我的意思是,我仍然希望在客户端显示很好的错误消息,说名称已经被使用了。但我无法解析SQLException之类的。我可以吗? - 与前一个类似,但这次"name"不是唯一的。在这种情况下,第二个事务将不会违反任何约束,从而导致有两个具有相同名称的组织,这违反了我们的业务约束。
- 我在哪里可以看到/学习教程/代码等。,你认为这是如何用复杂的业务逻辑编写正确/可靠的REST+DB代码的好例子。Github/书/博客,等等。我自己也试过找到类似的东西,但大多数例子只关注管道——将这些库添加到maven中,使用这些注释,这就是您的简单CRUD,结束。它们根本不包含任何事务考虑。例如
更新:我知道隔离级别和通常的错误/隔离矩阵(脏读取等)。我的问题是找到一些"生产就绪"的样本来学习。或者一本关于某个主题的好书。我仍然不知道如何正确处理所有的错误。我想我需要重试几次,如果交易失败…然后抛出一些通用错误并实现客户端,处理那个。但是,每当使用范围查询时,我真的必须使用SERIALIZABLE模式吗?因为它会极大地影响性能。否则我怎么能保证交易会失败呢?
无论如何,我已经决定,现在我需要更多的时间来学习事务和数据库管理一般解决这个问题…
一般来说,不讨论事务性,端点应该只从请求中获取参数并调用服务。它不应该做业务逻辑。
看起来你的checkXXX方法是业务逻辑的一部分,因为它们抛出关于域特定冲突的错误。为什么不把它们放到服务的一个方法中,顺便说一下,这个方法是事务性的?
//service code
public Organization createOrganization(String userId, String name, String code) {
if (this.checkOrganizationWithNameExists(request.name())) {
throw ...
}
if (this.checkOrganizationWithCodeExists(code)) {
throw ...
}
long organizationId = this.create(userId, name, code);
return dao.findById(organizationId);
}
作为你的参数是字符串,但他们可以是任何东西。我不确定你是否想要回复。因为它似乎是一个REST概念,但如果你愿意,你可以为它定义自己的异常类型。
端点代码应该是这样的,但是,它可能包含额外的try-catch块,将抛出的异常转换为错误响应:
//endpoint code
@POST
public OrganizationResponse createOrganization(@Auth Person person, CreateOrganizationRequest request) {
String code = request.code();
String name = request.name();
String userId = person.user().id();
return dtoConverter.from(organizationService.createOrganization(userId, name, code));
}
对于问题2和3,事务隔离级别是您的朋友。设置足够高的隔离级别。我认为"可重复阅读"对你来说是合适的。您的checkXXX方法将检测是否有其他事务提交具有相同名称或代码的实体,并且保证在执行'create'方法时情况保持不变。关于Spring和事务隔离级别的另一个有用的阅读。
根据我的理解,处理DB级事务的最佳方式必须在dao层中有效地使用Spring的隔离事务。以下是您的示例行业标准代码…
public interface OrganizationService {
@Retryable(maxAttempts=3,value=DataAccessResourceFailureException.class,backoff=@Backoff(delay = 1000))
public boolean checkOrganizationWithNameExists(String name);
}
@Repository
@EnableRetry
public class OrganizationServiceImpl implements OrganizationService {
@Transactional(isolation = Isolation.READ_COMMITTED)
@Override
public boolean checkOrganizationWithNameExists(String name){
//your code
return true;
}
}
如果我写错了请捏我一下
关注点分离:
- Jax-rs资源(端点)层:仅处理请求,调用服务并将潜在异常包装在适当的响应代码中(只需手动捕获和包装或使用异常映射器)。
- 服务/业务层:为每个工作单元公开一个事务方法,业务错误必须作为检查异常处理,操作错误必须作为未检查异常处理(
RuntimeException
的子类)。 - 数据访问层:只处理数据访问的东西(即获取数据库上下文,执行查询,最终映射结果)。
我坚持一件事,拥有事务边界的好地方是定义业务方法的地方。事务范围必须是一个业务工作单元。
关于并发问题,有两种方法来处理这种并发问题:悲观锁定或乐观锁定。- 悲观:
- 锁
- 做你的事
- <
- 释放锁/gh>
-
乐观:
- <
- 检查版本/gh>
- 做你的事
- 如果版本相同,则更新,否则失败
悲观是关于可伸缩性和性能的问题,乐观的问题是你有时会向最终用户发送一个操作错误。
我个人倾向于乐观锁定,JOOQ支持它
首先DAO层甚至不应该知道它是由REST web服务前端的。一定要分清责任。
在DAO上保留@Transactional。如果您只发出一条语句,那么您需要决定是否可以使用脏读取。基本上,找出应用程序的最低隔离级别是什么。每个方法都将启动一个新的事务(除非从已经启动了一个事务的另一个方法调用),如果抛出任何异常,它将回滚任何调用。你可以在你的控制器中设置一个自定义的ExceptionHandler来处理SQLDataIntegrityExceptions(就像你的"代码"插入示例)。
使用包含(id, name, code, status)的聚合主键,这样您就可以有一个名称相同的组织,但一个将是"CURRENT",一个将是"REMOVED"