JPA和Hibernate - 多对多关系的优雅实现
2025-01-22 08:19:30 1.8k 字 #hibernate This post is also available in English and alternative languages.之前都是使用 MyBatis 方式对数据进行持久化。现在项目中是通过注解方式(JPA/Hibernate) 进行对象关系映射,涉及 一对多、多对多。
以下通过一个实际案例,剖析如何使用 JPA/Hibernate 优雅地实现多对多关系。
1. Demo
案例涉及三个主要实体类:ShopArea
、ShopAreaCategory
和ShopAreaCategoryShopArea
。这些类分别代表商店区域、商店区域类别以及它们之间的关联。
1.1. Entity
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85
| @Data @Entity @EntityListeners(AuditingEntityListener.class) @GenericGenerator(name = "entityIdGenerator", strategy = "com.yxcheng.example22.util.EntityIdGenerator") public class ShopArea implements AbstractEntity {
@GeneratedValue(generator = "entityIdGenerator") @Id private Long id; private Integer companyId;
private String name;
@CreationTimestamp private Date createTime;
@UpdateTimestamp private Date updateTime;
@ManyToMany(fetch = FetchType.EAGER) @Fetch(FetchMode.SUBSELECT) @JoinTable(name = "shop_area_category_shop_area", joinColumns = {@JoinColumn(name = "shopAreaId", referencedColumnName = "id", insertable = false, updatable = false)}, inverseJoinColumns = {@JoinColumn(name = "categoryId", referencedColumnName = "id", insertable = false,updatable = false)}) private List<ShopAreaCategory> shopAreaCategories; }
@Data @Entity @Accessors(chain = true) @EntityListeners(AuditingEntityListener.class) @GenericGenerator(name = "entityIdGenerator", strategy = "com.yxcheng.example22.util.EntityIdGenerator") public class ShopAreaCategory implements AbstractEntity { @javax.persistence.Id @GeneratedValue(generator = "entityIdGenerator") private Long id;
private Integer companyId;
private String name; @CreatedDate private Date createTime; @LastModifiedDate private Date updateTime; }
@Data @Entity @IdClass(ShopAreaCategoryShopArea.Id.class) public class ShopAreaCategoryShopArea { @Data @NoArgsConstructor @AllArgsConstructor public static class Id implements Serializable { private static final long serialVersionUID = 1L; private Long categoryId; private Long shopAreaId; }
@javax.persistence.Id private Long categoryId;
@javax.persistence.Id private Long shopAreaId; }
|
1.2. Dao
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45
| @Repository public class ShopAreaCategoryDao { @Resource private JPAQueryFactory jpaQueryFactory; @Resource private EntityManager entityManager;
QShopAreaCategory qShopAreaCategory = QShopAreaCategory.shopAreaCategory;
public ShopAreaCategory save(ShopAreaCategory shopAreaCategory) { entityManager.persist(shopAreaCategory); return shopAreaCategory; }
public List<ShopAreaCategory> findByCompanyId(Integer companyId) { return jpaQueryFactory.selectFrom(qShopAreaCategory).where(qShopAreaCategory.companyId.eq(companyId)).fetch(); } }
@Repository public class ShopAreaDao { @Autowired private JPAQueryFactory jpaQueryFactory; @Autowired private EntityManager entityManager;
QShopArea qShopArea = QShopArea.shopArea;
public ShopArea save(ShopArea shopArea) { entityManager.persist(shopArea); return shopArea; }
public List<ShopArea> findByCompanyId(Integer companyId) { return jpaQueryFactory.selectFrom(qShopArea) .where(qShopArea.companyId.eq(companyId)) .fetch(); }
public ShopArea findByCompanyIdAndName(Integer companyId, String name) { return jpaQueryFactory.selectFrom(qShopArea) .where(qShopArea.companyId.eq(companyId)) .where(qShopArea.name.eq(name)).fetchFirst(); } }
|
1.3. Service
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
| @Slf4j @Service @Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRED) public class ShopAreaService { @Resource private ShopAreaDao shopAreaDao;
public ShopArea save(ShopAreaSaveDTO reqDTO) { ShopArea existShopArea = shopAreaDao.findByCompanyIdAndName(reqDTO.getCompanyId(), reqDTO.getName()); if (Objects.nonNull(existShopArea)) { throw new BusinessException("商店区域名称重复"); } ShopArea shopAreaEntity = new ShopArea(); BeanUtils.copyProperties(reqDTO, shopAreaEntity); shopAreaEntity.setShopAreaCategories(assembleCategoryByIds(reqDTO.getCategoryIds())); return shopAreaDao.save(shopAreaEntity); }
private List<ShopAreaCategory> assembleCategoryByIds(List<Long> ids) { if (CollectionUtils.isEmpty(ids)) { return new ArrayList<>(); } return ids.stream().map(i -> new ShopAreaCategory().setId(i)).collect(Collectors.toList()); }
public List<ShopArea> find(Integer companyId) { return shopAreaDao.findByCompanyId(companyId); } }
@Slf4j @Service @Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRED) public class ShopAreaCategoryService { @Autowired private ShopAreaCategoryDao shopAreaCategoryDao;
public ShopAreaCategory save(ShopAreaCategory shopAreaCategory) { return shopAreaCategoryDao.save(shopAreaCategory); }
public List<ShopAreaCategory> findByCompanyId(Integer companyId) { return shopAreaCategoryDao.findByCompanyId(companyId); } }
|
1.4. Test
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
| @Slf4j @SpringBootTest @RunWith(SpringRunner.class) public class ApplicationSpringBootExampleTest22 {
@Resource private ShopAreaCategoryService shopAreaCategoryService;
@Resource private ShopAreaService shopAreaService; @Test public void saveShopAreaCategory() { ShopAreaCategory shopAreaCategory1 = new ShopAreaCategory().setCompanyId(9527).setName("区域分类1"); ShopAreaCategory shopAreaCategory2 = new ShopAreaCategory().setCompanyId(9527).setName("区域分类2"); ShopAreaCategory shopAreaCategory3 = new ShopAreaCategory().setCompanyId(9527).setName("区域分类3"); ArrayList<ShopAreaCategory> shopAreaCategories = CollectionUtil.newArrayListInstance(shopAreaCategory1, shopAreaCategory2, shopAreaCategory3); for (ShopAreaCategory shopAreaCategory : shopAreaCategories) { shopAreaCategoryService.save(shopAreaCategory); } }
@Test public void findShopAreaCategory() { List<ShopAreaCategory> shopAreaCategories = shopAreaCategoryService.findByCompanyId(9527); shopAreaCategories.forEach(shopAreaCategory -> log.info("shopAreaCategory:{}", JsonUtil.beanToJson(shopAreaCategory))); }
@Test public void saveShopArea() { List<Long> categoryIds = shopAreaCategoryService.findByCompanyId(9527).stream().map(ShopAreaCategory::getId) .collect(Collectors.toList());
ShopAreaSaveDTO shopAreaSaveDTO = new ShopAreaSaveDTO(); shopAreaSaveDTO.setName("商店区域名称1"); shopAreaSaveDTO.setCompanyId(9527); shopAreaSaveDTO.setCategoryIds(categoryIds); shopAreaService.save(shopAreaSaveDTO); }
@Test public void testQueryShopArea() { List<ShopArea> shopAreas = shopAreaService.find(9527); shopAreas.forEach(shopArea -> log.info("shopArea:{}", JsonUtil.beanToJson(shopArea))); } }
|
1.5. 建表SQL
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| create table shop_area ( id bigint unsigned not null comment '主键id' primary key, company_id int not null comment '公司id', name varchar(50) null comment '名称', create_time datetime not null comment '创建时间', update_time datetime not null comment '更新时间' ) comment '商店区域';
create table shop_area_category ( id bigint not null comment '主键id' primary key, company_id int not null comment '公司id', name varchar(50) null comment '商店区域分类名称', create_time datetime default CURRENT_TIMESTAMP null comment '创建时间', update_time datetime default CURRENT_TIMESTAMP null on update CURRENT_TIMESTAMP comment '更新时间' ) comment '商店区域分类';
create table shop_area_category_shop_area ( shop_area_id bigint not null comment '商店区域id', category_id bigint not null comment '商店区域分类id', primary key (category_id, shop_area_id) ) comment '商店区域分类_商店区域_关联表';
|
2. 注解
运行 saveShopArea
方法,在 entity 中设置 categoryIds
属性后,对应的数据会自动保存到 shop_area_category_shop_area
关系表中。
运行 testQueryShopArea
方法,输出 ShopArea
对象中对应关联的 ShopAreaCategory
也一起输出了。
这就是通过注解方式(Hibernate) 进行对象关系映射,一对多、多对多 关系的映射和查询。
2.1. ShopArea Entity
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| @Data @Entity @EntityListeners(AuditingEntityListener.class) @GenericGenerator(name = "entityIdGenerator", strategy = "com.yxcheng.example22.util.EntityIdGenerator") public class ShopArea implements AbstractEntity { @GeneratedValue(generator = "entityIdGenerator") @Id private Long id; @ManyToMany(fetch = FetchType.EAGER) @Fetch(FetchMode.SUBSELECT) @JoinTable(name = "shop_area_category_shop_area", joinColumns = {@JoinColumn(name = "shopAreaId", referencedColumnName = "id", insertable = false, updatable = false)}, inverseJoinColumns = {@JoinColumn(name = "categoryId", referencedColumnName = "id", insertable = false, updatable = false)}) private List<ShopAreaCategory> shopAreaCategories;
}
|
@Entity
: 表明该类是一个实体类,并且与数据库中的表对应。@EntityListeners(AuditingEntityListener.class)
: 启用实体审计功能,自动管理创建时间和更新时间。@GenericGenerator
: 自定义 ID 生成策略。@GeneratedValue
: 指定 ID 使用自定义生成器生成。@Id
: 标识该字段为主键。@ManyToMany(fetch = FetchType.EAGER)
: 定义与 ShopAreaCategory
的多对多关系,并且立即加载相关数据。@Fetch(FetchMode.SUBSELECT)
: 使用子查询来获取相关实体。@JoinTable
: 指定多对多关系的连接表和连接列。
2.2. ShopAreaCategory Entity
1 2 3 4 5 6 7 8 9 10 11 12
| @Data @Entity @Accessors(chain = true) @EntityListeners(AuditingEntityListener.class) @GenericGenerator(name = "entityIdGenerator", strategy = "com.yxcheng.example22.util.EntityIdGenerator") public class ShopAreaCategory implements AbstractEntity { @javax.persistence.Id @GeneratedValue(generator = "entityIdGenerator") private Long id;
}
|
ShopAreaCategory
Entity 中使用的注解与 ShopArea
中的相似。
2.3. ShopAreaCategoryShopArea Entity
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| @Data @Entity @IdClass(ShopAreaCategoryShopArea.Id.class) public class ShopAreaCategoryShopArea {
@Data @NoArgsConstructor @AllArgsConstructor public static class Id implements Serializable { private static final long serialVersionUID = 1L; private Long categoryId; private Long shopAreaId; }
@javax.persistence.Id private Long categoryId;
@javax.persistence.Id private Long shopAreaId; }
|
@IdClass
: 指定该实体的复合主键类。- 内部静态类
Id
用于表示复合主键。 @Id
: 标识这些字段为复合主键的一部分。
3. 实体关联分析
3.1. 保存操作
执行 saveShopArea
方法,保存ShopArea
实体时:
- 主实体会被直接保存到对应的表中。
- 设置了多对多关系,Hibernate 会自动处理中间表
shop_area_category_shop_area
的插入操作。
Hibernate会执行以下操作:
- 向
shop_area
表插入新记录。 - 向
shop_area_category_shop_area
表插入关联记录。
3.2. 查找操作
执行 testQueryShopArea
方法,查找ShopArea
实体时:
- 由于使用了
FetchType.EAGER
,关联的 ShopAreaCategory
实体会被立即加载。 @Fetch(FetchMode.SUBSELECT)
优化了加载过程,使用子查询而不是多次单独查询。
Hibernate会执行一个主查询和一个子查询:
- 从
shop_area
表获取主实体。 - 使用子查询从
shop_area_category
和 shop_area_category_shop_area
表获取关联的类别。
4. 性能考虑
- 立即加载(EAGER)可能会在某些场景下导致性能问题,特别是当关联实体数量很大时。在这种情况下,可以考虑使用延迟加载(LAZY)并结合其他查询优化技术。
@Fetch(FetchMode.SUBSELECT)
在处理大量数据时能显著提升性能,但也要注意可能带来的内存压力。