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

标签 java spring rest transactions jax-rs

基本上,我试图了解在使用 Jax-RS 和 Spring 开发 REST 服务时如何编写正确(或“正确编写”?)事务代码。此外,我们正在使用 JOOQ 进行数据访问。但这应该不是很相关......
考虑一个简单的模型,我们有一些组织,它们有这些字段:“id”、“name”、“code”。所有这些都必须是唯一的。还有一个 status 字段。
组织可能会在某个时候被删除。但我们不想完全删除数据,因为我们想保存它以用于分析/维护目的。所以我们只需将组织的“状态”字段设置为'REMOVED'
因为我们没有从表中删除组织行,所以我们不能简单地将唯一约束放在“名称”列上,因为,我们可能会删除组织,然后创建一个具有相同名称的新组织。但是我们假设代码必须在全局范围内是唯一的,所以我们在 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. 我应该在 Resource 的 createOrganization 方法上添加 @Transactional 注释吗?或者我应该再创建一个与 DAO 对话的服务并将 @Transactional 注释添加到它的方法中吗?还有别的吗?
  2. 如果两个用户同时使用相同的"code" 字段发送请求会发生什么。在提交第一个事务之前,检查已成功通过,因此不会发送 409 响应。第一个事务将被正确提交,但第二个事务将违反数据库约束。这将抛出 SQLException。如何优雅地处理它?我的意思是我仍然想在客户端显示不错的错误消息,说该名称已被使用。但我不能真正解析 SQLException 或 smth.. 可以吗?
  3. 与上一个类似,但这次“名称”不是唯一的。在这种情况下,第二笔交易不会违反任何约束,这会导致两个组织同名,这违反了我们的业务约束。
  4. 我在哪里可以看到/学习教程/代码/等等,您认为这是关于如何使用复杂的业务逻辑编写正确/可靠的 REST+DB 代码的很好的例子。 Github/书籍/博客,随便什么。我试图自己找到类似的东西,但大多数示例只关注管道 - 将这些库添加到 Maven,使用这些注释,这是你的简单 CRUD,结束。它们根本不包含任何交易考虑。即

更新: 我知道隔离级别和通常的 error/isolation matrix (脏读等)。我遇到的问题是找到一些“生产就绪”的样本来学习。或者一本关于某个主题的好书。我仍然不明白如何正确处理所有错误。我想我需要重试几次,如果交易失败......而不是抛出一些一般性错误并实现客户端,处理那个......但是做每当我使用范围查询时,我真的必须使用 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);
}

我认为你的参数是字符串,但它们可以是任何东西。我不确定你想在服务层抛出 Responses.abortConflict 因为它似乎是一个 REST 概念,但如果你愿意,你可以为它定义自己的异常类型。

端点代码应该看起来像这样,但是,它可能包含额外的 try-catch block ,它将抛出的异常转换为错误响应:

//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,transaction isolation levels是你的 friend 。将隔离级别设置得足够高。我认为“可重复读取”适合您的情况。您的 checkXXX 方法将检测是否有其他事务提交了具有相同名称或代码的实体,并保证这种情况在执行“创建”方法时保持不变。再来一个useful read关于 Spring 和事务隔离级别。

关于java - 如何使用 JAX-RS 和 Spring 编写正确/可靠的事务代码,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/39044269/

相关文章:

java - Spring:在 Java 配置中定义自定义 @Transactional 行为

java - 如何正确使用Spring AOP来选择执行带有特定注解的方法?

java - 从第三方框架中过滤掉 log4j 消息?

php - API - 处理嵌套资源 (Laravel)

java - 如果未完成客户确认会怎样?

java - 如何使用正则表达式来检查字符串中的重复字符?

java - 查找/替换高亮替换词 Netbeans/Java

java - onAnimationEnd 监听器的可靠性如何?

asp.net - 网络 API : Basic Authentication or HMAC over SSL?

java - 自定义响应头 Jersey/Java