如何使用JAX-RS和Spring编写正确/可靠的事务代码



基本上,当使用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;
    }
}

这带来了一些问题:

  1. 我应该把@Transactional注释资源的createOrganization方法?或者我应该再创建一个与DAO对话的服务,并将@Transactional注释放到它的方法中?别的吗?
  2. 如果两个用户同时发送具有相同"code"字段的请求会发生什么?在提交第一个事务之前,检查已成功通过,因此不会发送409响应。第一个事务将被正确提交,但第二个事务将违反数据库约束。这将抛出SQLException。如何优雅地处理这种情况?我的意思是,我仍然希望在客户端显示很好的错误消息,说名称已经被使用了。但我无法解析SQLException之类的。我可以吗?
  3. 与前一个类似,但这次"name"不是唯一的。在这种情况下,第二个事务将不会违反任何约束,从而导致有两个具有相同名称的组织,这违反了我们的业务约束。
  4. 我在哪里可以看到/学习教程/代码等。,你认为这是如何用复杂的业务逻辑编写正确/可靠的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"

相关内容

最新更新