应使用包装类(Integer、Boolean等)替代基本类型以真实感知数据库NULL,配合ORM显式配置(如@Nullable、callSettersOnNulls=true)确保null穿透至业务层,并在Service层按需做空值兜底而非污染实体语义。

要让实体类正确对接旧库中已存的 NULL 字段并支持安全更新,核心不是“抹掉 null”,而是让 Java 层能真实感知、区分并可控处理数据库里的空值。关键在类型语义对齐 + ORM 行为显式化,而不是靠默认值掩盖问题。
用包装类替代基本类型,暴露数据库真实状态
旧库字段允许 NULL,但实体若声明为 int age 或 boolean enabled,JPA/Hibernate 在查询时会静默转成 0 / false;反向更新时,哪怕你传了 null,也可能被忽略或报错。必须改为:
- Integer age、Boolean enabled、LocalDateTime updatedAt
- String 类型本身可空,无需改动;但注意不要把 String code 错误地初始化为 "",否则无法区分“空字符串”和“未填写”
- 主键、外键、时间戳等业务上非空字段,仍可用基本类型(如 long id),但需确保数据库 DDL 中有 NOT NULL 约束
配置 ORM 显式映射 NULL,禁用隐式兜底
避免框架自动“补零”或“填 false”,让 null 值穿透到业务逻辑层:
- JPA/Hibernate:在字段上加 @Column(nullable = true),并确认实体类无参构造器不给这些字段赋初值(如 private Integer status = 0; 就是错误的)
- MyBatis:全局设置 callSettersOnNulls=true,确保数据库返回 NULL 时一定调用 setter(null),而不是跳过
- MyBatis-Plus:在 @TableField 中明确写 fill = FieldFill.DEFAULT 或留空,避免自动填充干扰原始 NULL
更新时保留 NULL 意图,不强行覆盖
前端传参或 DTO 转实体时,若某字段没传(JSON 缺失或为 null),应保持其为 null,而非塞入默认值:
- 禁止在 setter 里写 this.age = age == null ? 18 : age; —— 这会让“用户没填”和“用户明确填了 18”完全不可分
- 更新操作前做判空:if (user.getPhone() == null) { updateWrapper.set("phone", null); },让 SQL 真正执行 SET phone = NULL
- 使用动态 SQL(MyBatis)或 Example(JPA)只更新非 null 字段,避免把已有值误覆为默认值
兼容旧数据的兜底策略放在 Service 层
真正需要“null → 默认值”的场景(如统计报表要求 age 不为空),应在 Service 或 VO 转换环节做,而不是污染实体语义:
- public int getAgeForReport() { return Objects.requireNonNullElse(age, 0); }
- DTO 构建时用 MapStruct 的 @Mapping(defaultValue = "0") 或自定义转换器
- 数据库迁移脚本可批量补全历史 NULL(如 UPDATE users SET age = 0 WHERE age IS NULL AND status = 'active'),但这是数据治理动作,不应由 Java 实体承担

















