Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Delete Not Working with JpaRepository

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?

like image 826
Twisty McGee Avatar asked Mar 27 '14 12:03

Twisty McGee


People also ask

How do I delete a record in JPA?

To delete a record from database, EntityManager interface provides remove() method. The remove() method uses primary key to delete the particular record.

How does JPA delete work?

In JPA, to delete an entity, the entity itself must be managed, meaning that it is present in the persistence context. This means that the calling application should have already loaded or accessed the entity and is now issuing a command to remove it.

Which is better CrudRepository or JpaRepository?

Crud Repository doesn't provide methods for implementing pagination and sorting. JpaRepository ties your repositories to the JPA persistence technology so it should be avoided. We should use CrudRepository or PagingAndSortingRepository depending on whether you need sorting and paging or not.

How do I delete multiple records in JPA?

First of all you need to create a jpa query method that brings all records belong to id. After that you can do deleteAll() operation on List.


3 Answers

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.

like image 162
pzeszko Avatar answered Oct 18 '22 20:10

pzeszko


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...

like image 48
Davi Arimateia Avatar answered Oct 18 '22 22:10

Davi Arimateia


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);
    }
}
like image 19
Taimur Avatar answered Oct 18 '22 20:10

Taimur