3 多对多
- 承接上一篇博客 JPA三:关联映射(一)
概述
- 中间表:中间表中最少应该由两个字段组成,这两个字段做为外键指向两张表的主键,又组成了联合主键【多对多需要用到中间表】
步骤分析
- 案例:用户和角色
- 明确表关系
- 多对多关系
- 确定表关系(描述 外键或中间表,一对多用外键、多对多用中间表)
- 中间表
- 编写实体类,在实体类中描述表关系(即包含关系)
- 用户:包含角色的集合
- 角色:包含用户的集合
- 配置映射关系
- 使用 JPA 注解配置多对多映射关系
3.1 添加
-
参考文章开头的博客 JPA三:关联映射(一) ,基于该项目继续编码。
-
配置多对多时实体类切莫使用@Data注解,因为@Data注解包含toString方法,双方互相调用会产生StackOverflowError错误,这是个巨坑!!!
-
实体类
package cn.entity; import lombok.Getter; import lombok.Setter; import javax.persistence.*; import java.util.HashSet; import java.util.Set; @Entity @Table(name = "t_users") @Getter @Setter public class UserEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "user_id") private Long userId; @Column(name = "user_name") private String userName; @Column(name = "user_age") private Integer userAge; /*-------------------------------------- 多对多 --------------------------------------*/ /** * 配置用户到角色的多对多映射关系 * <p> * 1.声明表关系的配置 * @ManyToMany(targetEntity = Role.class) //多对多 * 2.配置中间表(包含两个外键) * @JoinTable name : 中间表的名称, * joinColumns:配置当前对象在中间表的外键;@JoinColumn name:外键名,referencedColumnName:参照的主表的主键名 * inverseJoinColumns:配置对方对象在中间表的外键;@JoinColumn name:外键名,referencedColumnName:参照的主表的主键名 */ @ManyToMany(targetEntity = RoleEntity.class) @JoinTable( name = "t_users_roles", // 下面,name=外键名(随便取名),referencedColumnName=参照的主表即t_users,主键为user_id joinColumns = {@JoinColumn(name = "t_user_id", referencedColumnName = "user_id")}, // 同理,下面name随便取名,referencedColumnName参照对方表t_roles的主键role_id inverseJoinColumns = {@JoinColumn(name = "t_role_id", referencedColumnName = "role_id")} ) private Set<RoleEntity> roles = new HashSet<RoleEntity>(); }
package cn.entity; import lombok.Getter; import lombok.Setter; import javax.persistence.*; import java.util.HashSet; import java.util.Set; @Entity @Table(name = "t_roles") @Getter @Setter public class RoleEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "role_id") private Long roleId; @Column(name = "role_name") private String roleName; /*-------------------------------------- 多对多 --------------------------------------*/ /** * 测试一:配置多对多 * name:要与UserEntity实体中配置的一致(即中间表是同一个) * joinColumns和inverseJoinColumns与UserEntity实体相反 */ // @ManyToMany(targetEntity = UserEntity.class) // @JoinTable( // name = "t_users_roles", // joinColumns = {@JoinColumn(name = "t_role_id", referencedColumnName = "role_id")}, // inverseJoinColumns = {@JoinColumn(name = "t_user_id", referencedColumnName = "user_id")} // ) // private Set<UserEntity> users = new HashSet<UserEntity>(); /** * 测试二:角色放弃维护权(被动的一方放弃) */ @ManyToMany(mappedBy = "roles") private Set<UserEntity> users = new HashSet<UserEntity>(); }
-
持久层
package cn.repository; import cn.entity.UserEntity; import org.springframework.data.jpa.repository.JpaRepository; public interface UserRepository extends JpaRepository<UserEntity, Long> { }
package cn.repository; import cn.entity.RoleEntity; import org.springframework.data.jpa.repository.JpaRepository; public interface RoleRepository extends JpaRepository<RoleEntity, Long> { }
-
测试
package test; import cn.App; import cn.entity.RoleEntity; import cn.entity.UserEntity; import cn.repository.RoleRepository; import cn.repository.UserRepository; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.annotation.Rollback; import javax.transaction.Transactional; @SpringBootTest(classes = App.class) public class TestMany2Many { @Autowired private UserRepository userRepository; @Autowired private RoleRepository roleRepository; /** * 测试1:保存一个用户,保存一个角色 * <p> * 测试环境要求: * 1.RoleEntity实体中使用 @ManyToMany 和 @JoinTable 两个注解进行多对多关系的映射 * 2.application.yml 中 jpa.hibernate.ddl-auto=create */ @Test @Transactional @Rollback(false) public void testAdd1() { UserEntity user = new UserEntity(); user.setUserName("小李"); user.setUserAge(23); RoleEntity role = new RoleEntity(); role.setRoleName("java工程师"); // 配置用户到角色的中间关系,可以对中间表中的数据进行维护 // user.getRoles().add(role); // 配置角色到用户的中间关系,可以对中间表中的数据进行维护 role.getUsers().add(user); /** * 对上面两个配置的解析 * 配置用户到角色的时候,对中间表进行维护(即插入一条数据) * 配置角色到用户的时候,对中间表也进行维护(即插入相同的一条数据) * 因为两个表的主键在中间表中组成了联合主键,插入两次相同的数据在中间表中造成主键冲突异常 * 即SQLIntegrityConstraintViolationException: Duplicate entry '1-1' for key 'sys_user_role.PRIMARY' * 因此只配置一个即可(即上面两行配置代码注释掉任意一行),或者需要一方放弃维护权(见下面 testAdd2) * 注:推荐使用被动的一方放弃维护权方法,即下面的 testAdd2 */ userRepository.save(user); roleRepository.save(role); /** * 控制台SQL * Hibernate: insert into t_users (user_age, user_name) values (?, ?) * Hibernate: insert into t_roles (role_name) values (?) * Hibernate: insert into t_users_roles (t_role_id, t_user_id) values (?, ?) */ } /** * 测试2:保存一个用户,保存一个角色 * <p> * 补充:多对多放弃维权时,应该是被动的一方放弃。角色被选择,因此角色放弃维护权 * <p> * 测试环境要求: * 1.RoleEntity实体中使用 @ManyToMany(mappedBy = "roles") * 2.application.yml 中 jpa.hibernate.ddl-auto=create */ @Test @Transactional @Rollback(false) public void testAdd2() { UserEntity user = new UserEntity(); user.setUserName("小王"); user.setUserAge(25); RoleEntity role = new RoleEntity(); role.setRoleName("go工程师"); user.getRoles().add(role); // 注释掉不可行,中间表无数据 // role.getUsers().add(user); // 注释掉可行,中间表有数据(角色放弃维权) /** * 经测试 * 1. 仅有 user.getRoles().add(role); ==> 中间表有数据 * 2. 仅有 role.getUsers().add(user); ==> 中间表无数据 * 3. user.getRoles().add(role); 和 role.getUsers().add(user); 都有 * ==> 中间表有数据(上面 testAdd1 若是两者都写报异常) * * 综上,若是能理清关系、写一句代码即可;若是理不清关系、两句写上也行(就是多一句废代码而已、不影响程序) * 自己理解:RoleEntity没有配置@JoinTable注解、放弃了维权,故不能用 role.getUsers().add(user); */ userRepository.save(user); roleRepository.save(role); /** * 控制台SQL * Hibernate: insert into t_users (user_age, user_name) values (?, ?) * Hibernate: insert into t_roles (role_name) values (?) * Hibernate: insert into t_users_roles (t_user_id, t_role_id) values (?, ?) */ } }
3.2 级联
-
修改实体类
package cn.entity; import lombok.Getter; import lombok.Setter; import javax.persistence.*; import java.util.HashSet; import java.util.Set; @Entity @Table(name = "t_users") @Getter @Setter public class UserEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "user_id") private Long userId; @Column(name = "user_name") private String userName; @Column(name = "user_age") private Integer userAge; /*-------------------------------------- 多对多 --------------------------------------*/ // @ManyToMany(targetEntity = RoleEntity.class) @ManyToMany(targetEntity = RoleEntity.class, cascade = CascadeType.ALL) // 级联 @JoinTable( name = "t_users_roles", // 下面,name=外键名(随便取名),referencedColumnName=参照的主表即t_users,主键为user_id joinColumns = {@JoinColumn(name = "t_user_id", referencedColumnName = "user_id")}, // 同理,下面name随便取名,referencedColumnName参照对方表t_roles的主键role_id inverseJoinColumns = {@JoinColumn(name = "t_role_id", referencedColumnName = "role_id")} ) private Set<RoleEntity> roles = new HashSet<RoleEntity>(); }
// RoleEntity 不变,代码同上,略
-
测试
package test; import cn.App; import cn.entity.RoleEntity; import cn.entity.UserEntity; import cn.repository.UserRepository; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.annotation.Rollback; import javax.transaction.Transactional; @SpringBootTest(classes = App.class) public class TestMany2ManyCascade { @Autowired private UserRepository userRepository; /** * 测试级联添加(保存一个用户的同时保存用户的关联角色) * <p> * 测试环境要求: * 1.UserEntity 实体中使用 @ManyToMany(targetEntity = RoleEntity.class, cascade = CascadeType.ALL) * 2.RoleEntity 实体中使用 @ManyToMany(mappedBy = "roles") 即可,即放弃维权 * 3.application.yml 中 jpa.hibernate.ddl-auto=create */ @Test @Transactional @Rollback(false) public void testCascadeAdd() { UserEntity user = new UserEntity(); user.setUserName("小赵"); user.setUserAge(26); RoleEntity role1 = new RoleEntity(); role1.setRoleName("java工程师"); RoleEntity role2 = new RoleEntity(); role2.setRoleName("项目经理"); /** * user到role必须配,role到user可以不配 * 因为 RoleEntity 没有配置 @JoinTable 注解,即表示已经放弃了维护权, * 若是弄不明白哪方配哪方不配,就最好俩都写上!! */ user.getRoles().add(role1); user.getRoles().add(role2); // role1.getUsers().add(user); // role2.getUsers().add(user); // 仅保存用户 userRepository.save(user); /** * 控制台SQL * Hibernate: insert into t_users (user_age, user_name) values (?, ?) * Hibernate: insert into t_roles (role_name) values (?) * Hibernate: insert into t_roles (role_name) values (?) * Hibernate: insert into t_users_roles (t_user_id, t_role_id) values (?, ?) * Hibernate: insert into t_users_roles (t_user_id, t_role_id) values (?, ?) * 5条insert,分别插入三张表和中间表的两条数据,经测试无误! */ } /** * 测试级联删除 * <p> * 测试环境要求: * 1.UserEntity 实体中使用 @ManyToMany(targetEntity = RoleEntity.class, cascade = CascadeType.ALL) * 2.RoleEntity 实体中使用 @ManyToMany(mappedBy = "roles") 即可,即放弃维权 * 3.application.yml 中 jpa.hibernate.ddl-auto=update * 因为若是create时重新建表数据库中没有数据;可以先测试下上面的级联添加,有数据后改为update,然后测试级联删除 */ @Test @Transactional @Rollback(false) public void testCascadeDelete() { /** * 步骤:先查询id=1的用户,再删除(且在角色表、中间表中删除所有关于他的角色信息) * 注:级联删除慎用!!! */ UserEntity user = userRepository.getById(1L); userRepository.delete(user); /** * 控制台SQL * Hibernate: select userentity0_.user_id as user_id1_3_0_, userentity0_.user_age as user_age2_3_0_, userentity0_.user_name as user_nam3_3_0_ from t_users userentity0_ where userentity0_.user_id=? * Hibernate: select roles0_.t_user_id as t_user_i1_4_0_, roles0_.t_role_id as t_role_i2_4_0_, roleentity1_.role_id as role_id1_2_1_, roleentity1_.role_name as role_nam2_2_1_ from t_users_roles roles0_ inner join t_roles roleentity1_ on roles0_.t_role_id=roleentity1_.role_id where roles0_.t_user_id=? * Hibernate: delete from t_users_roles where t_user_id=? * Hibernate: delete from t_roles where role_id=? * Hibernate: delete from t_roles where role_id=? * Hibernate: delete from t_users where user_id=? * 两条select先去查询,然后四条delete语句,其中第一个delete删除了2条数据,共五条全部删除。经测试无误! * 注:级联删除慎用!!! */ } }
4 级联查询
对象导航查询(即级联查询)
-
对象导航查询:查询一个对象的同时,通过此对象查询他的关联对象。
-
案例:即一对多的客户与联系人(因为一对多既能包含Set集合,又能包含对象属性)
-
注:为了防止
StackOverflowError
错误,在LinkManEntity
类上不要用@Data
注解,toString
方法要手动生成、且不能含输出CustomerEntity
。CustomerEntity
类上直接使用@ToString
即可,即双方的toString
方法中中不能相互包含彼此。 -
fetch
用于配置关联映射的加载方式:EAGER
(立即加载),LAZY
(延迟加载)。虽然可以配置立即加载,但是不推荐使用,因为它的关联对象无论是否用到都进行查询,增加了数据库的压力。
编码
-
修改实体类
CustomerEntity
重写toString
方法(直接使用了@ToString
注解);LinkManEntity
也重写toString
方法、注意不包含customer
属性。
package cn.entity; import lombok.Getter; import lombok.Setter; import lombok.ToString; import javax.persistence.*; import java.util.HashSet; import java.util.Set; @Entity @Table(name = "cst_customer") //@Data @Getter @Setter @ToString public class CustomerEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "cust_id") private Long custId; @Column(name = "cust_name") private String custName; // 客户名称(或公司名称) @Column(name = "cust_source") private String custSource; // 客户信息来源 @Column(name = "cust_industry") private String custIndustry; // 客户所属行业 @Column(name = "cust_level") private String custLevel; // 客户级别 @Column(name = "cust_address") private String custAddress; @Column(name = "cust_phone") private String custPhone; /*-------------------------------------- 一对多 --------------------------------------*/ // @OneToMany(mappedBy = "customer") // @OneToMany(mappedBy = "customer", cascade = CascadeType.ALL) // 级联配置 // @OneToMany(mappedBy = "customer", fetch = FetchType.EAGER) // 对象导航查询 @OneToMany(mappedBy = "customer", cascade = CascadeType.ALL, fetch = FetchType.LAZY) // 级联和对象导航查询 private Set<LinkManEntity> linkMans = new HashSet<LinkManEntity>(); }
package cn.entity; import lombok.Getter; import lombok.Setter; import javax.persistence.*; @Entity @Table(name = "cst_linkman") //@Data @Getter @Setter public class LinkManEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "lkm_id") private Long lkmId; @Column(name = "lkm_name") private String lkmName; @Column(name = "lkm_gender") private String lkmGender; @Column(name = "lkm_phone") private String lkmPhone; // 联系人办公电话 @Column(name = "lkm_mobile") private String lkmMobile; // 联系人手机号 @Column(name = "lkm_email") private String lkmEmail; @Column(name = "lkm_position") private String lkmPosition; // 联系人职位 @Column(name = "lkm_memo") private String lkmMemo; // 联系人备注 /*-------------------------------------- 一对多 --------------------------------------*/ @ManyToOne(targetEntity = CustomerEntity.class) @JoinColumn(name = "lkm_cust_id", referencedColumnName = "cust_id") private CustomerEntity customer; // 重新生成 toString 方法,对象导航查询时不要包含 CustomerEntity 属性 @Override public String toString() { return "LinkManEntity{" + "lkmId=" + lkmId + ", lkmName='" + lkmName + '\'' + ", lkmGender='" + lkmGender + '\'' + ", lkmPhone='" + lkmPhone + '\'' + ", lkmMobile='" + lkmMobile + '\'' + ", lkmEmail='" + lkmEmail + '\'' + ", lkmPosition='" + lkmPosition + '\'' + ", lkmMemo='" + lkmMemo + '\'' + '}'; } }
-
测试
package test; import cn.App; import cn.entity.CustomerEntity; import cn.entity.LinkManEntity; import cn.repository.CustomerRepository; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.annotation.Rollback; import javax.transaction.Transactional; import java.util.Arrays; import java.util.List; import java.util.Set; /** * 对象导航查询测试 */ @SpringBootTest(classes = App.class) public class TestFindCascade { @Autowired private CustomerRepository customerRepository; /** * 初始化一些数据,方便下面的对象导航查询(即级联查询) * <p> * 为了代码简便,因此采用级联插入,故需要配置cascade属性,而下面对象导航查询又需要配置fetch属性 * 因此 CustomerEntity 的配置为 @OneToMany(mappedBy = "customer", cascade = CascadeType.ALL, fetch = FetchType.LAZY) * <p> * 测试环境要求: * 1.CustomerEntity实体中使用 cascade 属性 (使用上面给的代码即可,也包含级联) * 2.application.yml 中 jpa.hibernate.ddl-auto=update * 此处直接改成 update,若是没的话建表,有的话无需建表直接插入数据、也方便下面的对象导航查询使用(无需再次更改配置) */ @Test @Transactional @Rollback(false) public void testInitData() { CustomerEntity customer = new CustomerEntity(); customer.setCustName("华为"); List<String> nameList = Arrays.asList("张三", "李四", "王五", "赵六", "田七"); for (String name : nameList) { LinkManEntity linkMan = new LinkManEntity(); linkMan.setLkmName(name); linkMan.setCustomer(customer); customer.getLinkMans().add(linkMan); } customerRepository.save(customer); } /** * 测试对象导航查询 * <p> * 测试环境要求: * 1.CustomerEntity实体中使用 fetch 属性 * 因为还要初始化级联添加,故改为 @OneToMany(mappedBy = "customer", cascade = CascadeType.ALL, fetch = FetchType.LAZY) * 2.application.yml 中 jpa.hibernate.ddl-auto=update * <p> * 因为若是create时重新建表数据库中没有数据; * 可以先运行上面的 testInitData() 方法添加一些数据,然后改为update测试对象导航查询 */ @Test @Transactional @Rollback(false) public void testQuery1() { // 查询id=1的客户 CustomerEntity customer = customerRepository.getById(1L); // 对象导航查询,此客户下的所有联系人 Set<LinkManEntity> linkMans = customer.getLinkMans(); for (LinkManEntity linkMan : linkMans) { System.out.println(linkMan); } /** * Hibernate: select customeren0_.cust_id as cust_id1_0_0_, customeren0_.cust_address as cust_add2_0_0_, customeren0_.cust_industry as cust_ind3_0_0_, customeren0_.cust_level as cust_lev4_0_0_, customeren0_.cust_name as cust_nam5_0_0_, customeren0_.cust_phone as cust_pho6_0_0_, customeren0_.cust_source as cust_sou7_0_0_ from cst_customer customeren0_ where customeren0_.cust_id=? * Hibernate: select linkmans0_.lkm_cust_id as lkm_cust9_1_0_, linkmans0_.lkm_id as lkm_id1_1_0_, linkmans0_.lkm_id as lkm_id1_1_1_, linkmans0_.lkm_cust_id as lkm_cust9_1_1_, linkmans0_.lkm_email as lkm_emai2_1_1_, linkmans0_.lkm_gender as lkm_gend3_1_1_, linkmans0_.lkm_memo as lkm_memo4_1_1_, linkmans0_.lkm_mobile as lkm_mobi5_1_1_, linkmans0_.lkm_name as lkm_name6_1_1_, linkmans0_.lkm_phone as lkm_phon7_1_1_, linkmans0_.lkm_position as lkm_posi8_1_1_ from cst_linkman linkmans0_ where linkmans0_.lkm_cust_id=? * LinkManEntity{lkmId=4, lkmName='张三', lkmGender='null', lkmPhone='null', lkmMobile='null', lkmEmail='null', lkmPosition='null', lkmMemo='null'} * LinkManEntity{lkmId=5, lkmName='王五', lkmGender='null', lkmPhone='null', lkmMobile='null', lkmEmail='null', lkmPosition='null', lkmMemo='null'} * LinkManEntity{lkmId=3, lkmName='李四', lkmGender='null', lkmPhone='null', lkmMobile='null', lkmEmail='null', lkmPosition='null', lkmMemo='null'} * LinkManEntity{lkmId=1, lkmName='赵六', lkmGender='null', lkmPhone='null', lkmMobile='null', lkmEmail='null', lkmPosition='null', lkmMemo='null'} * LinkManEntity{lkmId=2, lkmName='田七', lkmGender='null', lkmPhone='null', lkmMobile='null', lkmEmail='null', lkmPosition='null', lkmMemo='null'} * 对象导航查询成功 */ } }
补充
- "一"查"多"一定要配置延迟加载,因为若是只想查客户本身、用不到联系人的情况下,假如联系人很多会造成数据库压力且查出来毫无意义(因为用不到啊)
- "多"查"一"可以使用立即加载也可以使用延迟加载,因为仅多一条数据对数据库来说基本上没压力,而 Data JPA 在"多"查"一"时默认使用立即加载。
5 @Query 连接查询
- 因为连接查询与上面级联查询功能类似,故将该块知识总结在本文中。
-
SQL
-- 商品表 DROP TABLE IF EXISTS `t_product`; CREATE TABLE `t_product` ( `product_id` int NOT NULL AUTO_INCREMENT COMMENT '商品号', `product_name` varchar(10) DEFAULT NULL COMMENT '商品名', `product_price` double DEFAULT NULL COMMENT '商品价格', `product_factory` varchar(20) DEFAULT NULL COMMENT '厂家', PRIMARY KEY (`product_id`) ) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; INSERT INTO `t_product` VALUES (1,'冰箱',4999,'西门子'), (2,'空调',3000,'海尔'), (3,'电脑',4500,'戴尔'), (4,'电脑',4500,'联想'); -- 订单表 DROP TABLE IF EXISTS `t_order`; CREATE TABLE `t_order` ( `id` int NOT NULL AUTO_INCREMENT COMMENT 'id,自增', `order_id` int DEFAULT NULL COMMENT '订单id,订单id可能重复,这样即一个订单对应多个商品', `product_id` int DEFAULT NULL COMMENT '商品id', PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; INSERT INTO `t_order` VALUES (1,1,3),(2,1,4),(3,2,1),(4,1,1);
编码
-
实体类
package cn.entity; import lombok.ToString; import javax.persistence.*; @Entity @Table(name = "t_order") @ToString public class OrderEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "id") private Integer id; @Column(name = "order_id") private Integer orderId; @Column(name = "product_id") private Integer productId; }
package cn.entity; import lombok.Data; import javax.persistence.*; @Entity @Table(name = "t_product") @Data public class ProductEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "product_id") private Integer productId; @Column(name = "product_name") private String productName; @Column(name = "product_price") private Double productPrice; @Column(name = "product_factory") private String productFactory; // 厂家 }
-
DO层
package cn.pojo; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; /** * 多表连接查询返回结果 * 法一:普通java类 */ @Data @AllArgsConstructor @NoArgsConstructor public class OrderDO { /** * 多表查询,定义订单信息 * 因为在OrderDaoRepo的JPQL语句中会new该类 * 所以要提供全参构造函数 * 写了全参后,默认的无参将消失, * 因此一般情况下再写个无参构造函数 */ private Integer orderId; private Integer productId; private String productName; private Double productPrice; private String productFactory; }
package cn.pojo; import lombok.Data; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.Id; /** * 多表连接查询返回结果 * 法二:实体类,需要@Entity等注解 * <p> * JPA中,主键相同会导致查询的结果重复 * 因为订单号相同,因此此时不能将orderId添加@Id注解 * 商品号不同,因此可以改为productId添加@Id注解 * 由此可知,@Id注解标注的属性不一定要为数据库中的主键 * (虽然这样能出结果、但总感觉@Id这个注解不能乱用,等将来项目用到再深入研究。多表联合查询更推荐法一,返回结果仅为一个java普通类) * <p> * 注:该方法存在一个坑,主键相同会导致查询结果重复(即上面第二段描述的问题)!!!但感觉有些情况是无法避免没有重复列的~ * 例如:根据userId(形参为userId)查询某用户的所有订单、且每个订单中包含的所有商品信息 * 此时若是把orderId标注@Id注解,会导致商品结果重复; * 若是把productId标注@Id注解,因为不同订单可能含有相同商品,即productId也可能重复不适合做主键; * 其他属性也可能重复更不适合。 * (除非返回的DO中加个userId属性、把他标记为@Id ?!) */ @Entity @Data public class OrderDO2 { // @Id @Column(name = "order_id") private Integer orderId; @Id @Column(name = "product_id") private Integer productId; @Column(name = "product_name") private String productName; @Column(name = "product_price") private Double productPrice; @Column(name = "product_factory") private String productFactory; }
-
持久层
package cn.dao; import cn.entity.OrderEntity; import cn.pojo.OrderDO; import cn.pojo.OrderDO2; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import java.util.List; /** * 经测试,JpaRepository的第一个参数填 OrderEntity 或 ProductEntity 均可出结果 * <p> * 猜想1: * 应该是手写SQL进行多实体(即多表)查询时,填任意一个表的实体均可,但是第二个参数主键id类型要与前面填的实体对应 * <p> * 猜想2: * 标注@Query方法的返回值优先级高于继承的JpaRepository的第一个形参指定的实体类型 * 即在JPQL中 new 对象,那么就会按照所指定的对象返回吧,因此前面配置的返回值类型优先级就变低了 * 但是不能不写,若是不继承JpaRepository会报错,若是只写继承JpaRepository不指定里面的两个参数依旧报错! * 且不继承加@Repository注解当dao层也不行,因为只有接口没实现类注入时导致空指针异常! * <p> * 推荐写法 * 写连表查询中的任意一个实体(但推荐与接口名对应,如OrderDaoRepo里面就填OrderEntity),不要写个无关的UserEntity * <p> * 注:以上均为自己测试所得结果和自己总结的,可能存在不正确现象! * ============================================================================================= * <p> * 本项目中,直接使用 JPA 进行数据库操作的写在 repository 包下; 自己手写SQL的写在dao包下 */ //public interface OrderDao extends JpaRepository<ProductEntity, Integer> { public interface OrderDao extends JpaRepository<OrderEntity, Integer> { /** * 需求:根据订单号查询该订单下的所有商品信息 */ /** * 法一:返回值为普通java类(非实体、无@Entity注解) * 此时使用的是JPQL语言,注意操作对象是实体而非数据库中的表 * new 的对象要写全路径,否则报异常:QuerySyntaxException: Unable to locate class [OrderDO] * <p> * 注:new的是普通类OrderDO,而from的是两个实体、对应连接查询时数据库中的俩表 */ @Query(value = "SELECT new cn.pojo.OrderDO(" + " x.orderId,\n" + " x.productId,\n" + " y.productName,\n" + " y.productPrice,\n" + " y.productFactory)\n " + " FROM OrderEntity x\n" + " LEFT JOIN ProductEntity y\n" + " ON x.productId = y.productId\n" + " WHERE x.orderId = ?1") List<OrderDO> getOrderInfo(Integer orderId); }
package cn.dao; import cn.pojo.OrderDO2; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import java.util.List; public interface OrderDao2 extends JpaRepository<OrderDO2, Integer> { /** * 需求:根据订单号查询该订单下的所有商品信息 */ /** * 法二:返回值为实体(有@Entity注解) * 此时使用的是原生SQL,nativeQuery = true * (因为已在实体类OrderDO2中使用@Column注解进行了属性与字段的映射、故可以返回为实体类) * <p> * 但该方法必需要保证@Id标注的属性不会重复、否则会造成多条数据一样! * (@Id是标注主键的,按理说主键本来就不该重复的~) */ @Query(value = "SELECT x.order_id,\n" + " x.product_id,\n" + " y.product_name,\n" + " y.product_price,\n" + " y.product_factory\n" + " FROM t_order x\n" + " LEFT JOIN t_product y\n" + " ON x.product_id = y.product_id\n" + " WHERE x.order_id = ?1", nativeQuery = true) List<OrderDO2> getOrderInfo(Integer orderId); }
-
测试
package test; import cn.App; import cn.dao.OrderDao; import cn.dao.OrderDao2; import cn.entity.OrderEntity; import cn.pojo.OrderDO; import cn.pojo.OrderDO2; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import java.util.List; /** * 多表查询 */ @SpringBootTest(classes = App.class) public class TestFindTables { @Autowired private OrderDao orderDaoRepo; @Autowired private OrderDao2 orderDaoRepo2; /** * 环境测试 */ @Test public void test() { List<OrderEntity> list = orderDaoRepo.findAll(); list.forEach(order -> System.out.println(order)); /** * OrderEntity(id=1, orderId=1, productId=3) * OrderEntity(id=2, orderId=1, productId=4) * OrderEntity(id=3, orderId=2, productId=1) * OrderEntity(id=4, orderId=1, productId=1) */ } /** * 测试一:多表连接查询 * <p> * 法一:此时返回的结果是普通的java对象,而不是@Entity标注的实体 */ @Test public void test1() { List<OrderDO> list = orderDaoRepo.getOrderInfo(1); list.forEach(order -> System.out.println(order)); /** * OrderDO(orderId=1, productId=3, productName=电脑, productPrice=4500.0, productFactory=戴尔) * OrderDO(orderId=1, productId=4, productName=电脑, productPrice=4500.0, productFactory=联想) * OrderDO(orderId=1, productId=1, productName=冰箱, productPrice=4999.0, productFactory=西门子) * 由上面 test() 的结果可知,orderId=1 时确实对应三个商品,结果与预期一致! */ } /** * 测试二:多表连接查询 * <p> * 法二:此时返回的结果是@Entity标注的实体 */ @Test public void test2() { List<OrderDO2> list = orderDaoRepo2.getOrderInfo(1); list.forEach(order -> System.out.println(order)); /** * @Id 注解标注在 productId 属性上,结果正常(因为 productId 属性没有重复的) * OrderDO2(orderId=1, productId=3, productName=电脑, productPrice=4500.0, productFactory=戴尔) * OrderDO2(orderId=1, productId=4, productName=电脑, productPrice=4500.0, productFactory=联想) * OrderDO2(orderId=1, productId=1, productName=冰箱, productPrice=4999.0, productFactory=西门子) * * @Id 注解标注在 orderId 属性上,结果异常(因为三条数据的 orderId 相同,可能 JPA 就会默认主键想相同是同一条数据) * OrderDO2(orderId=1, productId=3, productName=电脑, productPrice=4500.0, productFactory=戴尔) * OrderDO2(orderId=1, productId=3, productName=电脑, productPrice=4500.0, productFactory=戴尔) * OrderDO2(orderId=1, productId=3, productName=电脑, productPrice=4500.0, productFactory=戴尔) */ } }
链接
- 源码链接:blogs-jpa
- 关联映射一对多链接:JPA三:关联映射(一)