Delete Not Working with JpaRepository

前端 未结 12 2031
情书的邮戳
情书的邮戳 2020-12-07 11:55

I have a spring 4 app where I\'m trying to delete an instance of an entity from my database. I have the following entity:

@Entity
public class Token impleme         


        
12条回答
  •  挽巷
    挽巷 (楼主)
    2020-12-07 12:42

    Most probably such behaviour occurs when you have bidirectional relationship and you're not synchronizing both sides WHILE having both parent and child persisted (attached to the current session).

    This is tricky and I'm gonna explain this with the following example.

    @Entity
    public class Parent {
        @Id
        @GeneratedValue(strategy = IDENTITY)
        @Column(name = "id", unique = true, nullable = false)
        private Long id;
    
        @OneToMany(cascade = CascadeType.PERSIST, mappedBy = "parent")
        private Set children = new HashSet<>(0);
    
        public void setChildren(Set children) {
            this.children = children;
            this.children.forEach(child -> child.setParent(this));
        }
    }
    @Entity
    public class Child {
        @Id
        @GeneratedValue(strategy = IDENTITY)
        @Column(name = "id", unique = true, nullable = false)
        private Long id;
    
        @ManyToOne
        @JoinColumn(name = "parent_id")
        private Parent parent;
    
        public void setParent(Parent parent) {
            this.parent = parent;
        }
    }
    

    Let's write a test (a transactional one btw)

    public class ParentTest extends IntegrationTestSpec {
    
        @Autowired
        private ParentRepository parentRepository;
    
        @Autowired
        private ChildRepository childRepository;
    
        @Autowired
        private ParentFixture parentFixture;
    
        @Test
        public void test() {
            Parent parent = new Parent();
            Child child = new Child();
    
            parent.setChildren(Set.of(child));
            parentRepository.save(parent);
    
            Child fetchedChild = childRepository.findAll().get(0);
            childRepository.delete(fetchedChild);
    
            assertEquals(1, parentRepository.count());
            assertEquals(0, childRepository.count()); // FAILS!!! childRepostitory.counts() returns 1
        }
    }
    

    Pretty simple test right? We're creating parent and child, save it to database, then fetching a child from database, removing it and at last making sure everything works just as expected. And it's not.

    The delete here didn't work because we didn't synchronized the other part of relationship which is PERSISTED IN CURRENT SESSION. If Parent wasn't associated with current session our test would pass, i.e.

    @Component
    public class ParentFixture {
        ...
        @Transactional(propagation = Propagation.REQUIRES_NEW)
        public void thereIsParentWithChildren() {
            Parent parent = new Parent();
            Child child = new Child();
            parent.setChildren(Set.of(child));
    
            parentRepository.save(parent);
        }
    } 
    

    and

    @Test
    public void test() {
        parentFixture.thereIsParentWithChildren(); // we're saving Child and Parent in seperate transaction
    
        Child fetchedChild = childRepository.findAll().get(0);
        childRepository.delete(fetchedChild);
    
        assertEquals(1, parentRepository.count());
        assertEquals(0, childRepository.count()); // WORKS!
    }
    

    Of course it only proves my point and explains the behaviour OP faced. The proper way to go is obviously keeping in sync both parts of relationship which means:

    class Parent {
        ...
         public void dismissChild(Child child) {
             this.children.remove(child);
         }
    
         public void dismissChildren() {
            this.children.forEach(child -> child.dismissParent()); // SYNCHRONIZING THE OTHER SIDE OF RELATIONSHIP 
            this.children.clear();
         }
    
    }
    
    class Child {
        ...
        public void dismissParent() {
            this.parent.dismissChild(this); //SYNCHRONIZING THE OTHER SIDE OF RELATIONSHIP
            this.parent = null;
        }
    }
    

    Obviously @PreRemove could be used here.

提交回复
热议问题