Рекурсивные отношения в Spring Data и JPA?

Моя сущность Comment является самосоединенной и имеет набор subComment.

@Entity
public class Comment {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private String id;

@OneToMany(mappedBy = "parentComment", cascade = CascadeType.ALL)
private Set<Comment> subComments = new HashSet<>();

@ManyToOne
@JoinColumn(referencedColumnName = "id")
private Comment parentComment;

Здесь в моем методе addComment

public ResponseEntity<Comment> addComment(Comment comment) {
    Comment currComment = commentRepository.save(comment);
    if (currComment.getParentId() != null) {
        Comment parent = commentRepository.findById(currComment.getParentId()).orElse(null);
        if (parent != null) {
            parent.addSubComment(currComment);
            currComment.setParentId(parent.getId());
            currComment.setParentComment(parent);
            commentRepository.save(parent);
        }
    }
    Comment responseComment = commentRepository.save(currComment);
    return ResponseEntity.ok(responseComment);
}

Когда я попытался установить обратную связь (владеющая сторона), comment.setParentComment(parent); вызывает ошибку

comment.setParentComment(parent); вызывает ошибку: java.lang.IllegalStateException: Cannot call sendError() after the response has been committed

Класс сущности полного комментария

@OneToMany(mappedBy = "parentComment", cascade = CascadeType.ALL)
private Set<Comment> subComments = new HashSet<>();

@ManyToOne
@JoinColumn(referencedColumnName = "id")
private Comment parentComment;

private boolean isParent;
private String parentId;

public String getParentId() {
    return parentId;
}

public void setParentId(String parentId) {
    this.parentId = parentId;
}

public Set<Comment> getSubComments() {
    return subComments;
}

public void setSubComments(Set<Comment> subComments) {
    this.subComments = subComments;
}

public Comment addSubComment(Comment comment) {
    this.subComments.add(comment);
    return this;
}

public Comment getParentComment() {
    return parentComment;
}

public void setParentComment(Comment parentComment) {
    this.parentComment = parentComment;
}

public boolean getIsParent() {
    return isParent;
}

public void setIsParent(boolean isParent) {
    this.isParent = isParent;
}
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
В компьютерном программировании биты играют важнейшую роль в представлении и манипулировании данными на двоичном уровне. Побитовые операции...
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Приходилось ли вам сталкиваться с требованиями, в которых вас могли попросить поднять тревогу или выдать ошибку, когда метод Java занимает больше...
Полный курс Java для разработчиков веб-сайтов и приложений
Полный курс Java для разработчиков веб-сайтов и приложений
Получите сертификат Java Web и Application Developer, используя наш курс.
3
0
9 005
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Вы, кажется, слишком усложняете это.

1) У вас есть @JoinColumn(referencedColumnName = "id"), но он избыточен. В любом случае это то, на что ссылается внешний ключ, поэтому вам не нужно говорить об этом явно. Не беспокойтесь, но не пишите ненужный код.

2) Если у вас есть parentId в новом подкомментарии, вам не нужно искать его и добавлять в список родительских комментариев. Понятие, которое вам здесь не хватает, - это концепция "владения" сущностью. Посмотрите Javadoc для mappedBy. Поскольку поле parentComment выполняет отображение, оно определяет объект-владелец. Конечно, это та же самая Entity, но дело в том, что это поле parentComment, которое контролирует постоянство. Вам не нужно ничего добавлять к Set из subComments, чтобы связь сохранилась. Вы можете сделать это, если хотите, но JPA проигнорирует это. Вам нужно только установить поле parentComment. Например.

Обновлено: этот пример с JPA вместо Spring Data, но под капотом он такой же.

Ваша сущность должна быть только:

@Entity
public class Comment {
    @Id @GeneratedValue
    private Integer id;

    @OneToMany(mappedBy = "parentComment")
    private Set<Comment> subComments;

    @ManyToOne
    private Comment parentComment;

и вы используете это так:

private void run() {
    runWrite();
    runRead();
    Comment comment = new Comment();
    comment.setId(1);
    Comment subComment = new Comment();
    subComment.setParentComment(comment);
    runSaveSubComment(subComment);
    runRead();
}
private void runWrite() {
    EntityManagerFactory emf = Persistence.createEntityManagerFactory("persistence");
    em = emf.createEntityManager();
    tx = em.getTransaction();
    try {
        tx.begin();
        Comment comment = new Comment();
        Comment subComment = new Comment();
        subComment.setParentComment(comment);
        em.persist(comment);
        em.persist(subComment);

        tx.commit();
    } finally {
        emf.close();
    }        
}
private void runRead() {
    EntityManagerFactory emf = Persistence.createEntityManagerFactory("persistence");
    em = emf.createEntityManager();
    try {
        Comment comment = em.createQuery("select c from Comment c left join fetch c.subComments where c.id = :id", Comment.class).setParameter("id", 1).getSingleResult();
        System.out.println(comment + Arrays.toString( comment.getSubComments().toArray()) );
    } finally {
        emf.close();
    }
}
private void runSaveSubComment(Comment subComment) {
    EntityManagerFactory emf = Persistence.createEntityManagerFactory("persistence");
    em = emf.createEntityManager();
    tx = em.getTransaction();
    try {
        tx.begin();
        em.persist(subComment);
        tx.commit();
    } finally {
        emf.close();
    }        
}

или если вы хотите использовать Spring Data.

Comment comment = new Comment();
Comment sub1 = new Comment();
sub1.setParentComment(comment);
repo.save(comment);
repo.save(sub1);
Comment parentComment = repo.fetchSubCommentsById(1);
System.out.println(parentComment + Arrays.toString(parentComment.getSubComments().toArray()));
Comment sub2 = new Comment();
sub2.setParentComment(parentComment);
repo.save(sub2);
parentComment = repo.fetchSubCommentsById(1);
System.out.println(parentComment + Arrays.toString(parentComment.getSubComments().toArray()));
// or 
Comment p = new Comment();
p.setId(1);
Comment sub3 = new Comment();
sub3.setParentComment(p);
repo.save(sub3);
parentComment = repo.fetchSubCommentsById(1);
System.out.println(parentComment + Arrays.toString(parentComment.getSubComments().toArray()));

Следите за своими комментариями. Я использую Spring boot, есть ли способ упростить?

Jasmine Rain 27.05.2018 20:30

Я не использую, просто использую идентификатор первого родителя, который был записан в базу данных, который, как мне известно, равен 1. Как правило, текущий комментарий отправляется в представление с его идентификатором, а идентификатор возвращается из представления, когда пользователь добавляет новый дополнительный комментарий. Итак, вместо получения родительского комментария из базы данных с идентификатором просто создайте новый комментарий и установите идентификатор из представления. Вызов save () не заботится о каких-либо других полях родительского комментария, поэтому он сохраняет вам вызов базы данных. Пример с пружиной показывает, что это делается в обоих направлениях.

K.Nicholas 28.05.2018 01:10

Кроме того, пользовательский fetchSubCommentsById получает родителей И вложенные комментарии, так что имейте это в виду. Обычно, если вы только что получили комментарий из базы данных, вложенные комментарии не заполняются. Если вы попытаетесь получить к ним доступ, вы получите исключение ленивой инициализации. Название fetchSubCommentsById не совсем удачное. Я просто скопировал запрос из раздела JPA в собственный метод в интерфейсе репозитория. См. Обсуждение fetchMode на stackoverflow.com/questions/29602386/….

K.Nicholas 28.05.2018 01:19

@K Просто примечание, поскольку это может быть полезно для других: вы можете избежать исключения ленивой инициализации, аннотируя метод с помощью @Transactional. Как-то связано с тем, что сущности остаются подключенными к диспетчеру сущностей, позволяя диспетчеру сущностей лениво загружать отношения.

user625488 12.07.2020 22:04
Ответ принят как подходящий
@ManyToOne
@JoinColumn(referencedColumnName = "id")
@JsonBackReference
private Comment parentComment;

Я добавил @JsonBackReference, который решил ошибку java.lang.IllegalStateException: Cannot call sendError() after the response has been committed. Родительский Comment также может видеть набор subComments.

Другие вопросы по теме