JPA - ошибка дублирования записи merge () - PullRequest
0 голосов
/ 04 сентября 2018

У меня есть три таблицы Account, AccountStatus и AccountStatusCodes. AccountStatusCodes - это главная таблица, в которой есть фиксированные коды статуса учетной записи. Таблица счетов будет иметь другой счет в списке. Таблица AccountStatus - это своего рода таблица истории, когда пользователь выполняет какое-либо действие с учетной записью, старый статус учетной записи обновляется с помощью флага N и будет добавлен новый статус учетной записи. Таким образом, для каждой операции в учетной записи состояние учетной записи будет вести историю с отметкой времени, идентификатором пользователя, который обновил состояние, и флагом Y / N.

Взаимосвязь - отношение многие ко многим из таблицы Account в код AccountStatusCodes нарушается следующим образом 1. Одна учетная запись может иметь несколько AccountStatus - Account -> один ко многим -> AccountStatus 2. Один AccountStatusCodes может иметь несколько AccountStatus - AccountStatusCodes -> один ко многим -> AccountStatus

Код сущности JPA - фактический код не разрешен для совместного использования, следовательно, общий доступ к изменяемому коду объясняет сценарий.

Класс: AccountEntity

@DynamicUpdate(value = true)
public class AccountEntity{

    private Long accountKey;

    //Skipped other variables and getter setters
}

Класс: AccountStatusEntity

public class AccountStatusEntity{


    @Id
    @SequenceGenerator(name = "ACCOUNT_STATUSES_DOCUMENT_STATUSKEY_GENERATOR" , sequenceName = "AS_SEQ")
    @GeneratedValue(generator = "ACCOUNT_STATUSES_DOCUMENT_STATUSKEY_GENERATOR")
    @Column(name = "ACCOUNT_STATUS_KEY" , unique = true , nullable = false , precision = 10)
    private Integer accountStatusKey;

    @ManyToOne
    @JoinColumn(name = "ACCOUNT_STATUS_CODE_KEY" , nullable = false)
    private AccountStatusCodeEntity accountStatusCodeEntity;


    @ManyToOne
    @JoinColumn(name = "ACCOUNT_KEY" , nullable = false)
    private AccountEntity accountEntity;

    @Column(name = "CURRENT_STATUS_IND" , nullable = false , length = 1)
    private String currentStatusInd;

    //skipped other variables and getter setters
}

Класс: AccountStatusCodeEntity

public class AccountStatusCodeEntity{


    @Id
    @Column(name = "ACCOUNT_STATUS_CODE_KEY")
    @GeneratedValue(generator = "ASC_SEQ")
    @SequenceGenerator(name = "ASC_SEQ", sequenceName = "ASC_SEQ")
    private Integer accountStatusCodeKey;

    @OneToMany(mappedBy = "accountStatusEntity")
    private List<AccountStatusEntity> accountStatuseEntitiess;

}

В приложении каждый пользователь выполняет некоторую операцию с учетной записью, и каждый раз, когда статус учетной записи увеличивается до следующего AccountStatusCode, он сохраняет историю в таблице AccountStatusEntity, изменяя существующее состояние на флаг N и вставляя новый статус с отметкой времени, идентификатором пользователя и флаг Y.

Таким образом, с помощью @Transaction будут выполняться две операции с БД, во-первых, обновить старое состояние на N и вставить новое состояние с Y.

Метод, который делает это, имеет следующий код.

private void moveAccountStatus(final Long accountKey, final String loggedInUserID, final Integer currentStatus,
                                    final Integer nextStatus) {

        //Search existing account status with accountKey 
        // here I have skipped the code which will pull latest status entity from the history table based on date
        final AccountStatusEntity accountStatusEntity =
                accountDAO.findAccountStatusByAccountKey(accountKey);

        AccountEntity accountEntity;
        AccountStatusEntity newAccountStatusEntity;

        if (accountStatusEntity != null) {


            accountEntity = accountDAO.findAccountByAccountKey(accountKey);
            accountStatusEntity.setCurrentStatusInd(Constants.NO);
            accountStatusEntity.setModifiedBy(loggedInUserID);
            accountStatusEntity.setModifiedTs(new Date());
            accountStatusEntity.setAccountEntity(accountEntity);

            //The update method here is calling the JPA merge() to update the records in the table.
            accountDAO.update(accountStatusEntity);


            //Create new object of AccountStatusEntity to insert new row with the flag Y
            newAccountStatusEntity = new AccountStatusEntity();

            //Set the next status
            newAccountStatusEntity.setAccountStatusCodeEntity(
                    (AccountStatusCodeEntity) accountDAO.getById(AccountStatusCodeEntity.class, nextStatus));

            newAccountStatusEntity.setCurrentStatusInd(Constants.YES);
            newAccountStatusEntity.setCreatedBy(loggedInUserID);
            newAccountStatusEntity.setCreatedTs(new Date());
            newAccountStatusEntity.setAccountEntity(accountEntity);

            //The create() method is also calling the JPA merge() method. The Id is null hence it will consider a insert statement and will insert a new record.
            accountDAO.create(newAccountStatusEntity);
    }
}

Этот процесс отлично работает на 99% в Production, но иногда этот метод создает дублирующую запись в таблице AccountStatusEntity с той же отметкой времени, идентификатором пользователя и флагом Y. Метод не обновил запись с флагом N или из-за некоторой проблемы старая запись также обновляется с флагом Y.

Table: AccountStatus
___________________________________________________________________________________________________________________
accountStatusKey |  accountKey  |   accountStatusCodeKey    |   currentStatusInd    |   Created_TS  |   Created_BY
___________________________________________________________________________________________________________________
                 |              |                           |                       |               |
    1            |       5      |           3               |           Y           |       A       |   4/9/2018   
    2            |       5      |           3               |           Y           |       A       |   4/9/2018
___________________________________________________________________________________________________________________

В приведенной выше таблице показаны записи, которые создаются после выполнения метода. AccountStatusKey 1 должен иметь accountStatusCodeKey 2 (старый код состояния 2, а следующий код состояния 3) и currentStatusInd N. Но каким-то образом метод слияния вставляет сюда две записи.

Одно решение, которое я могу сделать, чтобы создать уникальное ограничение для столбца, чтобы избежать этой ситуации, но я просто хотел знать, почему метод слияния JPA создает эту проблему. Другое решение, которое я не пробовал, - это использовать метод persist () JPA во время вставки вместо merge ().

Эту проблему трудно воспроизвести в среде разработки, так как она работает 99% времени. Кроме того, пользователи сообщают об этой проблеме очень поздно, поэтому не могут отследить файлы журнала. Согласно журналам в среде dev, при выполнении транзакции JPA сначала вставляется оператор вставки, а затем в файл журнала записывается запрос на обновление. Я не уверен, как следует порядок выписки в случае транзакции.

Я знаю, что вопрос слишком длинный, но я просто хотел дать точную информацию для понимания этой проблемы. ТИА.

Ответы [ 2 ]

0 голосов
/ 11 июля 2019

Это была известная проблема в старых версиях Hibernate. Мое приложение использовало hibernate версию 4.2.8.Final.

Ссылка на сообщение о проблеме - https://hibernate.atlassian.net/browse/HHH-6776

Эта проблема была исправлена ​​в 5.0.8. Окончательная версия. Я обновил свои зависимости maven и буду продолжать тестирование.

Ссылка: https://hibernate.atlassian.net/browse/HHH-5855

Больше информации об этой проблеме -

https://javacodinggeeks.blogspot.com/2015/05/hibernate-inserts-duplicate-child-on.html

Hibernate вставляет дубликаты в коллекцию @OneToMany

0 голосов
/ 11 декабря 2018

Я изменил код, и вместо непосредственного обновления дочерних записей я создаю и обновляю дочернюю сущность, добавляю ее в список родительских сущностей и прошу JPA сохранить или обновить. Таким образом, на основе идентификатора либо он равен NULL, либо имеет действительный идентификатор, родительский объект решает, добавлять или обновлять дочерний элемент. Я переместил свой код в среду Prod, и в последние 2-3 недели я не вижу проблем с повторяющимися строками.

Я буду держать вас в курсе, если снова увижу этот вопрос. Спасибо!

...