Как добиться оптимистичной блокировки, когда не используются какие-либо связанные с JPA фреймворки? - PullRequest
0 голосов
/ 27 мая 2019

Я создал API, который переводит деньги между двумя аккаунтами. У нас есть fromAccountNumber, toAccountNumber, которые являются полями String и имеют значение BigDecimal Данные счета хранятся в базе данных H2 (в памяти).

Для достижения параллелизма я использовал оператор Select .. for update (этот запрос: GET_BY_ID_QUERY_FOR_TRANSACTION) при извлечении записей из БД.

public void transferMoney(String sender, String receiver, BigDecimal amount) throws TransferException {
        Connection conn = null;
        try {
            conn = H2DataBase.getConnection();

            checkIfAccountNumbersAreValid(sender, receiver);

            AccountDetailsDTO fromAccountDto = getAccountByNumber(conn, sender);
            AccountDetailsDTO toAccountDto = getAccountByNumber(conn, receiver);

            if (null != fromAccountDto && null != toAccountDto) {
                transfer(conn, fromAccountDto, toAccountDto, amount);
            } else {
                rollback(conn);
                throw new TransferException("Transfer Failed. Please check the account numbers.",
                        Response.Status.BAD_REQUEST);
            }
        } catch (SQLException e) {
            throw new TransferException("Transfer Failed !", Response.Status.INTERNAL_SERVER_ERROR);
        } catch (ValidationException e) {
            throw new TransferException(e.getMessage(), e.getStatus());
        } finally {
            closeConnection(conn);
        }
    }

    private void transfer(Connection conn, AccountDetailsDTO fromAccountDto, AccountDetailsDTO toAccountDto,
            BigDecimal transactionAmount) throws TransferException {

        PreparedStatement preparedStatementInsertToTransaction = null;
        PreparedStatement preparedStatementUpdateFromAccount = null;
        PreparedStatement preparedStatementUpdateToAccount = null;

        try {

            checkToAndFromAreDifferent(fromAccountDto, toAccountDto);
            checkAccountBalanceBeforeTransfer(fromAccountDto.getAccountBalance(), transactionAmount);

            fromAccountDto.setAccountBalance(fromAccountDto.getAccountBalance().subtract(transactionAmount));
            toAccountDto.setAccountBalance(toAccountDto.getAccountBalance().add(transactionAmount));

            preparedStatementInsertToTransaction = conn
                    .prepareStatement(DBQueryUtility.INSERT_INTO_TRANSACTION_TABLE_QUERY);
            preparedStatementInsertToTransaction.setString(1, fromAccountDto.getAccountNumber());
            preparedStatementInsertToTransaction.setString(2, toAccountDto.getAccountNumber());
            preparedStatementInsertToTransaction.setBigDecimal(3, transactionAmount);

            preparedStatementInsertToTransaction.executeUpdate();

            preparedStatementUpdateFromAccount = conn.prepareStatement(DBQueryUtility.UPDATE_BANK_ACCOUNT_TABLE_QUERY);
            preparedStatementUpdateFromAccount.setBigDecimal(1, fromAccountDto.getAccountBalance());
            preparedStatementUpdateFromAccount.setString(2, fromAccountDto.getAccountNumber());

            preparedStatementUpdateFromAccount.executeUpdate();

            preparedStatementUpdateToAccount = conn.prepareStatement(DBQueryUtility.UPDATE_BANK_ACCOUNT_TABLE_QUERY);
            preparedStatementUpdateToAccount.setBigDecimal(1, toAccountDto.getAccountBalance());
            preparedStatementUpdateToAccount.setString(2, toAccountDto.getAccountNumber());

            preparedStatementUpdateToAccount.executeUpdate();

            conn.commit();

        } catch (ValidationException e) {
            rollback(conn);
            throw new TransferException(e.getMessage(), Response.Status.BAD_REQUEST);
        } catch (RuntimeException | SQLException e) {
            rollback(conn);
            throw new TransferException("Transfer Failed", Response.Status.INTERNAL_SERVER_ERROR);
        } finally {
            closeConnection(conn);
            closePreparedStatement(preparedStatementInsertToTransaction, preparedStatementUpdateFromAccount,
                    preparedStatementUpdateToAccount);

        }
    }

    public void checkIfAccountNumbersAreValid(String sender, String receiver) throws ValidationException {
        if (null == sender || null == receiver || sender.isEmpty() || receiver.isEmpty()) {
            throw new ValidationException("Account details cannot be null or empty !", Response.Status.BAD_REQUEST);
        }
    }

    @Override
    public void checkToAndFromAreDifferent(AccountDetailsDTO fromAccountDto, AccountDetailsDTO toAccountDto)
            throws ValidationException {
        if (toAccountDto.getAccountNumber().equals(fromAccountDto.getAccountNumber())) {
            throw new ValidationException("To and From accounts cannot be same !", Response.Status.BAD_REQUEST);
        }
    }

    @Override
    public void checkAccountBalanceBeforeTransfer(BigDecimal accountBalance, BigDecimal amount)
            throws ValidationException {
        if (accountBalance.compareTo(BigDecimal.ZERO) < 0 || accountBalance.compareTo(amount) < 0) {
            throw new ValidationException("Low account balance. Cannot initiate transfer !",
                    Response.Status.BAD_REQUEST);
        }
    }

    public AccountDetailsDTO getAccountByNumber(Connection conn, String accountNumber) throws TransferException {
        AccountDetailsDTO detailsDTO = null;
        try {
            PreparedStatement preparedStatement = conn.prepareStatement(DBQueryUtility.GET_BY_ID_QUERY_FOR_TRANSACTION);
            preparedStatement.setString(1, accountNumber);

            ResultSet rs = preparedStatement.executeQuery();

            while (rs.next()) {
                detailsDTO = DBQueryUtility.extractAccountDtoFromResultSet(rs);
            }
        } catch (SQLException e) {
            throw new TransferException("Get by account number failed ! Try again.",
                    Response.Status.INTERNAL_SERVER_ERROR);
        }
        return detailsDTO;
    }

НО это пессимистическая блокировка, которая не идеальна. Мне нужно сделать ОПТИМАЛЬНУЮ блокировку здесь для лучшей производительности. Я проверил в интернете, как я могу это реализовать, но не получил ответа, специфичного для моего варианта использования.

Везде, где они используют JPA и его аннотации.

Я не должен использовать какой-либо фреймворк!

Как получить и реализовать оптимистическую блокировку для следующего кода?

...