1. 概述

初学者常误以为 @NotNull@Column(nullable = false) 是一回事,可以随意替换使用。✅
但实际上,两者虽然最终都能防止字段存入 null 值,但作用机制和生效时机完全不同。❌

  • @NotNull运行时的数据校验,由 Bean Validation 规范驱动
  • @Column(nullable = false)数据库 schema 的元数据定义,影响 DDL 生成

本文将从实战角度对比这两个注解的行为差异,帮你避开常见“坑”。


2. 依赖环境

本文示例基于 Spring Boot 项目,核心依赖如下:

<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 示例实体类

定义一个简单的 Item 实体用于测试:

@Entity
public class Item {

    @Id
    @GeneratedValue
    private Long id;

    private BigDecimal price;
}

3. @NotNull 注解详解

@NotNull 来自 Bean Validation 规范(如 Hibernate Validator),属于通用校验注解,不仅限于 JPA 实体,任何 POJO 都可用。✅

3.1 使用示例

price 字段加上 @NotNull

@Entity
public class Item {
    @Id
    @GeneratedValue
    private Long id;

    @NotNull
    private BigDecimal price;
}

尝试保存 pricenull 的对象:

@SpringBootTest
public class ItemIntegrationTest {

    @Autowired
    private ItemRepository itemRepository;

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

执行结果抛出异常:

Caused by: javax.validation.ConstraintViolationException: 
Validation failed for classes [com.example.demo.Item] during persist time for groups [javax.validation.groups.Default,] 
List of constraint violations:[
    ConstraintViolationImpl{interpolatedMessage='must not be null', propertyPath=price, ...}
]

关键点分析 ✅

  • SQL 语句未执行:Hibernate 在 pre-persist 阶段触发 Bean Validation,校验失败直接中断流程
  • 异常类型为 ConstraintViolationException,属于应用层校验错误
  • 数据根本不会到达数据库,更安全、更早拦截

3.1 Schema 自动生成行为

若启用 Hibernate 自动建表(ddl-auto=create-drop):

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

生成的 SQL 为:

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

⚠️ 注意:price 列自动带上了 NOT NULL 约束!

原因揭秘

Hibernate 默认会将实体上的 Bean Validation 注解(如 @NotNull自动映射为数据库约束。这个行为由以下配置控制:

# 默认为 true,开启自动映射
spring.jpa.properties.hibernate.validator.apply_to_ddl=true

若想关闭此功能(比如希望 DB 层更宽松),可设置为 false

spring.jpa.properties.hibernate.validator.apply_to_ddl=false

此时生成的表结构中 price 将不再有 NOT NULL


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

@Column 是 JPA 规范的一部分,主要用于 定义数据库列的元数据,比如列名、长度、是否允许为空等。

4.1 使用示例

修改实体:

@Entity
public class Item {
    @Id
    @GeneratedValue
    private Long id;

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

再次执行保存 null 的测试:

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

输出日志片段:

Hibernate: 
    insert into item (price, id) values (?, ?)
    
WARN  SqlExceptionHelper: SQL Error: 23502, SQLState: 23502
ERROR SqlExceptionHelper: NULL not allowed for column "PRICE"

关键点分析 ✅

  • ✅ SQL INSERT 语句被成功生成并发送到了数据库
  • ❌ 最终由 数据库层报错拒绝插入
  • 异常为 DataIntegrityViolationException,属于数据库约束冲突

⚠️ 这意味着:应用已经完成了持久化流程的大部分工作,直到执行 SQL 才失败,资源浪费且响应延迟。

4.1 启用 Hibernate 层空值检查

Hibernate 实际上支持在 session flush 阶段对 @Column(nullable = false) 字段做空值检查,但默认不开启

需手动启用:

spring.jpa.properties.hibernate.check_nullability=true

开启后,再执行测试,抛出异常:

org.hibernate.PropertyValueException: 
not-null property references a null or transient value : com.example.demo.Item.price

此时,Hibernate 在生成 SQL 前就拦截了 null 值,不会发送到数据库。

但注意:这个功能是 Hibernate 特有的,不是 JPA 标准行为,可移植性差。


5. 总结与建议

对比维度 @NotNull @Column(nullable = false)
规范来源 Bean Validation JPA
校验时机 持久化前(Pre-persist) 可配置,但默认不校验
是否生成 DDL 约束 ✅ 默认生成(需 apply_to_ddl=true ✅ 生成
异常类型 ConstraintViolationException PropertyValueException 或数据库异常
是否发送 SQL ❌ 不发送(校验失败时) ✅ 默认会发送
跨平台兼容性 ✅ 高(标准 JSR-380) ⚠️ 依赖 Hibernate 特性

✅ 推荐做法

  1. 优先使用 @NotNull
    靠前拦截,减少无效数据库交互,符合“fail-fast”原则。

  2. 配合 validation 依赖使用
    确保 spring-boot-starter-validation 在 classpath 中。

  3. 保持 hibernate.validator.apply_to_ddl=true
    让应用层校验自动同步到数据库 schema,避免不一致。

  4. @Column(nullable = false) 仅用于纯 DDL 控制场景
    比如你不用 Bean Validation,或者只想控制建表。

⚠️ 踩坑提醒

  • 单独使用 @Column(nullable = false) 而不开启 check_nullability,等于把校验甩锅给数据库,性能差、体验差
  • 不引入 validation 依赖时,@NotNull 不会生效(只是个普通注解)
  • 多层校验时,注意 @Valid@NotNull 的组合使用,别漏掉级联校验

示例代码已托管至 GitHub:https://github.com/spring-examples/hibernate-notnull-vs-column


原始标题:Hibernate @NotNull vs @Column(nullable = false)