Delete Not Working with JpaRepository

109,520

Solution 1

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<Child> children = new HashSet<>(0);

    public void setChildren(Set<Child> 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.

Solution 2

I had the same problem

Perhaps your UserAccount entity has an @OneToMany with Cascade on some attribute.

I've just remove the cascade, than it could persist when deleting...

Solution 3

You need to add PreRemove function ,in the class where you have many object as attribute e.g in Education Class which have relation with UserProfile Education.java

private Set<UserProfile> userProfiles = new HashSet<UserProfile>(0);

@ManyToMany(fetch = FetchType.EAGER, mappedBy = "educations")
public Set<UserProfile> getUserProfiles() {
    return this.userProfiles;
}

@PreRemove
private void removeEducationFromUsersProfile() {
    for (UsersProfile u : usersProfiles) {
        u.getEducationses().remove(this);
    }
}

Solution 4

One way is to use cascade = CascadeType.ALL like this in your userAccount service:

@OneToMany(cascade = CascadeType.ALL)
private List<Token> tokens;

Then do something like the following (or similar logic)

@Transactional
public void deleteUserToken(Token token){
    userAccount.getTokens().remove(token);
}

Notice the @Transactional annotation. This will allow Spring (Hibernate) to know if you want to either persist, merge, or whatever it is you are doing in the method. AFAIK the example above should work as if you had no CascadeType set, and call JPARepository.delete(token).

Solution 5

This is for anyone coming from Google on why their delete method is not working in Spring Boot/Hibernate, whether it's used from the JpaRepository/CrudRepository's delete or from a custom repository calling session.delete(entity) or entityManager.remove(entity).

I was upgrading from Spring Boot 1.5 to version 2.2.6 (and Hibernate 5.4.13) and had been using a custom configuration for transactionManager, something like this:

@Bean
public HibernateTransactionManager transactionManager(EntityManagerFactory entityManagerFactory) {
    return new HibernateTransactionManager(entityManagerFactory.unwrap(SessionFactory.class));
}

And I managed to solve it by using @EnableTransactionManagement and deleting the custom transactionManager bean definition above.

If you still have to use a custom transaction manager of sorts, changing the bean definition to the code below may also work:

@Bean
public PlatformTransactionManager transactionManager(EntityManagerFactory entityManagerFactory) {
    return new JpaTransactionManager(entityManagerFactory);
}

As a final note, remember to enable Spring Boot's auto-configuration so the entityManagerFactory bean can be created automatically, and also remove any sessionFactory bean if you're upgrading to entityManager (otherwise Spring Boot won't do the auto-configuration properly). And lastly, ensure that your methods are @Transactional if you're not dealing with transactions manually.

Share:
109,520
Admin
Author by

Admin

Updated on February 18, 2022

Comments

  • Admin
    Admin about 2 years

    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 implements Serializable {
    
        @Id
        @SequenceGenerator(name = "seqToken", sequenceName = "SEQ_TOKEN", initialValue = 500, allocationSize = 1)
        @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "seqToken")
        @Column(name = "TOKEN_ID", nullable = false, precision = 19, scale = 0)
        private Long id;
    
        @NotNull
        @Column(name = "VALUE", unique = true)
        private String value;
    
        @ManyToOne(fetch = FetchType.EAGER)
        @JoinColumn(name = "USER_ACCOUNT_ID", nullable = false)
        private UserAccount userAccount;
    
        @Temporal(TemporalType.TIMESTAMP)
        @Column(name = "EXPIRES", length = 11)
        private Date expires;
    
        ...
        // getters and setters omitted to keep it simple
    }
    

    I have a JpaRepository interface defined:

    public interface TokenRepository extends JpaRepository<Token, Long> {
    
        Token findByValue(@Param("value") String value);
    
    }
    

    I have a unit test setup that works with an in memory database (H2) and I am pre-filling the database with two tokens:

    @Test
    public void testDeleteToken() {
        assertThat(tokenRepository.findAll().size(), is(2));
        Token deleted = tokenRepository.findOne(1L);
        tokenRepository.delete(deleted);
        tokenRepository.flush();
        assertThat(tokenRepository.findAll().size(), is(1));
    }
    

    The first assertion passes, the second fails. I tried another test that changes the token value and saves that to the database and it does indeed work, so I'm not sure why delete isn't working. It doesn't throw any exceptions either, just doesn't persist it to the database. It doesn't work against my oracle database either.


    Edit

    Still having this issue. I was able to get the delete to persist to the database by adding this to my TokenRepository interface:

    @Modifying
    @Query("delete from Token t where t.id = ?1")
    void delete(Long entityId);
    

    However this is not an ideal solution. Any ideas as to what I need to do to get it working without this extra method?

  • Admin
    Admin about 10 years
    I don't think that's the issue here because when I seed the test with test data I'm specifying the id to be 1.
  • Brian Kates
    Brian Kates over 8 years
    I had the same problem and came across this thread. I don't know if I would call this a solution or a workaround, but this is how to delete the child objects.
  • GameSalutes
    GameSalutes about 8 years
    This worked for me as well. Specifically I played around and was able to keep these cascade types: [CascadeType.PERSIST,CascadeType.REFRESH] and still get the delete to work through child repository. What made no sense to me is that I made the owner entity the child via "mappedBy" on the parent
  • Ivan Matavulj
    Ivan Matavulj almost 7 years
    This solution seems to work but does not give any explanation .
  • Ognjen Mišić
    Ognjen Mišić over 4 years
    Real good job on the explanation here. I had the same issue and the same apparent "workaround" - delete through a custom query, but I didn't want to use it as it was obviously a hack. The @PreRemove hook really did wonders here. Thanks!
  • pzeszko
    pzeszko over 4 years
    Glad I could help!
  • driversti
    driversti about 3 years
    I had a similar issue with @OneToOne relation and your explanation helped me to resolve it. Very nice! Thank you.
  • kelsanity
    kelsanity over 2 years
    kind of late to this, but what do you mean by synchronizing in this context?