如何使用JAX-RS和Spring

时间:2016-08-19 16:57:52

标签: 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. 我应该在资源的@Transactional方法上添加createOrganization注释吗?或者我应该再创建一个与DAO对话的服务并将@Transactional注释放到它的方法中?还有别的吗?
  2. 如果两个用户同时发送具有相同"code"字段的请求,会发生什么情况。在提交第一个事务之前,检查成功通过,因此不会发送409个响应。比第一个事务将正确提交,但第二个事务将违反DB约束。这将抛出SQLException。如何优雅地处理?我的意思是我仍然希望在客户端显示好的错误消息,说该名称已被使用。但我无法真正解析SQLException或smth ..我可以吗?
  3. 与前一个类似,但这次“名字”并不是唯一的。在这种情况下,第二个交易不会违反任何约束,这会导致两个组织具有相同的名称,这违反了我们的商业约束。
  4. 我在哪里可以看到/学习教程/代码/等等,您可以考虑如何使用复杂的商务逻辑编写正确/可靠的REST + DB代码。 Github /书籍/博客,无论如何。我试图找到类似myselft的东西,但大多数例子只关注管道 - 将这些库添加到maven,使用这些注释,有简单的CRUD,结束。它们根本不包含任何交易条款。即。
  5. 更新: 我知道隔离级别和通常的error/isolation matrix(脏读等等)。我遇到的问题是找到一些“生产就绪”的样本来学习。或者关于某个主题的好书。我仍然没有真正得到如何正确处理所有错误..我想我需要重试几次,如果事务失败..而不是只是抛出一些通用错误和实现客户端,处理那个..但是做每当我使用范围查询时,我真的必须使用SERIALIZABLE模式吗?因为它会大大影响性能。但除此之外我怎么能保证交易失败..

    无论如何,我已经决定现在需要更多时间来了解交易和数据库管理以解决这个问题......

4 个答案:

答案 0 :(得分:1)

通常,在不谈论事务性的情况下,端点应该只从请求中获取参数并调用服务。它不应该做业务逻辑。

您的checkXXX方法似乎是业务逻辑的一部分,因为它们会针对特定于域的冲突引发错误。为什么不将它们放入Service中的一个方法,这是顺便说一下的方式?

//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块,它将抛出的异常转换为错误响应:

//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是你的朋友。隔离级别足够高。我认为&#39;可重复阅读&#39;在你的情况下是合适的。您的checkXXX方法将检测某些其他事务是否提交具有相同名称或代码的实体,并且它保证情况保持到时间“创建”#39;方法被执行。关于Spring和事务隔离级别还有一个useful read

答案 1 :(得分:1)

根据我的理解,处理数据库级事务的最佳方法是必须在dao层中有效地使用Spring的Isolation trnsaction。以下是您的案例中的行业标准编码样本...

{
  "error": {
    "message": "(#803) Cannot query users by their username (username1)",
    "type": "OAuthException",
    "code": 803
  }
}

如果我在这里错了,请掐我

答案 2 :(得分:0)

关注点分离:

  • Jax-rs资源(端点)层:只需处理请求,调用服务并将潜在异常包装在适当的响应代码中(只需捕获并手动换行或使用exception mapper)。
  • 服务/业务层:为每个工作单元公开一个事务方法,业务错误必须作为已检查的异常处理,运算方式为未选中(RuntimeException的子类)。
  • 数据访问层:只处理数据访问内容(即获取db上下文,执行查询并最终映射结果)。

我坚持一件事,拥有交易边界的好地方是定义业务方法的地方。交易范围必须是业务工作单元。

关于并发问题,有两种方法可以处理这种并发问题:悲观或乐观锁定。

  • 悲观:

    • 锁定
    • 做你的东西
    • 更新
    • 释放锁定
  • 乐观:

    • 检查版本
    • 做你的东西
    • 如果版本相同则更新,否则失败

悲观是一个关于可伸缩性和性能的问题,乐观的问题是你有时会通过向最终用户发送操作错误来结束。

我个人会乐观地锁定您的案例,JOOQ support it

答案 3 :(得分:-1)

首先,DAO层甚至不应该知道它是由REST Web服务提供的。务必分开责任。

将@Transactional保留在DAO上。如果您只发出一个语句而不是需要判断脏读是否正常。基本上,找出适用于您的应用程序的最低隔离级别。每个方法都将启动一个新的Transaction(除非从另一个已启动的方法调用),如果抛出任何异常,它将回滚任何调用。您可以在Controller中设置自定义ExceptionHandler来处理SQLDataIntegrityExceptions(就像您&#39;代码&#34;插入示例一样)。

使用涵盖(ID,名称,代码,状态)的聚合主键,这样您就可以拥有一个具有相同名称的组织,但其中一个将是&#34; CURRENT&#34;一个将被&#34;删除&#34;