Вариант использования: У нас одно-много двунаправленных отношений, и мы будем получать запросы на обновление в качестве родителя, который содержит либо модифицируемый дочерний элемент, либо нет.
Технологический стек
- Spring boot 2.0.2
- Пружинные данные Jpa
Код образца:
Сущность родительского класса:
package com.example.demo.model;
import java.util.HashSet;
import java.util.Set;
import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.OneToMany;
import org.hibernate.annotations.DynamicInsert;
import org.hibernate.annotations.DynamicUpdate;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Getter
@Setter
@NoArgsConstructor
@DynamicInsert
@DynamicUpdate
@Entity
public class Parent {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String a;
private String b;
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true, mappedBy = "parent")
private Set<Child> childs = new HashSet<>();
public void addChild(Child child) {
childs.add(child);
child.setParent(this);
}
public void removeChild(Child child) {
childs.remove(child);
child.setParent(null);
}
public void setChilds(
Set<Child> childrens) {
if (this.childs == null) {
this.childs = childrens;
}
else {
this.childs.retainAll(childrens);
this.childs.addAll(childrens);
}
}
}
Детский класс
package com.example.demo.model;
import java.util.Objects;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;
import javax.persistence.UniqueConstraint;
import org.hibernate.annotations.DynamicInsert;
import org.hibernate.annotations.DynamicUpdate;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Getter
@Setter
@NoArgsConstructor
@DynamicInsert
@DynamicUpdate
@Entity
@Table(name = "child", uniqueConstraints = {
@UniqueConstraint(columnNames = { "a", "b", "c", "parent_id" }) })
public class Child {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String a;
private String b;
private String c;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "parent_id")
private Parent parent;
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof Child)) {
return false;
}
Child that = (Child) o;
return Objects.equals(getA(), that.getA()) && Objects.equals(getB(), that.getB())
&& Objects.equals(getC(), that.getC());
}
@Override
public int hashCode() {
return Objects.hash(getA(), getB(), getC());
}
}
Класс репозитория:
package com.example.demo.model;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.stereotype.Repository;
@Repository
public interface ParentRepository extends JpaRepository<Parent, Long> {
@Query("select p from Parent p join fetch p.childs where p.a = ?1")
Parent findByA(String a);
}
Main-Class с бизнес-кейсом:
package com.example.demo;
import java.util.HashSet;
import java.util.Set;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.util.Assert;
import com.example.demo.model.Child;
import com.example.demo.model.Parent;
import com.example.demo.model.ParentRepository;
@SpringBootApplication
public class DemoApplication implements CommandLineRunner {
@Autowired
ParentRepository repository;
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
@Override
public void run(String... args) throws Exception {
Child c1 = new Child();
c1.setA("a1");
c1.setB("b1");
c1.setC("c1");
Child c2 = new Child();
c2.setA("a2");
c2.setB("b2");
c2.setC("c2");
Parent p = new Parent();
p.addChild(c1);
p.addChild(c2);
p.setA("a");
repository.save(p);
// This works till now
// We will get the request for updating parent which might contain removal or addition of the child
Parent retrievedParent = repository.findByA("a");
retrievedParent.setB("b");
Child c4 = new Child();
c4.setA("a2");
c4.setB("b2");
c4.setC("c2");
Child c3 = new Child();
c3.setA("a3");
c3.setB("b3");
c3.setC("c3");
//If we know that c1 is removed and c3 is added we can use synchronize methods written in Parent
//As we don't know which are removed and which are added also as we won't get the id from request passing them
// directly to set to let hibernate handle it as equals and Hashcode is already written.
Set<Child> childrens = new HashSet<>();
childrens.add(c3);
childrens.add(c4);
retrievedParent.setChilds(childrens);
Parent persistedParent = repository.save(retrievedParent);
for (Child child : persistedParent.getChilds()) {
Assert.notNull(child.getParent(), "Parent must not be null");
//For child 3 it is failing
}
}
}
С помощью приведенного выше кода невозможно установить родительский идентификатор для дочерней сущности 4, если мы печатаем журналы SQL, мы можем наблюдать, что дочерний элемент с идентификатором 1 удален, а дочерний элемент с идентификатором 3 вставлен, что ожидается.
В качестве обходного пути я перебираю все дочерние записи, и если родительский элемент не установлен, то устанавливаю вручную. Я не хочу этого дополнительного заявления об обновлении.
Пробовали другие подходы, удаляя все дочерние записи, используя синхронизированный метод removeChild, а затем добавляя оставшиеся по одному, используя синхронизированный метод addChild. Это вызывает уникальное исключение сбоя ограничения.
Что требуется?
Установка родителя вместо обходного пути в операторе вставки при его выполнении.