实体映射深入
一、一对一映射(@OneToOne)
@OneToOne 建立两个实体之间的一对一关系,例如用户对应身份证信息。
1. 主表关联(外键列)
主动方(owning side)实体表中添加外键列指向被关联实体主键。
java
@Entity
@Table(name = "users")
public class User {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String username;
@OneToOne(cascade = CascadeType.ALL)
@JoinColumn(name = "id_card_id", referencedColumnName = "id")
private IdCard idCard;
}
@Entity
@Table(name = "id_cards")
public class IdCard {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String cardNumber;
@OneToOne(mappedBy = "idCard")
private User user;
}sql
CREATE TABLE users (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
username VARCHAR(50),
id_card_id BIGINT UNIQUE,
FOREIGN KEY (id_card_id) REFERENCES id_cards(id)
);
CREATE TABLE id_cards (id BIGINT PRIMARY KEY AUTO_INCREMENT, card_number VARCHAR(18));关键点:
@JoinColumn放在拥有外键的一方(owning side)。mappedBy放在反方,指向主动方的字段名。- 外键列应添加
UNIQUE约束保证一对一语义。
2. 外键关联(被控端)
外键列存在于被关联方表中。
java
@Entity
@Table(name = "users")
public class User {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String username;
@OneToOne(mappedBy = "user")
private IdCard idCard;
}
@Entity
@Table(name = "id_cards")
public class IdCard {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String cardNumber;
@OneToOne
@JoinColumn(name = "user_id")
private User user;
}sql
CREATE TABLE id_cards (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
card_number VARCHAR(18),
user_id BIGINT UNIQUE,
FOREIGN KEY (user_id) REFERENCES users(id)
);3. 共享主键关联
子表主键同时作为外键引用父表主键,使用 @MapsId 实现。
java
@Entity
@Table(name = "users")
public class User {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String username;
@OneToOne(mappedBy = "user", cascade = CascadeType.ALL)
private UserProfile profile;
}
@Entity
@Table(name = "user_profiles")
public class UserProfile {
@Id
private Long id;
private String avatarUrl;
private String bio;
@OneToOne
@MapsId
@JoinColumn(name = "id")
private User user;
}sql
CREATE TABLE users (id BIGINT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50));
CREATE TABLE user_profiles (
id BIGINT PRIMARY KEY,
avatar_url VARCHAR(255), bio TEXT,
FOREIGN KEY (id) REFERENCES users(id)
);@MapsId 作用:子表主键同时作为外键,与父表主键保持一致,无需额外外键列。
二、一对多映射(@OneToMany / @ManyToOne)
单向一对多(不推荐)
java
@Entity @Table(name = "categories")
public class Category {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@OneToMany
@JoinColumn(name = "category_id")
private List<Product> products = new ArrayList<>();
}注意: 单向 @OneToMany 导致 JPA 先 INSERT 再 UPDATE 外键列,性能较差。
双向一对多(推荐)
java
@Entity @Table(name = "categories")
public class Category {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@OneToMany(mappedBy = "category", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Product> products = new ArrayList<>();
public void addProduct(Product product) {
products.add(product);
product.setCategory(this);
}
public void removeProduct(Product product) {
products.remove(product);
product.setCategory(null);
}
}
@Entity @Table(name = "products")
public class Product {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String title;
private BigDecimal price;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "category_id")
private Category category;
}sql
CREATE TABLE categories (id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(100));
CREATE TABLE products (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
title VARCHAR(200), price DECIMAL(10,2),
category_id BIGINT,
FOREIGN KEY (category_id) REFERENCES categories(id)
);最佳实践:
| 建议 | 原因 |
|---|---|
使用 mappedBy 让 @ManyToOne 方成为主动方 | 避免多余 UPDATE |
@ManyToOne 重设为 FetchType.LAZY | 避免 N+1 问题 |
提供 addXxx() / removeXxx() 工具方法 | 维护对象图一致性 |
初始化集合为 ArrayList<>() | 避免 null 问题 |
三、多对多映射(@ManyToMany)
双向多对多
java
@Entity @Table(name = "products")
public class Product {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String title;
@ManyToMany
@JoinTable(
name = "product_tag",
joinColumns = @JoinColumn(name = "product_id"),
inverseJoinColumns = @JoinColumn(name = "tag_id")
)
private Set<Tag> tags = new HashSet<>();
}
@Entity @Table(name = "tags")
public class Tag {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@ManyToMany(mappedBy = "tags")
private Set<Product> products = new HashSet<>();
}sql
CREATE TABLE product_tag (
product_id BIGINT NOT NULL,
tag_id BIGINT NOT NULL,
PRIMARY KEY (product_id, tag_id),
FOREIGN KEY (product_id) REFERENCES products(id),
FOREIGN KEY (tag_id) REFERENCES tags(id)
);关键说明:
@JoinTable.name:中间表名;joinColumns:当前实体外键列;inverseJoinColumns:对端实体外键列。- 建议用
Set<>而非List<>避免重复数据。 mappedBy放在被控端,默认FetchType.LAZY。
带额外字段的关联表
中间表需要额外字段时,拆分为两个 @OneToMany + @ManyToOne:
java
@Entity @Table(name = "product_tag")
public class ProductTag {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "product_id")
private Product product;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "tag_id")
private Tag tag;
private Integer sortOrder;
private LocalDateTime createdAt;
}Product 和 Tag 中分别使用 @OneToMany(mappedBy = "product") 和 @OneToMany(mappedBy = "tag")。
四、@JoinColumn 详解
指定关联的外键列,可用于 @OneToOne、@ManyToOne、单向 @OneToMany 及 @JoinTable。
属性一览
java
@Repeatable(JoinColumns.class)
public @interface JoinColumn {
String name() default "";
String referencedColumnName() default "";
boolean unique() default false;
boolean nullable() default true;
boolean insertable() default true;
boolean updatable() default true;
String columnDefinition() default "";
String table() default "";
ForeignKey foreignKey() default @ForeignKey(PROVIDER_DEFAULT);
}使用示例
java
@Entity public class Order {
@Id private Long id;
@ManyToOne
@JoinColumn(
name = "buyer_id",
referencedColumnName = "user_id",
nullable = false,
foreignKey = @ForeignKey(ConstraintMode.NO_CONSTRAINT)
)
private User buyer;
}referencedColumnName 可引用关联实体的非主键列(需有 UNIQUE 约束):
java
@Entity public class Employee {
@Id private Long id;
private String employeeNo; // 业务唯一键
}
@Entity public class Workstation {
@ManyToOne
@JoinColumn(name = "emp_no", referencedColumnName = "employeeNo")
private Employee employee;
}五、@JoinTable 详解
属性一览
java
public @interface JoinTable {
String name() default "";
String catalog() default "";
String schema() default "";
JoinColumn[] joinColumns() default {};
JoinColumn[] inverseJoinColumns() default {};
ForeignKey foreignKey() default @ForeignKey(PROVIDER_DEFAULT);
ForeignKey inverseForeignKey() default @ForeignKey(PROVIDER_DEFAULT);
UniqueConstraint uniqueConstraints() default {};
Index[] indexes() default {};
}复杂示例
java
@Entity @Table(name = "students")
public class Student {
@Id private Long id;
@ManyToMany
@JoinTable(
name = "student_course", schema = "academic",
joinColumns = @JoinColumn(name = "student_id"),
inverseJoinColumns = @JoinColumn(name = "course_id"),
uniqueConstraints = @UniqueConstraint(columnNames = {"student_id", "course_id"}),
indexes = {
@Index(name = "idx_sc_sid", columnList = "student_id"),
@Index(name = "idx_sc_cid", columnList = "course_id")
}
)
private Set<Course> courses = new HashSet<>();
}sql
CREATE TABLE academic.student_course (
student_id BIGINT NOT NULL, course_id BIGINT NOT NULL,
PRIMARY KEY (student_id, course_id),
FOREIGN KEY (student_id) REFERENCES students(id),
FOREIGN KEY (course_id) REFERENCES courses(id),
CONSTRAINT uk_student_course UNIQUE (student_id, course_id)
);
CREATE INDEX idx_sc_sid ON academic.student_course(student_id);
CREATE INDEX idx_sc_cid ON academic.student_course(course_id);六、orphanRemoval 孤儿删除
orphanRemoval 用于 @OneToOne 和 @OneToMany,自动删除从集合中移除的子实体。
java
@Entity @Table(name = "orders")
public class Order {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@OneToMany(mappedBy = "order", cascade = CascadeType.ALL, orphanRemoval = true)
private List<OrderItem> items = new ArrayList<>();
public void removeItem(OrderItem item) {
items.remove(item);
item.setOrder(null);
}
}java
// 触发孤儿删除
Order order = entityManager.find(Order.class, 1L);
OrderItem item = order.getItems().get(0);
order.removeItem(item);
entityManager.flush();
// => DELETE FROM order_items WHERE id = ?orphanRemoval vs CascadeType.REMOVE
| 特性 | orphanRemoval = true | CascadeType.REMOVE |
|---|---|---|
| 触发时机 | 从集合移除时 | 父实体删除时 |
| 子实体独立删除 | 是 | 否 |
| 清理孤儿数据 | 是 | 否 |
| 适用场景 | 值对象、生命周期绑定 | 级联删除所有关联 |
注意:
- 仅适用于
@OneToOne和@OneToMany,不适用于@ManyToMany。 - 常与
cascade = CascadeType.ALL搭配使用。 - 误移出集合又未重新
persist,事务提交时子实体将被删除。
七、七种 CascadeType 级联策略对比
策略总览
java
public enum CascadeType {
ALL, // 全部
PERSIST, // 保存
MERGE, // 合并/更新
REMOVE, // 删除
REFRESH, // 刷新
DETACH // 分离
}详细对比
| 级联类型 | 父操作 | 对子实体的影响 | 示例场景 |
|---|---|---|---|
PERSIST | em.persist(parent) | 子实体自动被 persist | 新建订单时保存订单项 |
MERGE | em.merge(parent) | 子实体自动被 merge | 脱管实体合并时同步子实体 |
REMOVE | em.remove(parent) | 子实体自动被 remove | 删除订单时级联删除订单项 |
REFRESH | em.refresh(parent) | 子实体自动被 refresh | 重新读取时刷新子实体 |
DETACH | em.detach(parent) | 子实体自动被 detach | 父实体脱管时子实体一并脱管 |
ALL | 任意操作 | 以上全部生效 | 完全生命周期管理 |
常见组合推荐
java
// 强生命周期绑定——父实体完全拥有子实体
@OneToMany(mappedBy = "order", cascade = CascadeType.ALL, orphanRemoval = true)
// 引用其他聚合根——仅允许保存和更新,不级联删除
@ManyToOne(cascade = {CascadeType.PERSIST, CascadeType.MERGE})
// 只读引用——不做任何级联
@ManyToOne(cascade = {})
// 值对象——所有操作级联 + 孤儿删除
@OneToOne(cascade = CascadeType.ALL, orphanRemoval = true)重要警告
- 不要滥用
CascadeType.ALL:子实体被多个父实体共享时,REMOVE 可能导致意外删除。 REMOVEvsorphanRemoval:前者在显式删除父实体时触发;后者在从集合移除时触发。DETACH常被忽略:不级联脱管可能导致意外的延迟加载。
八、@MappedSuperclass 继承映射
将多个实体的公共字段抽取到父类中,不生成单独的表,子类各自映射到自己的表。
基本用法
java
@MappedSuperclass
public abstract class BaseEntity {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "created_at", updatable = false)
private LocalDateTime createdAt;
@Column(name = "updated_at")
private LocalDateTime updatedAt;
@PrePersist
protected void onCreate() {
this.createdAt = LocalDateTime.now();
this.updatedAt = this.createdAt;
}
@PreUpdate
protected void onUpdate() {
this.updatedAt = LocalDateTime.now();
}
}
@Entity @Table(name = "products")
public class Product extends BaseEntity {
private String title;
private BigDecimal price;
}
@Entity @Table(name = "categories")
public class Category extends BaseEntity {
private String name;
private String description;
}sql
CREATE TABLE products (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
created_at DATETIME, updated_at DATETIME,
title VARCHAR(200), price DECIMAL(10,2)
);
CREATE TABLE categories (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
created_at DATETIME, updated_at DATETIME,
name VARCHAR(100), description TEXT
);特性总结
| 特性 | 说明 |
|---|---|
| 是否生成表 | 不生成,纯模板作用 |
| 子类能否覆盖映射 | 可以,使用 @AttributeOverride |
能否包含 @Id / 关联 | 可以 |
| 能否独立查询 | 不能,FROM BaseEntity 无效 |
| 支持多级继承 | 支持 |
重写列映射
java
@MappedSuperclass
public abstract class BaseEntity {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "created_at")
private LocalDateTime createdAt;
}
@Entity @Table(name = "products")
@AttributeOverride(name = "createdAt", column = @Column(name = "product_created_at"))
public class Product extends BaseEntity {
private String title;
}@MappedSuperclass vs @Inheritance
| 对比维度 | @MappedSuperclass | @Inheritance |
|---|---|---|
| 表数量 | 每个子类一张表 | 取决于策略 |
| 多态查询 | 不支持 | 支持 |
| 父类能否包含关联 | 可以 | 可以 |
| 适用场景 | 抽取公共字段 | 真正继承体系 |
| ORM 复杂度 | 低 | 中~高 |
九、实战:电商订单实体关系设计
综合运用各种映射关系设计电商订单模型。
业务模型
Category (1) ──── (N) Product (1) ──── (N) OrderItem (N) ──── (1) Order
│ │
│ (N) │ (1)
└─── (N) Tag AddressCategory->Product:一对多Product->Tag:多对多Order->OrderItem:一对多OrderItem->Product:多对一Order->Address:一对一(共享主键)
实体代码
java
@MappedSuperclass
public abstract class BaseEntity {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "created_at", updatable = false)
private LocalDateTime createdAt;
@Column(name = "updated_at")
private LocalDateTime updatedAt;
@PrePersist
protected void onCreate() { this.createdAt = LocalDateTime.now(); this.updatedAt = this.createdAt; }
@PreUpdate
protected void onUpdate() { this.updatedAt = LocalDateTime.now(); }
}
@Entity @Table(name = "categories")
public class Category extends BaseEntity {
@Column(nullable = false, unique = true, length = 50)
private String name;
@Column(length = 500)
private String description;
@OneToMany(mappedBy = "category", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Product> products = new ArrayList<>();
public void addProduct(Product product) { products.add(product); product.setCategory(this); }
public void removeProduct(Product product) { products.remove(product); product.setCategory(null); }
}
@Entity @Table(name = "tags")
public class Tag extends BaseEntity {
@Column(nullable = false, unique = true, length = 30)
private String name;
@ManyToMany(mappedBy = "tags")
private Set<Product> products = new HashSet<>();
}
@Entity @Table(name = "products")
public class Product extends BaseEntity {
@Column(nullable = false, length = 200)
private String title;
@Column(nullable = false, precision = 10, scale = 2)
private BigDecimal price;
@Column(name = "stock_quantity", nullable = false)
private Integer stockQuantity;
@Column(length = 2000)
private String description;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "category_id")
private Category category;
@ManyToMany
@JoinTable(name = "product_tag",
joinColumns = @JoinColumn(name = "product_id"),
inverseJoinColumns = @JoinColumn(name = "tag_id"))
private Set<Tag> tags = new HashSet<>();
}
@Entity @Table(name = "addresses")
public class Address {
@Id private Long id;
@Column(nullable = false, length = 100) private String province;
@Column(nullable = false, length = 100) private String city;
@Column(nullable = false, length = 200) private String detail;
@Column(name = "phone", nullable = false, length = 20) private String phone;
@OneToOne @MapsId @JoinColumn(name = "id") private Order order;
}
@Entity @Table(name = "orders")
public class Order extends BaseEntity {
@Column(name = "order_no", nullable = false, unique = true, length = 32)
private String orderNo;
@Column(name = "total_amount", nullable = false, precision = 12, scale = 2)
private BigDecimal totalAmount;
@Column(length = 20, nullable = false)
private String status;
@OneToMany(mappedBy = "order", cascade = CascadeType.ALL, orphanRemoval = true)
private List<OrderItem> items = new ArrayList<>();
@OneToOne(mappedBy = "order", cascade = CascadeType.ALL, orphanRemoval = true)
private Address shippingAddress;
public void addItem(OrderItem item) { items.add(item); item.setOrder(this); }
public void removeItem(OrderItem item) { items.remove(item); item.setOrder(null); }
}
@Entity @Table(name = "order_items")
public class OrderItem extends BaseEntity {
@Column(name = "quantity", nullable = false) private Integer quantity;
@Column(name = "unit_price", nullable = false, precision = 10, scale = 2) private BigDecimal unitPrice;
@Column(name = "subtotal", nullable = false, precision = 12, scale = 2) private BigDecimal subtotal;
@ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "order_id", nullable = false) private Order order;
@ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "product_id", nullable = false) private Product product;
}完整表结构 DDL
sql
CREATE TABLE categories (
id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL UNIQUE,
description VARCHAR(500), created_at DATETIME NOT NULL, updated_at DATETIME NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE tags (
id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(30) NOT NULL UNIQUE,
created_at DATETIME NOT NULL, updated_at DATETIME NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE products (
id BIGINT PRIMARY KEY AUTO_INCREMENT, title VARCHAR(200) NOT NULL,
price DECIMAL(10,2) NOT NULL, stock_quantity INT NOT NULL DEFAULT 0,
description VARCHAR(2000), category_id BIGINT,
created_at DATETIME NOT NULL, updated_at DATETIME NOT NULL,
FOREIGN KEY (category_id) REFERENCES categories(id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE product_tag (
product_id BIGINT NOT NULL, tag_id BIGINT NOT NULL,
PRIMARY KEY (product_id, tag_id),
FOREIGN KEY (product_id) REFERENCES products(id),
FOREIGN KEY (tag_id) REFERENCES tags(id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE orders (
id BIGINT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL UNIQUE,
total_amount DECIMAL(12,2) NOT NULL, status VARCHAR(20) NOT NULL DEFAULT 'PENDING',
created_at DATETIME NOT NULL, updated_at DATETIME NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE order_items (
id BIGINT PRIMARY KEY AUTO_INCREMENT, quantity INT NOT NULL,
unit_price DECIMAL(10,2) NOT NULL, subtotal DECIMAL(12,2) NOT NULL,
order_id BIGINT NOT NULL, product_id BIGINT NOT NULL,
created_at DATETIME NOT NULL, updated_at DATETIME NOT NULL,
FOREIGN KEY (order_id) REFERENCES orders(id),
FOREIGN KEY (product_id) REFERENCES products(id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE addresses (
id BIGINT PRIMARY KEY, province VARCHAR(100) NOT NULL,
city VARCHAR(100) NOT NULL, detail VARCHAR(200) NOT NULL,
phone VARCHAR(20) NOT NULL, FOREIGN KEY (id) REFERENCES orders(id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;关系图
┌──────────────┐ ┌──────────────┐
│ Category │ │ Tag │
│ id (PK) │ 1 N │ id (PK) │
│ name │◄────────►│ name │
└──────┬───────┘ └──────┬───────┘
│ ┌──────────────┐ │
│ │ product_tag │ │ N
│ │ product_id │─────┘
│ │ tag_id │
│ └───────┬──────┘
│ N │
│ ┌───────┴───────┐ ┌──────────────┐
│ │ Product │ │ OrderItem │
└───►│ id (PK) │ │ id (PK) │
│ category_id │ │ order_id(FK) │
└───────┬───────┘ │ product_id(FK)│
│ 1 └───────┬───────┘
└────────────────────┘ N
┌──────────────────┘
│ N
┌────────┴──────────┐
│ Order │
│ id (PK), order_no │
└────────┬──────────┘
│ 1
┌────────┴──────────┐
│ Address │
│ id (PK,FK) @MapsId│
└───────────────────┘级联策略总结
| 关系 | 级联类型 | orphanRemoval | 说明 |
|---|---|---|---|
Category -> Product | CascadeType.ALL | true | 分类删除时级联删除商品 |
Product -> Tag | 无 | 不适用 | 标签是共享引用 |
Order -> OrderItem | CascadeType.ALL | true | 订单生命周期绑定订单项 |
Order -> Address | CascadeType.ALL | true | 地址生命周期绑定订单 |
OrderItem -> Product | 无 | 不适用 | 商品是独立聚合根 |
典型 CRUD 示例
java
Category category = new Category();
category.setName("电子产品");
Product phone = new Product();
phone.setTitle("智能手机");
phone.setPrice(new BigDecimal("2999.00"));
phone.setStockQuantity(100);
category.addProduct(phone);
entityManager.persist(category); // 级联保存 Product
Tag tag = new Tag();
tag.setName("热销");
entityManager.persist(tag);
Product product = entityManager.find(Product.class, phone.getId());
product.getTags().add(tag); // 建立多对多关联
Order order = new Order();
order.setOrderNo("ORD202607220001");
order.setStatus("PENDING");
OrderItem item = new OrderItem();
item.setQuantity(2);
item.setUnitPrice(phone.getPrice());
item.setSubtotal(phone.getPrice().multiply(new BigDecimal("2")));
item.setProduct(phone);
order.addItem(item);
Address address = new Address();
address.setProvince("广东省");
address.setCity("深圳市");
address.setDetail("南山区科技园");
address.setPhone("13800138000");
order.setShippingAddress(address);
entityManager.persist(order); // 级联保存 OrderItem + Address
order.setTotalAmount(order.getItems().stream()
.map(OrderItem::getSubtotal)
.reduce(BigDecimal.ZERO, BigDecimal::add));
entityManager.merge(order);总结
| 映射类型 | 核心注解 | 主动方选择 | 主要场景 |
|---|---|---|---|
| 一对一 | @OneToOne | 包含外键的一方 | 用户资料、收货地址 |
| 一对多 | @OneToMany + @ManyToOne | @ManyToOne 方 | 分类-商品、订单-订单项 |
| 多对多 | @ManyToMany | 任选一方 | 商品-标签、学生-课程 |
| 继承 | @MappedSuperclass | 不适用 | 公共时间戳字段抽取 |
| 关键实践建议: |
- 总是使用双向关联,用
mappedBy指定被控端,避免多余 SQL。 - 合理选择
FetchType,@ManyToOne默认 EAGER,按需改为 LAZY。 - 提供集合工具方法(
addXxx()/removeXxx()),维护对象图一致性。 - 小心级联删除,避免误删被共享引用的实体。
- 共享主键优先于外键列进行一对一关联,性能更优。
- 中间表拆分为实体,当需要额外字段时,将
@ManyToMany重构为@OneToMany+@ManyToOne。