Basically, I'm trying to figure out how to write the correct (or "write correctly"?) Transactional code when developing a REST service using Jax-RS and Spring. In addition, we use JOOQ to access data. But this does not have to be very relevant ...
Consider a simple model where we have some organizations that have the following fields: "id", "name", "code" . All this must be unique. There is also a status field.
At some point, the organization may be deleted. But we don’t want to delete the data at all, because we want to save it for analytics / maintenance purposes. Therefore, we simply set the status field of the organization 'REMOVED' .
Since we are not deleting the organization row from the table, we cannot simply put a unique constraint in the "name" column, because we can delete the organization and then create a new one with the same name. But suppose that the codes must be unique globally, so we have a unique restriction on the code column.
So, let's look at this simple example that creates an organization by doing some validation along the way.
Resource:
@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)); } }
The DAO service looks like this:
@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; } }
This raises some questions:
- Should I put the
@Transactional annotation in the Resource createOrganization method? Or should I create another service that is negotiating with the DAO and add @Transactional annotation to this method? Something else? - What happens if two users send a request simultaneously with the same
"code" field. Before the first transaction, transactions were successfully completed, so 409 requests will not be sent. Then the first transaction will be executed correctly, but the second will violate the DB constraint. This will throw a SQLException. How to gracefully handle this? I mean, I still want to show a good error message on the client side, stating that this name is already in use. But I can not parse SQLException or anything else. Can I? - Like the previous one, but this time the "name" is not unique. In this case, the second transaction will not violate any restrictions, which leads to the presence of two organizations with the same name, which violates our availability restrictions.
- Where I can see / study tutorials / code / etc, you consider excellent examples of how to write correct / reliable REST + DB code with complex work logic. Github / books / blogs, whatever. I tried to find something like this myselft, but most of the examples are simply focused on plumbing - add these libraries to maven, use these annotations, there is your simple CRUD, the end. They do not contain any transactional considerations at all. I.e.
UPDATE: I know about the isolation level and the usual error / separation matrix (dirty reads, etc.). The problem that I have is finding some sort of “production-ready” sample. Or a good book on the topic. I still don’t understand how to handle all errors correctly. I think I need to repeat a couple of times if the transaction failed .. and just throw some general error and implement a client that handles this. But do I really need to use SERIALIZABLE mode when I use range queries? Because it will greatly affect performance. But otherwise, how can I guarantee that the transaction will fail.
In any case, I decided that at the moment I need more time to learn about transactions and managing db in general to solve this problem ...