LINUX.ORG.RU

Каскадное удаление связных сущностей

 , , , ,


0

1

У меня есть несколько связных сущностей:

@NoArgsConstructor
@Getter @Setter
@Entity
public class Organization extends AbstractIdentifiable {

    @Column(nullable = false, unique = true)
    @NotBlank
    private String title;

    @ManyToOne(optional = false)
    @JoinColumn(nullable = false)
    private Member registrationMember;

    @Column(nullable = false)
    @Temporal(TemporalType.TIMESTAMP)
    private Calendar registrationDateTime;

    @OneToOne(mappedBy = "organization", cascade = CascadeType.ALL)
    private Site site;

    @OneToOne(mappedBy = "organization", cascade = CascadeType.ALL)
    private Advertiser advertiser;

}

@NoArgsConstructor
@Getter @Setter
@Entity
public class Advertiser extends AbstractIdentifiable {

    @OneToOne(optional = false)
    @JoinColumn(nullable = false)
    private Organization organization;

    @ManyToMany(mappedBy = "advertisers")
    private Set<Member> members = new HashSet<>();

    @OneToMany(mappedBy = "advertiser", cascade = CascadeType.ALL)
    private Set<PaidVideo> paidVideos = new HashSet<>();

}
@NoArgsConstructor
@Getter @Setter
@Entity
public class PaidVideo extends AbstractIdentifiable {

    @OneToOne(optional = false)
    @JoinColumn(nullable = false)
    private Video video;

    @ManyToOne(optional = false)
    @JoinColumn(nullable = false)
    private Advertiser advertiser;

    @OneToMany(mappedBy = "paidVideo", cascade = CascadeType.ALL)
    private Set<Booking> bookings = new HashSet<>();

}

Перечислил только те, что имеют непосредственное отношение к вопросу, от общей сущности (вверху), к зависимым (внизу).

Пытаюсь удалить сущность Organization. Ожидаю, что по каскадной операции (cascade = CascadeType.ALL) будут удалены все связные сущности: Advertiser, PaidVideo и так далее:

@Log4j2
@Controller
@RequestMapping("/api/organizations")
public class OrganizationController {

    @Autowired
    private OrganizationRepository organizationRepository;

    @PreAuthorize("hasRole('ADMIN')")
    @Transactional
    @RequestMapping(value = "/{id}/delete", method = RequestMethod.POST)
    public ResponseEntity delete(@PathVariable("id") Long id) {
        organizationRepository.deleteById(id);
        return ResponseEntity.ok().build();
    }

}

Однако этого не происходит. Hibernate выкачивает все связные сущности, а после пытается первой и единственной удалить сущность Advertiser, закономерно получая ошибку ограничения целостности: https://pastebin.com/Z0M9BdM7

Чем может быть вызвано такое поведение? Разве hibernate не должен был построить дерево зависимостей сущностей и удалять их начиная с листьев? Почему при удалении Organization происходит только попытка удаления Advertiser, игнорируя зависимые сущности Advertiser? При чем это единственная попытка удаления чего либо, она происходит при вызове метода organizationRepository.deleteById(id); и после происходит выход из транзакционного метода, и коммит транзакции. В общем, колдунство какое-то.

Граф связей таблиц в БД: https://i.imgur.com/lPixAnd.png

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

Попробуй в своих связях со стороны @ManyToOne добавить имя свойства в @JoinColumn.

hippi90 ★★★★ ()
Ответ на: комментарий от hippi90

Попробовал. И даже добавил orphanRemoval в @OneToMany:

@NoArgsConstructor
@Getter @Setter
@Entity
public class Advertiser extends AbstractIdentifiable {

    @OneToOne(optional = false)
    @JoinColumn(nullable = false)
    private Organization organization;

    @ManyToMany(mappedBy = "advertisers")
    private Set<Member> members = new HashSet<>();

    @OneToMany(mappedBy = "advertiser", cascade = CascadeType.ALL, orphanRemoval = true)
    private Set<PaidVideo> paidVideos = new HashSet<>();

}
@NoArgsConstructor
@Getter @Setter
@Entity
public class PaidVideo extends AbstractIdentifiable {

    @OneToOne(optional = false)
    @JoinColumn(nullable = false)
    private Video video;

    @ManyToOne(optional = false)
    @JoinColumn(name="advertiser", referencedColumnName = "id", nullable = false)
    private Advertiser advertiser;

    @OneToMany(mappedBy = "paidVideo", cascade = CascadeType.ALL)
    private Set<Booking> bookings = new HashSet<>();

}

Вообще ничего не изменилось.

popov-aa ()
If X is a managed entity, the remove operation causes it to become removed. The remove oper-
ation is cascaded to entities referenced by X, if the relationships from X to these other entities
is annotated with the cascade=REMOVE or cascade=ALL annotation element value.



Каскадирование производится в прямом порядке - сначала удаляются сушности-владельцы, только потом подчинённые.
Так что, если у вас доступ к СУБД только через JPA, то убирайте внешние ключи оттуда. Как правило при использовании ORM, нужно сразу решать, кто будет управлять целостностью - СУБД или ORM.

sanwashere ★★ ()
Последнее исправление: sanwashere (всего исправлений: 3)
Ответ на: комментарий от sanwashere

Каскадирование производится в прямом порядке - сначала удаляются сушности-владельцы, только потом подчинённые.
Так что, если у вас доступ к СУБД только через JPA, то убирайте внешние ключи оттуда.

База данных без внешних ключей? Звучит не очень здорово, если я правильно понял. Удаление сущностей начиная с объекта владельца - тоже странное решение. Как он удалится, если на него есть ссылки?

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

    @PreAuthorize("hasRole('ADMIN')")
    @Transactional
    @RequestMapping(value = "/{id}/delete", method = RequestMethod.POST)
    public ResponseEntity delete(@PathVariable("id") Long id) {
        Organization organization = organizationRepository.findById(id).get();
        organization.getAdvertiser().getPaidVideos().forEach(paidVideo -> {
            paidVideo.getBookings().forEach(booking -> {
                bookingRepository.delete(booking);
            });
            paidVideoRepository.delete(paidVideo);
        });
        organizationRepository.delete(organization);
        return ResponseEntity.ok().build();
    }

Результат тот же. После выхода из метода первым вызывается:

Hibernate: 
    delete 
    from
        webserver.advertiser 
    where
        id=?
Что вообще происходит? :D Где же удаление booking и paidVideo?

popov-aa ()
Ответ на: комментарий от sanwashere

Как правило при использовании ORM, нужно сразу решать, кто будет управлять целостностью - СУБД или ORM.

Это вообще популярная практика, не использовать ограничения целостности субд и отдавать всё на откуп ORM? Я всегда старался проектировать структуры базы данных в соответствии с инфологической моделью, строго, с использованием ограничений и избегая неоднозначностей и дублирования данных. Отказ от контроля целостности СУБД для меня действительно что-то новое и странное.

Получается, что каскадное удаление в hibernate работает только при отказе от обраничений СУБД? Но зачем тогда аннотация @JoinColumn?

popov-aa ()
Ответ на: комментарий от sanwashere

Пока только один подход позволил мне добиться ожидаемого поведения: я удалил везде cascade и удаляю все связные сущности в том порядке, в котором удалял бы их из вручную из субд, то есть от детей к родителям. Включение каскада игнорирует почему-то ручное удаление детей и сразу по выходе из метода удаляет сущность, удаляемую последней в методе.

popov-aa ()