Hibernate потокобезопасное идемпотентное upsert без обработки исключений ограничений? - PullRequest
5 голосов
/ 05 июня 2019

У меня есть код, который выполняет UPSERT, также известный как Merge . Я хочу очистить этот код, в частности, я хочу отойти от обработки исключений и уменьшить общую детализацию и сложность кода для такой простой операции. Требуется вставить каждый элемент, если он еще не существует:

public void batchInsert(IncomingItem[] items) {
    try(Session session = sessionFactory.openSession()) {
        batchInsert(session, items);
    }
    catch(PersistenceException e) {
        if(e.getCause() instanceof ConstraintViolationException) {
            logger.warn("attempting to recover from constraint violation");
            DateTimeFormatter dbFormat = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS");
            items = Arrays.stream(items).filter(item -> {
                int n = db.queryForObject("select count(*) from rets where source = ? and systemid = ? and updtdate = ?::timestamp",
                        Integer.class,
                        item.getSource().name(), item.getSystemID(), 
                        dbFormat.format(item.getUpdtDateObj()));
                if(n != 0) {
                    logger.warn("REMOVED DUPLICATE: " +
                            item.getSource() + " " + item.getSystemID() + " " + item.getUpdtDate());
                    return false;
                }
                else {
                    return true; // keep
                }
            }).toArray(IncomingItem[]::new);
            try(Session session = sessionFactory.openSession()) {
                batchInsert(session, items);
            }
        }
    }
}

Первоначальный поиск SO неудовлетворителен:

В вопросе Как сделать ON DUPLICATE KEY UPDATE в Spring Data JPA? , который был помечен как дубликат, я заметил этот интригующий комментарий: enter image description here

Это был тупик, так как я действительно не понимаю комментарий, несмотря на то, что он звучит как умное решение, и упоминание о "том же самом операторе SQL".

Другой многообещающий подход заключается в следующем: Запрос изменения Hibernate и Spring перед отправкой в ​​БД

ВКЛЮЧИТЬ КОНФЛИКТ НИЧЕГО / ВКЛЮЧИТЬ ОБНОВЛЕНИЕ КЛЮЧЕВОГО КЛЮЧА

Обе основные базы данных с открытым исходным кодом поддерживают механизм проталкивания идемпотентности в базу данных. В приведенных ниже примерах используется синтаксис PostgreSQL, но его можно легко адаптировать для MySQL.

Следуя принципам Запрос изменения в Hibernate и Spring Перед отправкой в ​​БД , Подключение к генерации запросов Hibernate и Как настроить StatementInspector в Hibernate? Я реализовал:

import org.hibernate.resource.jdbc.spi.StatementInspector;

@SuppressWarnings("serial")
public class IdempotentInspector implements StatementInspector {

    @Override
    public String inspect(String sql) {
        if(sql.startsWith("insert into rets")) {
            sql += " ON CONFLICT DO NOTHING";
        }
        return sql;
    }

}

с собственностью

        <prop key="hibernate.session_factory.statement_inspector">com.myapp.IdempotentInspector</prop>

К сожалению, это приводит к следующей ошибке при обнаружении дубликата:

Вызвано: org.springframework.orm.hibernate5.HibernateOptimisticLockingFailureException: Пакетное обновление вернуло неожиданное количество строк из обновления [0]; фактическая строка количество: 0; ожидается: 1; вложенное исключение org.hibernate.StaleStateException: пакетное обновление вернуло неожиданное количество строк из обновления [0]; фактическое количество строк: 0; ожидается: 1

Это имеет смысл, если вы думаете о том, что происходит под обложками: ON CONFLICT DO NOTHING заставляет вставлять ноль строк, но ожидается одна вставка.

Существует ли решение, которое позволяет выполнять потоковые безопасные параллельные идемпотентные вставки без исключений и не требует ручного определения всего оператора вставки SQL для выполнения Hibernate?

Мне кажется, что подходы, которые подталкивают дупчек к базе данных, - это путь к правильному решению.

ПОЯСНЕНИЯ Объекты IncomingItem, используемые методом batchInsert, происходят из системы, в которой записи являются неизменяемыми. При этом особом условии ON CONFLICT DO NOTHING ведет себя так же, как UPSERT, несмотря на возможную потерю N-го обновления .

Ответы [ 3 ]

3 голосов
/ 09 июня 2019

Краткий ответ - Hibernate не поддерживает его «из коробки» (что подтверждено гуру Hibernate в этом сообщении в блоге ). Возможно, в некоторых сценариях вы могли бы заставить его работать в некоторой степени с механизмами, которые вы уже описали, но простое использование нативных запросов напрямую выглядит для меня самым простым подходом для этой цели.

Более длинный ответ будет состоять в том, что было бы трудно поддержать его, учитывая все аспекты Hibernate, например ::

  • Что делать с экземплярами, для которых обнаружены дубликаты, поскольку они должны стать управляемыми после сохранения? Слить их в контекст постоянства?
  • Что делать с ассоциациями, которые уже были сохранены, какие каскадные операции применить к ним (persist / merge / нечто_new; или в этот момент уже слишком поздно принимать это решение)?
  • Возвращают ли базы данных достаточно информации из операций upsert, чтобы охватить все варианты использования (пропущенные строки; сгенерированные ключи для пропущенных в режимах пакетной вставки и т. Д.)
  • А как насчет @Audit объектов, созданных или обновленных, если обновлено, что изменилось?
  • Или управление версиями и оптимистическая блокировка (по определению, вы действительно хотите исключение в этом случае)?

Даже если бы Hibernate каким-то образом поддерживал его, я не уверен, что использовал бы эту функцию, если бы было слишком много предостережений, чтобы их не пропустить и принять во внимание.

Итак, эмпирическое правило, которому я следую, таково:

  • Для простых сценариев (которые большую часть времени): сохраняются + повторные попытки. Повторные попытки в случае определенных ошибок (по типу исключения или аналогичным) могут быть глобально сконфигурированы с использованием AOP-подобных подходов (аннотации, пользовательские перехватчики и тому подобное) в зависимости от того, какие платформы вы используете в своем проекте, и в любом случае это хорошая практика, особенно в распределенных средах. .
  • Для сложных сценариев и операций с высокой производительностью (особенно когда речь идет о пакетной обработке, очень сложных запросах и т. П.): Собственные запросы для максимального использования определенных функций базы данных.
0 голосов
/ 12 июня 2019

Обратите внимание, что "идемпотент" - это не то же самое, что "игнорирование конфликта".Последнее может привести к тому, что вторая запись в базу данных будет проигнорирована, даже если она действительно должна выполнить обновление в случае сбоя вставки.

Существует ли решение, позволяющеебезопасные параллельные идемпотентные вставки без исключений

Я бы сказал, что это, вероятно, даже теоретически невозможно без специальной поддержки СУБД, особенно «параллельной» части.Причина в том, что данные не станут фактически записанными и, вероятно, даже не будут «видны», пока транзакция не будет зафиксирована.Итак, что произойдет, если в транзакции A определено, что запись не существует, и INSERT сделано.Даже если этот INSERT будет немедленно и атомарно видим для других транзакций, параллельная транзакция B определит, что она должна выполнить UPDATE.Теперь, что если в более поздней транзакции A возникнет проблема, которая приведет к ее откату?INSERTED данные из транзакции A исчезают, а UPDATE транзакции B не найдет ни одной записи для обновления.

Это одна из причин, по которой «параллельная» часть не будет работать вообще, потому чтоне все РСУБД поддерживают некоторую атомарную UPSERT (или «игнорирование при конфликте»).

Однако, похоже, вы не против проиграть вторую запись (обновление) той же записи, потому чтоВы говорите об идемпотентности, подразумевая, что потенциал UPDATE фактически не изменит данные записи, если она уже существует.В этом случае «игнорирование конфликта» действительно эквивалентно идемпотентности.

Одним (очевидным?) «Решением» было бы использование некоторой явной блокировки (в базе данных) для взаимного исключения, т.е. транзакция A получаетблокировка, делает свое дело, а затем отпускает его снова.Транзакция B пытается получить блокировку, но будет заблокирована до завершения транзакции A.Это, однако, уменьшит или предотвратит параллелизм, особенно если вы обрабатываете много записей в одной транзакции.Кроме того, поскольку СУБД не знает о связи между блокировкой и записями, которые она защищает, блокировка носит рекомендательный характер, и каждому клиенту придется использовать одну и ту же схему блокировки.

Вы говорите, что хотели бы«протолкнуть идемпотентность вниз к базе данных».Если это не является строгим требованием, вы можете просто контролировать параллелизм в своем Java-коде;например, используя некоторую коллекцию с поддержкой параллелизма, где ваш код атомарно проверяет и вставляет идентификатор каждого элемента данных, который он собирается записать в СУБД.Если идентификатор уже находится в коллекции, пропустите элемент, иначе вставьте в БД.

0 голосов
/ 07 июня 2019

На основании вашего поста я предполагаю, что source, systemid и updtdate - это уникальный ключ.На основании этого.Я бы

  • получил бы список IncomingItem одним запросом.(Я предполагаю, что у вас нет 1 миллиона записей в этой БД)
  • сравните уникальный ключ с вашим списком и оставьте тот, который вы хотите вставить.
  • сохраните элементы

Какой-то псевдокод:

public void batchInsert(IncomingItem[] items) {
    //get all IncomingItem from the DB
    List<IncomingItem> incomingItems = //DB query findAll;
    List<IncomingItem> incomingItemsToSave = new ArrayList<>();
    //check your duplicates!
    for(IncomingItem incomingItem : incomingItems){
        Arrays.stream(items).filter(item -> {
            //compare unique key
            // ...  code here ...
            if(!same unique key){
                incomingItemsToSave.add(item);
            }
        });
    }

    try(Session session = sessionFactory.openSession()) {
        batchInsert(session, incomingItemsToSave);
    }
    catch(PersistenceException e) {

    }
}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...