JPA四:关联映射(二)

3 多对多

概述

  1. 中间表:中间表中最少应该由两个字段组成,这两个字段做为外键指向两张表的主键,又组成了联合主键【多对多需要用到中间表】

步骤分析

  • 案例:用户和角色
  1. 明确表关系
    • 多对多关系
  2. 确定表关系(描述 外键或中间表,一对多用外键、多对多用中间表)
    • 中间表
  3. 编写实体类,在实体类中描述表关系(即包含关系)
    1. 用户:包含角色的集合
    2. 角色:包含用户的集合
  4. 配置映射关系
    • 使用 JPA 注解配置多对多映射关系

3.1 添加

  • 参考文章开头的博客 JPA三:关联映射(一) ,基于该项目继续编码。

  • 配置多对多时实体类切莫使用@Data注解,因为@Data注解包含toString方法,双方互相调用会产生StackOverflowError错误,这是个巨坑!!!

  1. 实体类

    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>();
    }
    
  2. 持久层

    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> {
    }
    
  3. 测试

    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 级联

  1. 修改实体类

    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 不变,代码同上,略
    
  2. 测试

    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 级联查询

对象导航查询(即级联查询)

  1. 对象导航查询:查询一个对象的同时,通过此对象查询他的关联对象。

  2. 案例:即一对多的客户与联系人(因为一对多既能包含Set集合,又能包含对象属性)

  3. 注:为了防止 StackOverflowError 错误,在 LinkManEntity 类上不要用 @Data 注解,toString 方法要手动生成、且不能含输出 CustomerEntityCustomerEntity 类上直接使用 @ToString 即可,即双方的 toString 方法中中不能相互包含彼此。

  4. fetch 用于配置关联映射的加载方式:EAGER (立即加载),LAZY (延迟加载)。虽然可以配置立即加载,但是不推荐使用,因为它的关联对象无论是否用到都进行查询,增加了数据库的压力。

编码

  1. 修改实体类

    • 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 + '\'' +
                    '}';
        }
    }
    
  2. 测试

    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'}
             * 对象导航查询成功
             */
        }
    }
    

补充

  1. "一"查"多"一定要配置延迟加载,因为若是只想查客户本身、用不到联系人的情况下,假如联系人很多会造成数据库压力且查出来毫无意义(因为用不到啊)
  2. "多"查"一"可以使用立即加载也可以使用延迟加载,因为仅多一条数据对数据库来说基本上没压力,而 Data JPA 在"多"查"一"时默认使用立即加载。

5 @Query 连接查询

  • 因为连接查询与上面级联查询功能类似,故将该块知识总结在本文中。
  1. 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);
    

编码

  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; // 厂家
    }
    
  2. 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;
    }
    
  3. 持久层

    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);
    }
    
  4. 测试

    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=戴尔)
             */
        }
    }
    

链接

  1. 源码链接:blogs-jpa
  2. 关联映射一对多链接:JPA三:关联映射(一)
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值