Contents

hibernate @notnull vs @column(nullable = false)

1. 简介

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

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

在本快速教程中,我们将比较*@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注解添加到Itemprice*字段:

@Entity
public class Item {
    @Id
    @GeneratedValue
    private Long id;
    @NotNull
    private BigDecimal price;
}

现在,让我们尝试使用null  price来持久化一个项目:

@SpringBootTest
public class ItemIntegrationTest {
    @Autowired
    private ItemRepository itemRepository;
    @Test
    public void shouldNotAllowToPersistNullItemsPrice() {
        itemRepository.save(new Item());
    }
}

让我们看看 Hibernate 的输出:

2019-11-14 12:31:15.070 ERROR 10980 --- [ main] o.h.i.ExceptionMapperStandardImpl : 
HHH000346: Error during managed flush [Validation failed for classes 
[com.blogdemo.h2db.springboot.models.Item] during persist time for groups 
[javax.validation.groups.Default,] List of constraint violations:[
ConstraintViolationImpl{interpolatedMessage='must not be null', propertyPath=price, rootBeanClass=class 
com.blogdemo.h2db.springboot.models.Item, 
messageTemplate='{javax.validation.constraints.NotNull.message}'}]]
 
(...)
 
Caused by: javax.validation.ConstraintViolationException: Validation failed for classes 
[com.blogdemo.h2db.springboot.models.Item] during persist time for groups 
[javax.validation.groups.Default,] List of constraint violations:[
ConstraintViolationImpl{interpolatedMessage='must not be null', propertyPath=price, rootBeanClass=class 
com.blogdemo.h2db.springboot.models.Item, 
messageTemplate='{javax.validation.constraints.NotNull.message}'}]

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

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

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

3.1. 模式生成

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

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

出于这个原因,我们将在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 自动将非空约束添加到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 decimal(19,2),
    primary key (id)
)

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

4. *@Column(nullable = false)*注解

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

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

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

@Entity
public class Item {
    @Id
    @GeneratedValue
    private Long id;
    @Column(nullable = false)
    private BigDecimal 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 decimal(19,2) not null,
        primary key (id)
    )
(...)
Hibernate: 
    insert 
    into
        item
        (price, id) 
    values
        (?, ?)
2019-11-14 13:23:03.000  WARN 14580 --- [main] o.h.engine.jdbc.spi.SqlExceptionHelper   : 
SQL Error: 23502, SQLState: 23502
2019-11-14 13:23:03.000 ERROR 14580 --- [main] o.h.engine.jdbc.spi.SqlExceptionHelper   : 
NULL not allowed for column "PRICE"

首先,我们可以注意到Hibernate 生成的价格列具有我们预期的非空约束

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

4.1. 验证

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

然而,Hibernate 能够针对可能的null值执行实体验证,即使相应的字段仅使用@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 : com.blogdemo.h2db.springboot.models.Item.price; 
nested exception is org.hibernate.PropertyValueException: 
not-null property references a null or transient value : com.blogdemo.h2db.springboot.models.Item.price

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

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