• Spring Data JPA @Entity之间的关联关系注解如何正确使用?


    首先,实体与实体之间的关联关系一共分为四种,分别为OneToOne、OneToMany、ManyToOne和ManyToMany;而实体之间的关联关系又分为 双向和单向 。实体之间的关联关系是在JPA使用中最容易发生问题的地方。

    1、OneToOne关联关系

    @OneToOne一般表示对象之间一对一的关联关系,它可以放在field上面,也可以放在get/set方法上面。其中JPA协议有规定, 如果配置双向关联,维护关联关系的是拥有外键的一方,而另一方必须配置mappedBy ;如果是单项关联,直接配置在拥有外键的一方即可。

    举例说明:

    user表是用户的主信息,user_info是用户的拓展信息, 两者之间是一对一的关系 。user_info表里面有一个user_id作为关联关系的外键, 如果是单项关联 ,我们的写法如下:

    @Data
    @Entity
    @NoArgsConstructor
    @AllArgsConstructor
    @Builder
    public class User {
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        private Integer id;
        private String name;
        private String email;
        private String sex;
        private String address;
    }
    复制代码

    我们只需要在拥有外键的一方 配置@OneToOne注解 就可以了

    @Entity
    @Data
    @Builder
    @AllArgsConstructor
    @NoArgsConstructor
    @ToString(exclude = "user")
    public class UserInfo {
        @Id
        @GeneratedValue(strategy = GenerationType.AUTO)
        private Integer id;
        private Integer ages;
        private String telephone;
        @OneToOne
        private User user;
    }
    复制代码

    这就是 单向关联关系 ,那么如何设置 双向关联关系 呢? 我们保持UserInfo不变,在User实体对象里面添加一段代码即可

    @OneToOne(mappedBy = "user")
    private UserInfo userInfo;
    
    @Data
    @Entity
    @NoArgsConstructor
    @AllArgsConstructor
    @Builder
    public class User {
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        @Column(name = "id")
        private Integer id;
        private String name;
        private String email;
        private String sex;
        private String address;
        @OneToOne(mappedBy = "user")
        private UserInfo userInfo;
    }
    复制代码

    1.1 解读OneToOne源码

    public @interface OneToOne {
    
        Class targetEntity() default void.class;
    
        CascadeType[] cascade() default {};
    
        FetchType fetch() default EAGER;
    
        boolean optional() default true;
    
        String mappedBy() default "";
    
        boolean orphanRemoval() default false;
    }
    复制代码

    targetEntity:作为关联目标的实体类。

    cascade:级联操作策略,就是我们常说的级联操作。

    fetch:数据获取方式EAGER(立即加载)/LAZY(延迟加载) optional :表示关联的实体是否能够存在null值 mappedBy :关联关系被谁维护的一方对象里面的属性名字, 双向关联的时候必填 。

    1.2 mappedBy 注意事项

    • 只有 关联关系的维护方 才能操作两个实体之间外键的关系。 被维护方即使设置维护方属性进行存储也不会更新外键关联

    • mappedBy不能与@JoinColumn或者@JoinTable同时使用,因为没有任何意义,关联关系不在这里面维护。

    • mappedBy的值是指 另一方的实体里面属性的字段 ,而不是数据库字段,也不是实体的对象的名字。 也就是维护关联关系的一方属性字段名称 ,或者加了@JoinColumn 或 @JoinTable注解的属性字段名称。如上面的User例子user里面的mappedBy的值,就是userinfo里面的user字段的名字。

    1.3 CascadeType 用法

    在CascadeType的用法中,CascadeType的枚举值只有5个,分别如下:

    • CascadeType.PERSIST 级联新建
    • CascadeType.REMOVE 级联删除
    • CascadeType.PEFRESH 级联刷新
    • CascadeType.MERGE 级联更新
    • CascadeType.ALL 四项全选

    测试级联新建和级联删除 :

    第一步: 在@OneToOne上面添加 cascade = {CascadeType.PERSIST,CascadeType.REMOVE},代码如下所示:

    @Entity
    @Data
    @Builder
    @AllArgsConstructor
    @NoArgsConstructor
    @ToString(exclude = "user")
    public class UserInfo {
        @Id
        @GeneratedValue(strategy = GenerationType.AUTO)
        private Integer id;
        private Integer ages;
        private String telephone;
        @OneToOne(cascade = {CascadeType.PERSIST,CascadeType.REMOVE})
        private User user;
    }
    复制代码

    新增测试方法:

    @Test
    public void tesyPersistAndRemove(){
        User user = User.builder()
                .name("jackxx")
                .email("123456@126.com")
                .build();
        UserInfo userInfo = UserInfo.builder()
                .ages(12)
                .user(user)
                .telephone("12345678")
                .build();
        // 新建UserInfo,级联新建User
        userInfoRepo.save(userInfo);
        // 删除UserInfo,级联删除User
        userInfoRepo.delete(userInfo);
    }
    复制代码

    执行SQL如下所示:

    从上面运行结果中可以看到,执行insert的时候,会先插入user表,再插入user_info表。 执行delete的时候,先删除user_info表中数据,再删除user表中的数据。

    上面只是讲述级联删除的场景,下面我们再说一下 关联关系的删除场景 该怎么做?

    1.4 orphanRemoval属性用法

    orphanRemoval表示当关联关系被删除的时候,是否应用级联删除。

    首先我们,沿用上面的例子, 当我们删除userinfo的时候,把user置空

    userInfo.setUser(null);
    userInfoRepo.delete(userInfo);
    复制代码

    再看运行结果

    Hibernate: delete from user_info where id=?
    复制代码

    我们只删除了UserInfo的数据,没有删除user的数据, 说明没有进行级联删除 ,我们将orphanRemoval属性设置为true

    @OneToOne(cascade = {CascadeType.PERSIST},orphanRemoval = true)
    private User user;
    复制代码

    测试代码:

    @Test
    public void testRemove(){
        User user = User.builder()
                .name("jackxx")
                .email("123456@126.com")
                .build();
        UserInfo userInfo = UserInfo.builder()
                .ages(12)
                .user(user)
                .telephone("12345678")
                .build();
        // 新建UserInfo,级联新建User
        userInfoRepo.save(userInfo);
        userInfo.setUser(null);
        // 删除UserInfo,级联删除User
        userInfoRepo.delete(userInfo);
    }
    复制代码

    执行结果如下所示:

    在执行结果中多了一条update语句,是因为去掉了CascadeType.REMOVE,这个时候不会进行级联删除了。当我们把user对象更新为null的时候,就会执行一个update语句把关联关系去掉。

    1.5 orphanRemoval 和 CascadeType.REMOVE的区别

    • CascadeType.REMOVE 级联删除,先删除user表的数据,再删除user_info表的数据。 (因为存在外键关联,无法先删除user_info表的数据)
    • orphanRemoval = true 先将user_info表中的数据外键user_id 更新为 null,然后删除user_info表的数据,再删除user表的数据。

    2、@JoinColumns & @JoinColumn

    这两个注解是集合关系,他们可以同时使用,@JoinColumn表示单字段,@JoinColumns表示多个@JoinColumn

    @JoinColumn源码

    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);
    }
    复制代码
    • name :代表外键的字段名。
    • referencedColumnName : 关联表对应的字段,如果不注明,默认就是关联表的主键
    • unique :外键字段是否唯一
    • nullable :外键字段是否允许为空
    • insertable :是否跟随一起新增
    • updateable :是否跟随一起更新
    • columnDefinition :为列生成DDL时使用的SQL片段
    • foreignKey :外键策略
    // 外键策略
    public enum ConstraintMode {
    
      // 创建外键约束
      CONSTRAINT,
      // 不创建外键约束
      NO_CONSTRAINT,
      // 采用默认行为
      PROVIDER_DEFAULT
    }
    复制代码

    foreignKey的用法:

    @OneToOne(cascade = {CascadeType.PERSIST},orphanRemoval = true)
    @JoinColumn(foreignKey = @ForeignKey(ConstraintMode.NO_CONSTRAINT), name = "user_id")
    private User user;
    
    复制代码

    JoinColumns的用法:

    @OneToOne(cascade = {CascadeType.PERSIST},orphanRemoval = true)
    @JoinColumns({
           @JoinColumn(name = "user_id",referencedColumnName = "ID"),
           @JoinColumn(name = "user_ZIP",referencedColumnName = "ZIP")
    })
    private User user;
    复制代码

    3、@ManyToOne & @OneToMany

    @ManyToOne代表多对一的关联关系,而@OneToMany代表一对多,一般两个成对使用表示双向关联关系。在JPA协议中也是明确规定: 维护关联关系的是拥有外键的一方,而另一方必须配置mappedBy

    public @interface OneToMany {
    
        Class targetEntity() default void.class;
    
        CascadeType[] cascade() default {};
    
        FetchType fetch() default LAZY;
    
        String mappedBy() default "";
    
        boolean orphanRemoval() default false;
    }
    
    
    public @interface ManyToOne {
    
        Class targetEntity() default void.class;
    
        CascadeType[] cascade() default {};
    
        FetchType fetch() default EAGER;
    
        boolean optional() default true;
    }
    复制代码

    使用这两个字段, 需要注意以下几点 :

    一定不能把One的一方删除了
    

    3.1 Lazy机制

    举例说明 : 假设User有多个地址Address

    @Data
    @Entity
    @NoArgsConstructor
    @AllArgsConstructor
    @Builder
    public class User {
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        @Column(name = "id")
        private Integer id;
        private String name;
        private String email;
        private String sex;
        @OneToMany(mappedBy = "user",fetch = FetchType.LAZY)
        private List address;
    }
    复制代码

    @OneToMany 双向关联并且采用 LAZY的机制

    @Entity
    @Data
    @Builder
    @AllArgsConstructor
    @NoArgsConstructor
    public class UserAddress {
        @Id
        @GeneratedValue(strategy = GenerationType.AUTO)
        private Long id;
        private String address;
    
        @ManyToOne(cascade = CascadeType.ALL)
        private User user;
    }
    复制代码

    测试代码 :

    @Test
    @Transactional
    public void testUserAddress(){
        User user = User.builder()
                .name("jackxx")
                .email("123456@126.com")
                .build();
        UserAddress userAddress = UserAddress.builder()
                .address("shanghai1")
                .user(user)
                .build();
        UserAddress userAddress1 = UserAddress.builder()
                .address("shanghai2")
                .user(user)
                .build();
        addressRepo.saveAll(Lists.newArrayList(userAddress,userAddress1));
    
        User u = userRepo.findById(1).get();
        System.out.println(u.getName());
        System.out.println(u.getAddress());
    }
    复制代码

    运行结果如下所示:

    可以看到当我们 想要输出Address信息的时候,才会加载Addres的信息

    4、ManyToMany

    @ManyToMany代表多对多的关联关系、这种关联关系任何一方都可以维护关联关系。

    我们假设user表和room表是多对多的关系, 如下所示 :

    @Data
    @Entity
    @NoArgsConstructor
    @AllArgsConstructor
    @Builder
    public class User {
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        @Column(name = "id")
        private Integer id;
        private String name;
        @ManyToMany(mappedBy = "users")
        private List rooms;
    }
    复制代码
    @Entity
    @Data
    @Builder
    @AllArgsConstructor
    @NoArgsConstructor
    @ToString
    public class Room {
        @Id
        @GeneratedValue(strategy = GenerationType.AUTO)
        private Long id;
    
        private String title;
    
        @ManyToMany
        private List users;
    }
    复制代码

    这种方法实不可取, 当用到@ManyToMany的时候一定是三张表,不要想着建两张表,两张表肯定是违背表的原则

    改进方法:创建中间表 修改Romm里面的内容

    @Entity
    @Data
    @Builder
    @AllArgsConstructor
    @NoArgsConstructor
    @ToString
    public class Room {
        @Id
        @GeneratedValue(strategy = GenerationType.AUTO)
        private Long id;
    
        private String title;
    
        @ManyToMany
        @JoinTable(name = "user_room",
        joinColumns = @JoinColumn(name = "room_id"),
        inverseJoinColumns = @JoinColumn(name = "user_id"))
        private List users;
    }
    复制代码

    可以看到我们通过 @JoinTable注解创建一张中间表 ,并且添加了两个设定的外键,我们来看看@JoinTable的源码:

    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 {};
    }
    复制代码
    • name :中间表名称
    • joinColumns :维护关联关系一方的外键字段的名字
    • inverseJoinColumns :另一方表的外键字段的名字

    在现实开发中,@ManyToMany注解用的比较少,一般都会使用成对的@ManyToOne 和 @OneToMany代替, 因为我们的中间表可能还有一些约定的公共字段,如ID,update_time,create_time等其他字段

    4.1 利用@ManyToOne 和 @OneToMany表达多对多的关联关系

    在上面的Demo中,我们稍作修改,新建一张user_room 中间表来存储双方的关联关系和额外字段

    如下所示 : user_room中间表

    @Entity
    @Data
    @Builder
    @AllArgsConstructor
    @NoArgsConstructor
    public class user_room {
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        private Long id;
        private Date createTime;
        private Date updateTime;
        @ManyToOne
        private User user;
        @ManyToOne
        private Room room;
    }
    复制代码

    user表

    @Data
    @Entity
    @NoArgsConstructor
    @AllArgsConstructor
    @Builder
    public class User {
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        @Column(name = "id")
        private Integer id;
        @OneToMany(mappedBy = "user")
        private List userRoomList;
    }
    复制代码

    room表

    @Entity
    @Data
    @Builder
    @AllArgsConstructor
    @NoArgsConstructor
    @ToString
    public class Room {
        @Id
        @GeneratedValue(strategy = GenerationType.AUTO)
        private Long id;
        @OneToMany(mappedBy = "room")
        private List roomList;
    }    
    复制代码

    好了本次就介绍到这!!!

  • 相关阅读:
    Jetson Orin平台多路 FPDlink Ⅲ相机采集套装推荐
    如何查找遥感卫星相关参数
    基于Hi3861的听话的狗子
    windows cmd 常用操作命令
    LLM-TAP随笔——语言模型训练数据【深度学习】【PyTorch】【LLM】
    SpringBoot2.0数据访问之整合数据源(Druid)
    输入框内禁止输入特殊字符
    Linux--VMware的安装和Centos
    【Spring容器的启动过程】
    String和stringbuffer和stringbuilder
  • 原文地址:https://blog.csdn.net/m0_73311735/article/details/126869552