Hibernate中的@NotNull与@Column(nullable=false)

2023/05/18

1. 简介

乍一看,@NotNull和@Column(nullable = false)注解似乎都有相同的用途,并且可以互换使用。然而,正如我们很快就会看到的,这并不完全正确。

尽管在JPA实体上使用时,它们都基本上用于防止在底层数据库中存储空值,但这两种方法之间存在显著差异

在本快速教程中,我们将比较@NotNull和@Column(nullable = false)约束。

2. 依赖关系

对于所有呈现的示例,我们将使用一个简单的Spring Boot应用程序。

下面是pom.xml文件的相关部分,其中显示了所需的依赖项:

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-validation</artifactId>
    </dependency>
    <dependency>
        <groupId>com.h2database</groupId>
        <artifactId>h2</artifactId>
    </dependency>
</dependencies>

2.1 示例实体

我们还定义一个非常简单的实体,我们将在整个教程中使用它:

@Entity
public class Item {

    @Id
    @GeneratedValue
    private Long id;

    private BigDecimal price;
}

3. @NotNull注解

@NotNull注解在Bean Validation规范中定义,这意味着它的使用不仅限于实体。相反,我们也可以在任何其他bean上使用@NotNull注解。

不过,让我们坚持使用我们的用例,并将@NotNull注解添加到Item的prie字段:

@Entity
public class Item {

    @Id
    @GeneratedValue
    private Long id;

    @NotNull
    private BigDecimal price;
}

现在,让我们尝试保存一个价格为空的商品:

@SpringBootTest
public class ItemIntegrationTest {

    @Autowired
    private ItemRepository itemRepository;

    @Test
    public void shouldNotAllowToPersistNullItemsPrice() {
        itemRepository.save(new Item());
    }
}

让我们看看Hibernate的输出:

Caused by: javax.validation.ConstraintViolationException: Validation failed for classes 
[cn.tuyucheng.taketoday.h2db.notnull.models.Item] during persist time for groups 
[javax.validation.groups.Default, ]
List of constraint violations:[
	ConstraintViolationImpl{interpolatedMessage='不能为null', propertyPath=price, 
	rootBeanClass=class cn.tuyucheng.taketoday.h2db.notnull.models.Item, 
	messageTemplate='{javax.validation.constraints.NotNull.message}'}

正如我们所看到的,在这种情况下,我们的系统抛出了javax.validation.ConstraintViolationException

重要的是要注意Hibernate没有触发SQL插入语句。因此,无效数据不会保存到数据库中。

这是因为在将查询发送到数据库之前,pre-persist实体生命周期事件触发了bean验证。

3.1 Schema生成

在上一节中,我们介绍了@NotNull验证的工作原理。

现在让我们看看如果让Hibernate为我们生成数据库Schema会发生什么

因此,我们将在application.properties文件中设置几个属性:

spring.jpa.hibernate.ddl-auto=create-drop
spring.jpa.show-sql=true

如果我们现在启动我们的应用程序,我们将看到DDL语句:

create table item
(
    id    bigint         not null,
    price decimal(19, 2) not null,
    primary key (id)
)

令人惊讶的是,Hibernate自动将not null约束添加到price列定义中

这怎么可能?

事实证明,Hibernate开箱即用地将应用于实体的bean验证注解转换为DDL模式元数据

这非常方便并且很有意义。如果我们将@NotNull应用于实体,我们很可能也希望使相应的数据库列不为空。

然而,如果出于任何原因,我们想要禁用此Hibernate功能,我们需要做的就是将hibernate.validator.apply_to_ddl属性设置为false

为了测试这一点,让我们更新我们的application.properties:

spring.jpa.hibernate.ddl-auto=create-drop
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.validator.apply_to_ddl=false

让我们运行应用程序并查看DDL语句:

create table "item"
(
    "id"    bigint         not null,
    "price" numeric(19, 2) not null,
    primary key ("id")
)

正如预期的那样,这次Hibernate没有将not null约束添加到price列

4. @Column(nullable = false)注解

@Column注解被定义为Java Persistence API规范的一部分

它主要用于DDL模式元数据生成。这意味着如果我们让Hibernate自动生成数据库模式,它会将not null约束应用于特定的数据库列

让我们使用@Column(nullable = false)更新我们的Item实体,看看它是如何工作的:

@Entity
public class Item {

    @Id
    @GeneratedValue
    private Long id;

    @Column(nullable = false)
    private BigDecimal price;
}

现在,我们可以尝试保存一个空price值:

@SpringBootTest
public class ItemIntegrationTest {

    @Autowired
    private ItemRepository itemRepository;

    @Test
    public void shouldNotAllowToPersistNullItemsPrice() {
        itemRepository.save(new Item());
    }
}

这是Hibernate输出的片段:

Hibernate: 
    
    create table "item" (
       "id" bigint not null,
        "price" numeric(19,2) not null,
        primary key ("id")
    )

(...)

Hibernate: 
    insert 
    into
        "item"
        ("price", "id") 
    values
        (?, ?)
18:28:32.791 [main] WARN  [o.h.e.jdbc.spi.SqlExceptionHelper] >>> SQL Error: 23502, SQLState: 23502 
18:28:32.791 [main] ERROR [o.h.e.jdbc.spi.SqlExceptionHelper] >>> NULL not allowed for column "price"; SQL statement:
insert into "item" ("price", "id") values (?, ?) [23502-214] 

首先,我们可以注意到Hibernate如我们预期的那样生成了带有not null约束的price列

此外,它还能够创建SQL插入查询并将其传递。因此,是底层数据库触发了错误

4.1 验证

几乎所有来源都强调@Column(nullable = false)仅用于模式DDL生成。

但是,Hibernate能够针对可能的空值执行实体验证,即使相应的字段仅使用@Column(nullable = false)注解也是如此

为了激活这个Hibernate特性,我们需要显式地将hibernate.check_nullability属性设置为true:

spring.jpa.show-sql=true
spring.jpa.properties.hibernate.check_nullability=true

现在让我们再次执行我们的测试用例并检查输出:

org.springframework.dao.DataIntegrityViolationException: 
not-null property references a null or transient value : cn.tuyucheng.taketoday.h2db.notnull.models.Item.price; 
nested exception is org.hibernate.PropertyValueException: 
not-null property references a null or transient value : cn.tuyucheng.taketoday.h2db.notnull.models.Item.price

这一次,我们的测试用例抛出了org.hibernate.PropertyValueException。

重要的是要注意,在这种情况下,Hibernate没有将插入SQL查询发送到数据库

5. 总结

在本文中,我们描述了@NotNull和@Column(nullable = false)注解的工作原理。

尽管它们都可以防止我们在数据库中存储空值,但它们采用了不同的方法。

根据经验,我们应该更倾向于@NotNull注解而不是@Column(nullable = false)注解。这样,我们确保在Hibernate向数据库发送任何插入或更新SQL查询之前进行验证。

此外,通常最好依赖Bean Validation中定义的标准规则,而不是让数据库处理验证逻辑。

但是,即使我们让Hibernate生成数据库模式,它也会将@NotNull注解转换为数据库约束。然后我们必须确保hibernate.validator.apply_to_ddl属性设置为true

与往常一样,本教程的完整源代码可在GitHub上获得。

Show Disqus Comments

Post Directory

扫码关注公众号:Taketoday
发送 290992
即可立即永久解锁本站全部文章