1. 概述

在这篇文章中,我们将探讨Hibernate的PropertyValueException。我们将特别关注"not-null property references a null or transient value"错误消息。

Hibernate主要在两种情况下抛出PropertyValueException

  • 当尝试将null值保存到标记为nullable = false的列时。
  • 当保存一个关联引用未保存实例的实体对象时。

2. Hibernate的空值检查

首先,让我们讨论一下Hibernate的@Column(nullable = false)注解。如果没有其他Bean Validation存在,我们可以依赖Hibernate的空值检查。

此外,我们可以通过设置hibernate.check_nullability = true来强制执行这种验证。为了重现以下示例,我们需要启用空值检查。

3. 保存null值到非空列

现在,让我们利用Hibernate的验证机制来重现一个简单场景中的错误。我们将尝试保存一个没有设置必填字段的@Entity。在这个例子中,我们将使用简单的Book类:

@Entity
public class Book {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    @Column(nullable = false)
    private String title;

    // getters and setters
}

title列的nullable标志设置为false。现在,如果我们不设置书名就试图保存Book对象,会抛出PropertyValueException

@Test
public void whenSavingEntityWithNullMandatoryField_thenThrowPropertyValueException() {    
    Book book = new Book();

    assertThatThrownBy(() -> session.save(book))
      .isInstanceOf(PropertyValueException.class)
      .hasMessageContaining("not-null property references a null or transient value");
}

因此,为了解决问题,我们需要在保存实体之前设置必填字段:book.setTitle("Clean Code")

4. 保存关联引用未保存实例

在这一节,我们将探讨一个更复杂场景中的常见情况。在这个例子中,我们将使用AuthorArticle实体,它们之间有一个双向关系:

@Entity
public class Author {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private long id;

    private String name;

    @OneToMany
    @Cascade(CascadeType.ALL)
    private List<Article> articles;

    // constructor, getters and setters
}

articles字段带有@Cascade(CascadeType.ALL)注解。因此,当我们保存Author实体时,操作会传播到所有Article对象:

@Entity
public class Article {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private long id;

    private String title;

    @ManyToOne(optional = false)
    private Author author;

    // constructor, getters and setters
}

现在,让我们尝试保存一个Author和一些Articles,看看会发生什么:

@Test
public void whenSavingBidirectionalEntityiesithCorrectParent_thenDoNotThrowException() {
    Author author = new Author("John Doe");
    author.setArticles(asList(new Article("Java tutorial"), new Article("What's new in JUnit5")));

    assertThatThrownBy(() -> session.save(author))
      .isInstanceOf(PropertyValueException.class)
      .hasMessageContaining("not-null property references a null or transient value");
}

在处理双向关系时,我们可能会犯忘记从两个方向更新关联对象的常见错误。如果我们在Author类的setter方法中更新所有子articles,就可以避免这种情况。

为了展示文章中提到的所有用例,我们将创建一个不同的方法。然而,从父实体的setter方法中设置这些字段是一个好习惯:

public void addArticles(List<Article> articles) {
    this.articles = articles;
    articles.forEach(article -> article.setAuthor(this));
}

现在,我们可以使用新的设置方法,并期望不会出现错误:

@Test
public void whenSavingBidirectionalEntitesWithCorrectParent_thenDoNotThrowException() {
    Author author = new Author("John Doe");
    author.addArticles(asList(new Article("Java tutorial"), new Article("What's new in JUnit5")));

    session.save(author);
}

5. 总结

在这篇文章中,我们了解了Hibernate验证机制的工作原理。首先,我们学习了如何在项目中启用nullability checking。然后,我们举例说明了导致PropertyValueException的主要原因,并学习了如何修复这些问题。

如往常一样,源代码可以在GitHub上找到。