JpaRepository中的删除操作无法正常工作

107

我有一个Spring 4应用程序,我试图从我的数据库中删除一个实体的实例。我拥有以下实体:

@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
}

我有一个定义了JpaRepository接口:

public interface TokenRepository extends JpaRepository<Token, Long> {

    Token findByValue(@Param("value") String value);

}

我有一个单元测试设置,使用内存数据库(H2)并且我会预先填充数据库中的两个令牌:

@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));
}

第一个断言通过了,第二个失败了。我尝试了另一个测试,改变了令牌值并将其保存到数据库中,确实可以工作,所以我不确定为什么删除不起作用。它也没有抛出任何异常,只是没有将其持久化到数据库中。它在我的Oracle数据库上也不起作用。
编辑
仍然存在这个问题。我通过将以下内容添加到TokenRepository接口来使删除持久化到数据库中:
@Modifying
@Query("delete from Token t where t.id = ?1")
void delete(Long entityId);

然而,这并不是一个理想的解决方案。有什么建议可以让它在不使用额外方法的情况下正常工作吗?

4
你是否解决了这个问题?最新版的Spring也出现了相同的问题。根据MySQL日志表,没有删除语句被执行,也没有错误信息可用。 - phil294
1
很遗憾,我不再需要在这里做这件事了,而我在其他地方需要做的似乎都很顺利。我不知道这个案例有什么特别之处。 - userspaced
你应该考虑将那个“@Modifying delete”语句发布为答案。那是我解决问题的唯一方法。我已经为这个问题点了赞,但我也会为那个答案点赞。 - Taugenichts
2
覆盖删除方法也解决了我的问题,不确定为什么会发生这种情况(从Spring Boot 2.1.5升级到2.1.6)。 - Brunaldo
1
在一个最简单的实体上,我也遇到了同样的问题,没有使用任何关系。在一个事务中保存然后删除,例如测试方法在Spring 2.1.6中不再起作用。 我们现在使用的是Spring 2.4.1,但将spring-data降级为2.1.8.RELEASE以克服这个问题。Hibernate保持不变5.4.25。 - JRA_TLL
14个回答

100

当您拥有双向关系并且在父子均已持久化(附加到当前会话)时,如果未同步两侧,则很可能会出现此类行为。

这是一个棘手的问题,我将通过以下示例进行解释。

@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;
    }
}

让我们写一个测试(顺便说一下,是事务性的)

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

测试很简单,对吧?我们创建了一个父节点和子节点,将其保存到数据库中,然后从数据库中获取一个子节点,将其删除,并最终确保一切都如预期的那样运行。但实际情况并非如此。

这里的删除操作没有生效,因为我们没有同步另一部分关系,该关系存储在当前会话中。如果父节点未与当前会话相关联,测试将通过,即:

@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);
    }
} 

@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!
}

当然,这只证明了我的观点并解释了OP所面临的行为。显然正确的方法是保持关系的两个方面同步,这意味着:

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;
    }
}

显然这里可以使用@PreRemove


3
在这里的解释真的很好。我遇到了同样的问题和同样的“变通方法”——通过自定义查询删除,但我不想使用它,因为显然是一个hack。@PreRemove钩子确实在这里起了奇效。谢谢! - Ognjen Mišić
很高兴能够帮助! - pzeszko
我曾经遇到过与@OneToOne关系类似的问题,你的解释帮助我解决了它。非常好!谢谢。 - driversti
有点晚了,但在这个上下文中,你所说的同步是什么意思? - kelsanity

49
我遇到了相同的问题。 也许你的UserAccount实体具有某个属性上的Cascade @OneToMany。 我刚刚移除了级联,这样在删除时就可以持久化...

1
这对我也起作用了。具体来说,我尝试了一下,并能够保持这些级联类型:[CascadeType.PERSIST,CascadeType.REFRESH],并且仍然可以通过子存储库使删除操作正常工作。让我感到困惑的是,我通过在父级上使用“mappedBy”将所有者实体设置为子级。 - GameSalutes
19
这个解决方案似乎可行,但没有给出任何解释。 - Ivan Matavulj
你想分享一下解释吗?这也帮助了我。 - gromyk

20

你需要在具有多个对象作为属性的类中添加PreRemove函数,例如在与UserProfile相关联的Education Class中 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);
    }
}

9
一种方法是在您的userAccount服务中使用cascade = CascadeType.ALL,如下所示:
@OneToMany(cascade = CascadeType.ALL)
private List<Token> tokens;

那么可以执行以下类似逻辑的操作:
@Transactional
public void deleteUserToken(Token token){
    userAccount.getTokens().remove(token);
}

注意@Transactional注解。这将允许Spring(Hibernate)知道您要在方法中执行持久化、合并或其他操作。据我所知,上面的示例应该可以像没有设置CascadeType一样工作,并调用JPARepository.delete(token)


我移除了 @Transactional,并添加了:userRepository.save(user); 谢谢 - Chester mi niño

5
我遇到了类似的问题。
解决方案1:
记录没有被删除的原因可能是实体仍然被关联。因此,我们需要先将它们分离,然后再尝试删除它们。
以下是我的代码示例:
用户实体:
@Entity
public class User {
    @OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY, mappedBy = "user")
    private List<Contact> contacts = new ArrayList<>();
}

联系人实体:

@Entity
public class Contact {
    @Id
    private int cId;
    
    @ManyToOne
    private User user;
}

删除代码:

user.getContacts().removeIf(c -> c.getcId() == contact.getcId());
this.userRepository.save(user);
this.contactRepository.delete(contact);

方案2:

在这里,我们使用 orphanRemoval 属性来从数据库中删除孤立的实体。不再连接到其父实体的实体被称为孤立实体。

代码示例:

User Entity:

首先,我们从用户的联系人 ArrayList 中删除要删除的 Contact 对象,然后使用 delete() 方法。

@Entity
public class User {
    @OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY, mappedBy = "user", orphanRemoval = true)
    private List<Contact> contacts = new ArrayList<>();
}

联系实体:

@Entity
public class Contact {
    @Id
    private int cId;
    
    @ManyToOne
    private User user;
}

删除代码:

user.getContacts().removeIf(c -> c.getcId() == contact.getcId());
this.userRepository.save(user);

在这里,由于联系人实体不再连接到其父级,它成为了一个孤立的实体并将从数据库中删除。

4
这是针对从Google搜索关于在Spring Boot/Hibernate中为什么他们的删除方法无法正常工作的任何人,无论是从JpaRepository/CrudRepository的delete使用还是从自定义存储库调用session.delete(entity)entityManager.remove(entity)

我正在升级从Spring Boot 1.5到版本2.2.6(和Hibernate 5.4.13),并且一直在使用自定义配置的transactionManager,类似于以下内容:
@Bean
public HibernateTransactionManager transactionManager(EntityManagerFactory entityManagerFactory) {
    return new HibernateTransactionManager(entityManagerFactory.unwrap(SessionFactory.class));
}

我通过使用@EnableTransactionManagement并删除上面自定义的transactionManager bean定义成功解决了它。

如果您仍然需要使用某种自定义事务管理器,将bean定义更改为下面的代码也可能有效:

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

作为最后的注意事项,请记得启用Spring Boot的自动配置,这样entityManagerFactory bean就可以自动创建了。如果你正在升级到entityManager,请删除任何sessionFactory bean,否则Spring Boot将无法正确进行自动配置。最后,请确保你的方法是@Transactional的,如果你不是手动处理事务的话。

3

我也刚经历了这个问题。在我的情况下,我必须给子表添加一个可为空的外键字段,然后通过设置null来解除与父表的关系,接着调用save、delete和flush方法。

在执行这些操作之前,我没有看到日志中有任何删除或异常信息。


我也遇到了同样的问题,并找到了这个线程。我不知道是否应该将其称为解决方案或解决方法,但以下是如何删除子对象。 - Brian Kates

2
如果您使用较新版本的Spring Data,则可以使用deleteBy语法...因此,您可以删除其中一个注释:P
接下来是,这种行为已经被Jira票据跟踪: https://jira.spring.io/browse/DATAJPA-727

2
@Transactional
int deleteAuthorByName(String name);

你应该在继承JpaRepository的Repository中写上@Transactional。

1

我有同样的问题,测试是正常的,但数据库行没有被删除。

你是否在方法上添加了@Transactional注解?对我来说,这个改变使它能够工作。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接