《Spring Boot + JPA 实战》第3招 Entity怎么建
## 01、痛点 早上被同事拉住,说订单服务升级后,数据库里订单状态从 0 变成了 2,历史数据全乱套了。我是老李。这类问题我们在过去踩过不止一次:有人把 Entity 只当成数据库表的 Java 翻译版本,Hibernate 自动建表后看都不看,结果字段类型、空值约束、枚举取值全都跑偏。现在公司准备做订单系统第一版,如果不把 Entity 当成领域对象和关系模型之间的持久化契约,后面每一次发版都可能变成救火。 很多团队一上来就让 Hibernate 自动生成 Schema,开发时很方便。但订单系统涉及金额、状态、地址等核心概念,一旦表结构被自动生成绑架,后期补约束、改字段类型、重建索引都非常痛苦。更麻烦的是,Enum 默认用 ordinal 存值,表面上没问题,一旦枚举顺序调整,数据库里的 0、1、2 就失去了业务含义。 ## 02、是什么 Entity 不是数据库表的 Java 翻译版本,而是领域对象和关系模型之间的持久化契约。这句话的重点是“契约”二字:领域对象有自己的业务语言,数据库表有自己的存储结构,二者之间不是简单的一一照搬,而是通过显式规则建立受控映射。 常用映射规则包括:@Entity 标记实体;@Table 明确表名、唯一约束和索引;@Id 定义主键;@GeneratedValue 定义主键生成策略;@Column 定义列名、长度、精度、可空性;@Enumerated 定义枚举持久化方式;@Embedded 与 @Embeddable 定义值对象;@MappedSuperclass 抽取公共映射字段。类型上,金额统一使用 BigDecimal,时间使用 LocalDateTime。这里的列定义还要考虑 NULL、默认值、唯一约束和数据库字段类型,而不是让框架猜。 比如 @Column 的 nullable、length、precision、scale、columnDefinition 可以直接影响数据库结构。@Table 的 uniqueConstraints 可以声明业务唯一键。@Enumerated 可以选择 STRING 或 ORDINAL,建议用 STRING。@Embedded 让 Address 作为值对象被 Customer 复用,而不是拆成多个外键。@MappedSuperclass 适合放 id、版本号、创建时间等公共字段,避免每个实体重复声明。 ## 03、为什么 为什么不能把 Schema 完全交给自动生成?因为自动生成只能根据 Java 类型粗略推导数据库类型,但业务约束、索引策略、字段精度、命名规范、历史兼容都需要人工决策。例如 BigDecimal 默认可能生成不同精度,LocalDateTime 的时区表达各数据库不同,String 可能生成超长字段或错误字符集。枚举 ordinal 的问题更隐蔽:ordinal 保存枚举声明顺序,一旦枚举中插入新值,旧的 0/1 含义可能错位,数据无法自解释。 为什么现在要讲?因为订单系统第一版是很多团队从单体拆微服务、从简单 CRUD 转领域建模的起点。这个阶段把 Entity 设计好,后面做查询、报表、分库分表、数据迁移都会轻松;反过来,如果一开始就放任映射,后续每一张表都可能成为隐性债务。 ## 04、架构 订单系统第一版可设计五个核心实体:Customer、Address、Product、Order、OrderItem。其中 Address 使用 Value Object,不单独建表,而是作为列嵌入 Customer;订单状态使用枚举,建议 @Enumerated(EnumType.STRING);金额统一使用 BigDecimal。 实体关系上,Customer 一对多 Order,Order 一对多 OrderItem,Product 一对多 OrderItem。Customer 与 Address 是组合关系,Address 生命周期跟随 Customer。公共字段可以用 @MappedSuperclass 放在 BaseEntity 中,比如主键 id、创建时间 createdAt、更新时间 updatedAt。这样五个实体不用重复声明主键。 映射架构可以用一句话概括:领域对象表达业务语义,关系模型表达存储约束,Entity 映射是两者之间的翻译层。这个翻译层越清晰,系统越不容易腐化。订单状态不要用 int 或 ordinal 直接映射,而要定义 OrderStatus 枚举,并强制 STRING 持久化。金额不要用 double 或 float,BigDecimal 配合 precision 和 scale 才能保证分毫不差。 ## 05、实战 下面给出订单系统第一版的关键代码骨架。先看 Address 值对象: ```java @Embeddable public class Address { @Column(nullable = false, length = 120) private String street; @Column(nullable = false, length = 40) private String city; @Column(length = 20) private String zipCode; } ``` Customer 实体: ```java @Entity @Table(name = "customers", uniqueConstraints = @UniqueConstraint(name = "uk_customer_email", columnNames = "email")) public class Customer extends BaseEntity { @Column(nullable = false, length = 80) private String name; @Column(nullable = false, unique = true, length = 120) private String email; @Embedded private Address address; } ``` 订单状态枚举: ```java public enum OrderStatus { PENDING, PAID, SHIPPED, COMPLETED, CANCELLED } ``` Order 实体: ```java @Entity @Table(name = "orders") public class Order extends BaseEntity { @ManyToOne(optional = false) @JoinColumn(name = "customer_id", nullable = false) private Customer customer; @Enumerated(EnumType.STRING) @Column(nullable = false, length = 20) private OrderStatus status; @Column(nullable = false, precision = 18, scale = 2) private BigDecimal totalAmount; } ``` OrderItem 实体: ```java @Entity @Table(name = "order_items") public class OrderItem extends BaseEntity { @ManyToOne(optional = false) @JoinColumn(name = "order_id", nullable = false) private Order order; @ManyToOne(optional = false) @JoinColumn(name = "product_id", nullable = false) private Product product; @Column(nullable = false) private Integer quantity; @Column(nullable = false, precision = 18, scale = 2) private BigDecimal unitPrice; } ``` Product 实体: ```java @Entity @Table(name = "products") public class Product extends BaseEntity { @Column(nullable = false, length = 120) private String name; @Column(nullable = false, precision = 18, scale = 2) private BigDecimal price; } ``` BaseEntity 示例: ```java @MappedSuperclass public abstract class BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(nullable = false, updatable = false) private LocalDateTime createdAt; @Column(nullable = false) private LocalDateTime updatedAt; } ``` 这些代码写完后,不要立即提交,而是先用 Hibernate 生成 DDL,再人工审查生成的表、字段、约束和索引。重点检查:金额列是否都是 decimal(18,2);状态列是否是 varchar(20) 而不是 tinyint;唯一约束是否落在 email 上;外键是否都有索引;时间列是否与数据库时区策略一致。 ## 06、洞见 好的 Entity 设计首先表达业务语义,其次才是映射数据库。业务语义包括:Address 是不可变值对象,不应该有独立主键;OrderStatus 是业务状态机,不应该用 0/1/2 这种无意义数字;金额是精确十进制,不能因为 Java 类型方便就用 float 或 double。 为什么 Schema 不能完全交给 Hibernate 自动生成?因为自动生成解决的是“有没有表”,但解决不了“表合不合适”。Schema 不只是列和类型,它承载了业务约束。例如订单总额不能为负、邮箱不能重复、订单状态不能为空,这些规则应该显式出现在数据库层。自动生成通常会忽略默认值、检查约束、索引命名、字符集和排序规则,甚至在版本升级时直接删表重建或留下孤儿列。 为什么 Enum 不推荐直接使用 ordinal?ordinal 把枚举顺序当作持久化值,这意味着数据库里存的是 0、1、2。只要有人调整枚举顺序,比如在 PENDING 前插入 DRAFT,原来的 PAID 可能从 1 变成 2,历史数据全部错位。即便不调整顺序,数据库里的数字也完全失去可读性,排查问题要先翻代码。相比之下,EnumType.STRING 存储 PENDING、PAID 这样的字符串,可读性高,且不受顺序影响。如果担心空间,可以用数据库原生枚举或查找表,但绝不该用 ordinal。 人工审查 Schema 的目的,是把业务约束从代码层延伸到数据库层。代码可以改,数据库却很难改。好的持久化契约会在上生产前把字段长度、精度、空值、唯一键、外键索引都敲定下来,这样后续订单量增长时,不用半夜爬起来修表。 ## 07、行动 第一步,建立五个核心实体:Customer、Address、Product、Order、OrderItem。Address 用 @Embeddable,订单状态用 @Enumerated(EnumType.STRING),金额统一 BigDecimal。第二步,写 BaseEntity 抽取 id、createdAt、updatedAt。第三步,生成 DDL 后导出 SQL 脚本,逐表检查列类型、可空性、默认值和唯一约束。第四步,为外键和常用查询路径补索引,例如 orders.customer_id、order_items.order_id、order_items.product_id。第五步,用测试数据验证:插入正常订单、重复邮箱、空地址、非法状态,确认数据库约束生效。第六步,把最终 DDL 纳入版本管理,后续变更走迁移脚本,而不是继续依赖 Hibernate 自动更新。 另外建议团队成员在代码评审时,把 Entity 映射和 DDL 放在一起看。映射对了但数据库约束漏了,等于契约只签了一半。 ## 08、小结 这次聊了订单系统第一版的 Entity 设计。核心就一句话:Entity 不是数据库表的 Java 翻译版本,而是领域对象和关系模型之间的持久化契约。掌握 @Entity、@Table、@Id、@GeneratedValue、@Column、@Enumerated、@Embedded、@Embeddable、@MappedSuperclass 等常用映射,同时理解 NULL、默认值、唯一约束和数据库字段类型,才能避免自动生成带来的坑。金额用 BigDecimal,时间用 LocalDateTime,枚举用 STRING,Schema 要人工审查,这些都是订单系统上线前的基本功。 ## 09、链接与概念说明 - @Entity / @Table:JPA 实体与表映射核心注解,@Table 可声明唯一约束和索引。 - @Id / @GeneratedValue:主键与生成策略,GenerationType.IDENTITY 依赖数据库自增,适合单库;SEQUENCE 适合 PostgreSQL 等。 - @Column:列映射,可指定 name、nullable、length、precision、scale、columnDefinition。 - @Enumerated:枚举映射,EnumType.STRING 推荐;EnumType.ORDINAL 不推荐。 - @Embedded / @Embeddable:值对象映射,Address 不需要独立主键。 - @MappedSuperclass:公共字段抽取,不会生成单独表。 - BigDecimal:高精度十进制,订单金额等金融数据必须使用。 - LocalDateTime:日期时间,需注意数据库时区与驱动配置。 - 人工审查 DDL:使用数据库工具查看生成的表结构,确认字段类型、约束和索引。 参考可以查阅 Jakarta Persistence 规范、Hibernate ORM 官方文档、Spring Data JPA 官方文档。具体链接可搜索“Jakarta Persistence”“Hibernate ORM”“Spring Data JPA”获取。