2017-02-08 37 views
1

Итак, у меня есть такой сценарий, когда мне нужно взять запись заголовка, удалить данные для него, а затем повторно создать детали по-другому. Обновление деталей было бы слишком сложной задачей.Удалить, затем создать записи, вызывает дублирующее нарушение ключа с помощью Spring Data JPA

я в основном имеют:

@Transactional 
public void create(Integer id, List<Integer> customerIDs) { 

    Header header = headerService.findOne(id); 
    // header is found, has multiple details 

    // Remove the details 
    for(Detail detail : header.getDetails()) { 
     header.getDetails().remove(detail); 
    } 

    // Iterate through list of ID's and create Detail with other objects 
    for(Integer id : customerIDs) { 
     Customer customer = customerService.findOne(id); 

     Detail detail = new Detail(); 
     detail.setCustomer(customer); 

     header.getDetails().add(detail); 
    } 

    headerService.save(header); 
} 

Теперь база данных имеет ограничение вроде следующего:

Header 
================================= 
ID, other columns... 

Detail 
================================= 
ID, HEADER_ID, CUSTOMER_ID 

Customer 
================================= 
ID, other columns... 

Constraint: Details must be unique by HEADER_ID and CUSTOMER_ID so: 

Detail (VALID) 
================================= 
1, 123, 10 
2, 123, 12 

Detail (IN-VALID) 
================================= 
1, 123, 10 
1, 123, 10 

OK, когда я запускаю это и передать в 2, 3, 20 и т.д. клиентов, он создает все записи Detail просто отлично, пока их не было.

Если я запустил его снова, перейдя в другой список клиентов, я ожидаю, что детали ALL будут удалены сначала, а затем список из NEW. Подробности будут созданы.

Но все, что происходит, заключается в том, что удаление не выполняется перед созданием. Поскольку ошибка является дублирующим ключевым ограничением. Дублирующий ключ - это сценарий «IN-VALID» выше.

Если я вручную заполняю базу данных кучей деталей и комментирую часть CREATE details (только запустите удаление), тогда записи удаляются просто отлично. Таким образом, удаление работает. Создает работы. Просто они не работают вместе.

Я могу предоставить больше кода. Я использую Spring Data JPA.

Благодаря

UPDATE

Мои объекты помечаются в основном следующие:

@Entity 
@Table 
public class Header { 
... 
    @OneToMany(mappedBy = "header", orphanRemoval = true, cascade = {CascadeType.ALL}, fetch = FetchType.EAGER) 
    private Set<Detail> Details = new HashSet<>(); 

... 
} 

@Entity 
@Table 
public class Detail { 
... 
    @ManyToOne(optional = false) 
    @JoinColumn(name = "HEADER_ID", referencedColumnName = "ID", nullable = false) 
    private Header header; 
... 
} 

UPDATE 2

@Klaus Groenbaek

На самом деле, я не упоминал об этом изначально, но я сделал это в первый раз. Кроме того, я использую Cascading.ALL, который, как я предполагаю, включает PERSIST.

Только для тестирования, я обновил свой код к следующему:

@Transactional 
public void create(Integer id, List<Integer> customerIDs) { 

    Header header = headerService.findOne(id); 

    // Remove the details 
    detailRepository.delete(header.getDetails());  // Does not work 

    // I've also tried this: 
    for(Detail detail : header.getDetails()) { 
     detailRepository.delete(detail); 
    } 


    // Iterate through list of ID's and create Detail with other objects 
    for(Integer id : customerIDs) { 
     Customer customer = customerService.findOne(id); 

     Detail detail = new Detail(); 
     detail.setCustomer(customer); 
     detail.setHeader(header); 

     detailRepository.save(detail) 
    } 
} 

Опять ... Я хочу повторить .... что удаление будет работать, если у меня нет непосредственно создать после этого. Создать WILL WORK, если у меня нет удаления непосредственно перед ним. Но ни одна из них не будет работать, если они объединены из-за ошибки дублирования ключевого ограничения из базы данных.

Я пробовал один и тот же сценарий WITH и без каскадных удалений.

+0

Это зависит от того, как вы отображали детали ... просто '.remove (...)' они не всегда удаляют строки в БД – Andremoniy

+0

Итак, вы должны предоставить свои сущности аннотации – Andremoniy

+0

Не знаете, почему я ' м получить близкие голоса? В любом случае ... @Andremoniy да, я аннотировал мои сущности с Cascade все. Я обновлю эту информацию. – cbmeeks

ответ

4

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

Во-первых, добавление объектов в коллекцию или удаление объектов из коллекции не означает, что одна и та же операция будет выполняться в базе данных, если только операция сохранения не распространяется с использованием каскадирования или orphanRemoval.

Чтобы сущность была добавлена ​​в базу данных, вы должны позвонить по телефону EntityManager.persist() либо напрямую, либо через каскадное сохранение. Это в основном то, что происходит внутри JPARepository.save()

Если вы хотите удалить объект, вы должны позвонить по телефону EntityManager.remove() напрямую или путем каскадирования операции или через JpaRepository.delete().

Если у вас есть управляемый объект (тот, который загружен в контекст персистентности), и вы изменяете базовое поле (не-сущность, не-сбор) внутри транзакции, это изменение записывается в базу данных, когда транзакция совершает, даже если вы не звонили persist/save. Контекст persistence хранит внутреннюю копию каждого загруженного объекта, а когда транзакция фиксирует его в виде внутренних копий и сравнивается с текущим состоянием, а любые базовые внесенные изменения запускают запрос на обновление.

Если вы добавили новый объект (A) в коллекцию на другом объекте (B), но не вызвали persist on A, то A не будет сохранен в базе данных. Если вы вызываете persist on B, произойдет одна из двух вещей, если операция persist будет каскадирована, A также будет сохранена в базе данных. Если persist не каскадируется, вы получите сообщение об ошибке, потому что управляемый объект ссылается на неуправляемый объект, который дает эту ошибку на EclipseLink: «Во время синхронизации новый объект был найден через отношение, которое не было отмечено каскадом PERSIST». Cascade persist имеет смысл, потому что вы часто создаете родительский объект и его детей одновременно.

Если вы хотите удалить объект A из коллекции на другом объекте B, вы не можете полагаться на каскадирование, так как вы не удаляете B. Вместо этого вам нужно вызвать remove on A напрямую, удалив его из коллекции на B не имеет никакого эффекта, поскольку в EntityManager не вызывалась операция настойчивости. Вы также можете использовать orphanRemoval для запуска удаления, но я бы посоветовал вам быть осторожным при использовании этой функции, тем более, что вам, как представляется, не хватает некоторых базовых знаний о том, как работают операции сохранения.

Обычно это помогает думать о операции сохранения и о том, к какой сущности она должна применяться. Вот как бы выглядел код, если бы я его написал.

@Transactional 
public void create(Integer id, List<Integer> customerIDs) { 

    Header header = headerService.findOne(id); 
    // header is found, has multiple details 

    // Remove the details 
    for(Detail detail : header.getDetails()) { 
     em.remove(detail); 
    } 

    // em.flush(); // In some case you need to flush, see comments below 

    // Iterate through list of ID's and create Detail with other objects 
    for(Integer id : customerIDs) { 
     Customer customer = customerService.findOne(id); 

     Detail detail = new Detail(); 
     detail.setCustomer(customer); 
     detail.setHeader(header); // did this happen inside you service? 
     em.persist(detail); 
    } 
} 

Во-первых нет никаких причин, чтобы сохраняться заголовком, это управляемый объект и любое основное поле изменения будет изменить при транзакции.Заголовок является внешним ключом для элемента Details, что означает, что важная вещь - это detail.setHeader(header); и em.persist(details), так как вы должны установить все внешние отношения и сохранить любые новые Details. Аналогично, удаление существующих данных из заголовка не имеет ничего общего с заголовком, определяющее отношение (внешний ключ) находится в разделе «Подробности», поэтому удаление сведений из контекста персистентности - это то, что удаляет его из базы данных. Вы также можете использовать orphanRemoval, но для этого требуется дополнительная логика для каждой транзакции, и, на мой взгляд, код легче читать, если каждая операция существования является явной, поэтому вам не нужно возвращаться к сущности для чтения аннотаций.

И наконец: последовательность операций сохранения в вашем коде не транслируется к порядку запросов, выполняемых в базе данных. И Hibernate, и EclipseLink сначала вставляют новые объекты, а затем удаляют существующие сущности. По моему опыту, это самая распространенная причина, по которой «первичный ключ уже существует». Если вы удаляете объект с определенным первичным ключом, а затем добавляете новый объект с тем же самым первичным ключом, тогда вставка будет иметь место первой и вызвать нарушение ключа. Это можно исправить, указав JPA, чтобы сбросить текущее состояние Persistence в базу данных. em.flush() будет выталкивать запросы удаления в базу данных, поэтому вы можете вставить другую строку с тем же самым первичным ключом, что и тот, который вы удалили.

Это было много информации, пожалуйста, сообщите мне, было ли что-то, что вы не поняли, или мне нужно уточнить.

1

Прежде всего, только выполнение header.getDetails().remove(detail); не выполняет никаких операций с БД. Я полагаю, что в headerService.save(header); вы вызываете что-то вроде session.saveOrUpdate(header).

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

Я хотел бы предложить, по крайней мере, вызвать headerService.save(header);перед тем добавления новых деталей, то есть, как это:

// Remove the details 
    for(Detail detail : header.getDetails()) { 
     header.getDetails().remove(detail); 
    } 

    headerService.save(header); 

    // Iterate through list of ID's and create Detail with other objects 
    for(Integer id : customerIDs) { 
     // .... 
    } 

    headerService.save(header); 

для того, чтобы сказать Hibernate: да, удалять эти объекты, которые я удалены из коллекции, и после этого добавьте новые entites.