Другой объект с тем же идентификатором, использующий Spring data jpa с Hibernate - PullRequest
3 голосов
/ 20 мая 2019

Я проверил разные источники, но ни один не решил мою проблему, такую ​​как: https://coderanch.com/t/671882/databases/Updating-child-DTO-object-MapsId

Spring + Hibernate: другой объект с таким же значением идентификатора уже был связан с сеансом

Мой случай: я создал 2 класса, 1 хранилище, как показано ниже:

@Entity
public class Parent{
  @Id
  public long pid;

  public String name;

  @OneToMany(mappedBy = "parent", cascade = CascadeType.ALL)
  public List<Child> children;
}

-------------------------------------------------------------------

@Entity
public class Child{
  @EmbeddedId
  public PK childPK = new PK();

  public String name;

  @ManyToOne
  @MapsId("parentPk")
  @JoinColumn(name = "foreignKeyFromParent")
  public Parent parent;

  @Embeddable
  @EqualsAndHashCode
  static class PK implements Serializable {
      public long parentPk;
      public long cid;
  }
}
------------------------------------------------------------------------

public interface ParentRepository extends JpaRepository<AmazonTest, Long> {
}

Там, где Родитель и Ребенок имеют отношения Один ко Многим. По моему основной метод:

public static void main(String[] args) {
    @Autowired
    private ParentRepository parentRepository;

    Parent parent = new Parent();
    parent.pid = 1;
    parent.name = "Parent 1";

    Child child = new Child();
    List<Child> childList = new ArrayList<>();

    child.childPK.cid = 1;
    child.name = "Child 1";
    childList.add(child);

    parent.children= childList;

    parentRepository.save(parent);
    parentRepository.flush();
}


Когда я запускаю приложение в первый раз, данные могут быть успешно сохранены в базе данных. Но если я запускаю его снова, выдается ошибка «Exception: org.springframework.dao.DataIntegrityViolationException: другой объект с таким же значением идентификатора уже был связан с сеансом».
Я ожидал, что если данные будут новыми, это обновит мою базу данных, если данные будут такими же, ничего не произойдет. Что не так с моим кодом.

Если бы я оставил родителей в одиночестве (без каких-либо отношений с ребенком). Это не даст никакой ошибки, даже если я перезапущу приложение.

Отредактировано: Однако, если я использую приведенную ниже реализацию с простым первичным ключом в Child Entity, она будет работать так, как я ожидал. Я могу перезапустить приложение без ошибок. Я также могу изменить значение, например child.name, и оно будет отражено в базе данных.

@Entity
public class Parent{
   @Id
   public long pid;

   public String name;

   @OneToMany(mappedBy = "parent", cascade = CascadeType.ALL)
   public List<Child> children;
}

-------------------------------------------------------------------

@Entity
public class Child{
   @Id
   public long cid;


   public String name;

   @ManyToOne
   @JoinColumn(name = "foreignKeyFromParent")
   public Parent parent;

}
------------------------------------------------------------------------

public interface ParentRepository extends JpaRepository<AmazonTest, Long> {
}

-------------------------------------------------------------------------
public static void main(String[] args) {
   @Autowired
   private ParentRepository parentRepository;

   Parent parent = new Parent();
   parent.pid = 1;
   parent.name = "Parent 1";

   Child child = new Child();
   List<Child> childList = new ArrayList<>();

   child.cid = 1;
   child.name = "Child 1";
   childList.add(child);

   parent.children= childList;

   parentRepository.save(parent);
   parentRepository.flush();
}

Ответы [ 2 ]

1 голос
/ 20 мая 2019

Итак, parent.pid - это первичный ключ вашей базы данных. Вы можете сохранить только один набор записей в базе данных с id = 1. Это ожидаемое поведение.

Возможно, вам стоит ознакомиться с @GeneratedValue, чтобы не устанавливать идентификатор самостоятельно.

0 голосов
/ 20 мая 2019

Перед полным объяснением небольшая заметка: попробуйте опубликовать код, который на самом деле компилируется и работает как рекламируется.

  • Ваш main() не компилируется,
  • вы не устанавливаете полную связь между родителем и ребенком.
  • Также попробуйте явно разграничить транзакции в опубликованном примере.

Как работает ваш код

Вы вызываете save в репозитории.Ниже этот метод вызывает entityManager.merge(), так как вы сами установили идентификатор.Объединение вызывает SQL Выберите, чтобы проверить, существует ли объект, и впоследствии вызывает вставку или обновление SQL для объекта.(Предложения, которые сохраняются с объектом с идентификатором, который существует в db, неверны)

  • При первом запуске объекта там нет.

    • youвставить родительский
    • слияние каскадно, и вы вставляете дочерний (давайте назовем это childA)
  • Во втором запуске

    • merge выбирает родителя (с childA)
    • Мы сравниваем, если новый родитель уже находится в сеансе.Это делается в SessionImpl.getEntityUsingInterceptor
    • родитель найден
    • слияние каскадно переходит к дочернему
    • , мы снова проверяем, находится ли объект в сеансе.
    • Теперь возникает разница:
    • В зависимости от того, как вы установили отношение между дочерним элементом и родителем, у ребенка может быть неполный PK (и полагаться на его заполнение из отношения кродитель помечен @MapsId).К сожалению, сущность не найдена в сеансе через неполный PK, но позже, при сохранении, PK завершается, и теперь у вас есть 2 конфликтующих объекта с одним и тем же ключом.

Чтобы решить эту проблему

Child child = new Child();
child.parent = parent;
child.childPK.cid = 1;
child.childPK.parentPk = 1;

Это также объясняет, почему код работает, когда вы меняете PK Child на long - нет способа испортить его и получить неполный PK.

ПРИМЕЧАНИЕ

Приведенное выше решение создает беспорядок с сиротами.

Я все еще думаю, что оригинальное решение лучше, поскольку сироты удалены.Кроме того, добавление обновленного решения в оригинальное решение является полезным обновлением.Удаление всего списка и его повторная вставка вряд ли будут работать хорошо под нагрузкой.К сожалению, он удаляет список при первом слиянии родителя и повторно добавляет их при втором слиянии родителя.(Вот почему очистка не нужна)

Еще лучше, просто найдите родительскую сущность и внесите в нее обновления (как предлагают другие ответы).

Еще лучше, попробуйте взглянуть на решение и добавить / заменить только определенные дочерние элементы родителя, а не смотреть на родительский элемент и его дочерние элементы.Это, вероятно, будет наиболее производительным.

Исходное решение

Я предлагаю следующее (обратите внимание, что полная замена списка детей не разрешена, так как это Hibernate-прокси).

@OneToMany(mappedBy = "parent", cascade = CascadeType.ALL, orphanRemoval = true)
public List<Child> children = new ArrayList<>();
@SpringBootTest
public class ParentOrphanRepositoryTest {

    @Autowired
    private ParentOrphanRepository parentOrphanRepository;

    @Test
    public void testDoubleAdd() {
        addEntity();
        addEntity();
    }

    @Transactional
    public void addEntity() {
        Parent parent = new Parent();
        parent.pid = 1;
        parent.name = "Parent 1";

        parent = parentOrphanRepository.save(parent);


        Child child = new Child();
        List<Child> childList = new ArrayList<>();

        child.parent = parent;
        child.childPK.cid = 1;
        child.name = "Child 1";
        childList.add(child);

        // parent.children.clear(); Not needed.
        parent.children.addAll(childList);
        parentOrphanRepository.save(parent);
        parentOrphanRepository.flush();
    }
}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...