Spring Data JPA 之关系映射

1,839 阅读7分钟

这是我参与8月更文挑战的第16天,活动详情查看:8月更文挑战


众所周知,在设计数据库表的时候,表与表之间存在者一对一一对多多对一多读多的关联关系,而Spring Data JPA是一种全自动ORM框架。那么在JPA中也存在者管理实体间关联关系的方法。

JPA使用@OneToOne@OneToMany@ManyToOne@ManyToMany注解描述和管理实体之间的关联关联。

@OneToOne 一对一映射

@OneToOne注解用来描述管理实体一对一的映射关系,例如用户和账号、班级和班主任等。

image.png

  • cascade:级联操作 cascade定义了操作实体时,对其关联对象的操作。

image.png PERSIST:对应新增操作,即新增源实体对象的时候同时新增其关联的实体对象 MERGE:对应更新操作,即更新源实体对象的时候同时更新其关联的实体对象 REMOVE:对应删除操作,即删除源实体对象的时候同时删除其关联的实体对象 REFRESH:更新实体时,会先获取最新的实体信息再更新 DETACH: 分离实体,实体更新时智慧更新其对应表中的数据,引用该实体对应的表不会更新

  • fetch:拉取数据 fetch定义在查询时查询关联数据的时机。

image.png LAZY:只有在用到关联实体时才会查询关联的实体, EAGER:在查询源实体的时候同时查询关联的实体

@JoinColumn:关联列

通常关系映射注解需要@JoinColumn注解配合使用,@JoinColumn注解定义了实体间通过那个字段进行关联,相当于SQL语句中关联查询的on。如果没有@JoinColumn注解的话默认通过两个实体的主键进行关联

image.png name:源实体对应表关联的字段,默认源实体对应表的主键 referencedColumnName:引用实体对应表关联的字段,默认是引用实体对应表对应的主键 unique:约束关联字段是唯一的 nullable:关联字段是否可以为空 insertabl:在新增时,生成的insert语句是否包好这个字段。和源实体中的相应字段冲突 updatable:在更新时,生成的uptate语句是否包好这个字段。和源实体中的相应字段冲突 columnDefinition:为列生成DDL语句时使用的SQL片段 table:关联字段所在的的表名。默认为源实体对应的表明 foreignKey:生成DDL语句时是否将关联列加上外键约束。默认加上外键约束

示例

  1. 建表
CREATE TABLE `user` (
  `id` int(64) NOT NULL AUTO_INCREMENT COMMENT '用户id',
  `name` varchar(255) DEFAULT NULL COMMENT '用户名',
  `sex` varchar(2) DEFAULT NULL COMMENT '性别',
  `birth_day` datetime DEFAULT NULL COMMENT '出生日期',
  `account_id` int(64) DEFAULT NULL COMMENT '账号id',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COMMENT='用户';

CREATE TABLE `account` (
  `id` int(64) NOT NULL AUTO_INCREMENT COMMENT '账号id',
  `username` varchar(255) DEFAULT NULL COMMENT '用户名',
  `password` varchar(255) DEFAULT NULL COMMENT '密码',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COMMENT='账号';

  1. 建实体
@Data
@Entity
@Table(name = "user")
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;

    private String name;

    private String sex;

    private Date birthDay;

//    private Integer accountId;

    /**
     * 使用@JoinColumn时, 字段accountId不能存在,除非将级联新增和更新关闭,即 @JoinColumn(name = "accountId", insertable = false, updatable = false)
     */
    @JoinColumn(name = "accountId")
    @OneToOne(cascade = CascadeType.ALL)
    private Account account;
    
}


@Data
@Entity
@Table(name = "account")
public class Account {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;

    private String username;

    private String password;
}

  1. 编写Repository
public interface UserRepo extends JpaRepository<User, Integer> {
}

测试

  • 新增 image.png

这里User实体中关联的accountId字段被注释掉了,这是因为@JoinColumninsertable属性默认为true,即生成insert语句时生成accountId字段的插入语句 如果不想注释accountId字段,可以按照下图的写法 image.png

  • 查询

image.png

  • 延迟查询关联对象 在@OneToOne注解注解中,设置fetch = FetchType.LAZY,延迟查询关联实体的数据。如下图:

image.png

测试 这里编写两个两条打印语句,可以看到当没有获取Account时,只会查询User实体。当获取Account时,才去查询Account实体对应的数据

image.png

注意:要在方法上加上@Transactional(readOnly = true),否则会抛出异常

  • 更新

image.png

注意:这里的User实体中关联的accountId字段被注释掉了,所有没有设置accountId字段值。如果accountId字段没有被注释,并且没有设值的话,acoountId将会编程null

  • 删除

image.png

@OneToMany@ManyToOne 一对多多对一关联

一对多多对一关系最常见的例子就是订单和订单项,一个订单可以有多个订单明细、多个订单项明细对应一个订单JPA中使用@OneToMany@ManyToOne管理一对多多对一的关联关系

image.png

@OneToMany@ManyToOne属性同@OneToOne

示例

  1. 建表 新建itemitem_detail表,其中item_detail中持有item的id
CREATE TABLE `item` (
  `id` int(64) NOT NULL AUTO_INCREMENT COMMENT '订单id',
  `item_name` varchar(255) DEFAULT NULL COMMENT '订单名',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单';


CREATE TABLE `item_detail` (
  `id` int(64) NOT NULL AUTO_INCREMENT COMMENT '订单明细id',
  `item_detail_name` varchar(255) DEFAULT NULL COMMENT '订单明细名称',
  `item_id` int(64) DEFAULT NULL COMMENT '订单id',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单项id';

注意:这里item_detail表中item_id字段没有设置非空约束。这是因为如果设置了非空约束,级联新增时会出现错误

  1. 建实体 新建itemitem_detail表对应的实体。其中item实体中使用List结构持有多个ItemDetail实体。

可以使用ListSet结构持有多个引用对象

@Data
@Entity
@Table(name = "item")
public class Item {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;

    private String itemName;

    @JoinColumn(name = "itemId")
    @OneToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER)
    private List<ItemDetail> detailList;

}


@Data
@Entity
@Table(name = "item_detail")
public class ItemDetail {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;

    private String itemDetailName;

    private Integer itemId;
}

  1. 编写Repository
public interface ItemRepo extends JpaRepository<Item, Integer> {
}

测试

  • 新增

image.png

如果想要获取最后的实体,可以根据返回的id重新查一次

  • 查询

image.png

@ManyToMany 多对多关系

多对多关系在建表的时候,通常会使用一个第三方的表表示关联关系。多对多关系的一个常见的例子是用户角色,一个用户有多个角色,一个角色可以被多个用户拥有。

image.png

@ManyToMany属性同@OneToOne

@JoinTable 关联的中间表

多对多关系在建表的时候需要一个中间表来描述关联关系,同样的在实体间需要@JoinTable关联中间实体来描述实体间的对应关系

image.png

  • name: 中间表的表名
  • catalog:中间表的catalog
  • schema: 中间表的schema
  • joinColumns:主表在中间表中的关联字段名
  • inverseJoinColumns:关联表在中间表中的关联字段名
  • foreignKey:主表在中间表中的外键名
  • inverseForeignKey:关联表在中间表中的关联外键名
  • uniqueConstraints:要放置在表上的唯一约束。 这些仅在表生成有效时使用
  • indexes:表的索引。 这些仅在表生成有效时使用

示例

这里以用户角色为例,用户表使用之前建的

  1. 建表
CREATE TABLE `role` (
  `id` int(64) NOT NULL AUTO_INCREMENT COMMENT '角色id',
  `role_name` varchar(255) DEFAULT NULL COMMENT '角色名',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='角色';

CREATE TABLE `user_role` (
  `id` int(64) NOT NULL AUTO_INCREMENT COMMENT '用户角色关联id',
  `user_id` int(64) DEFAULT NULL COMMENT '用户id',
  `role_id` int(64) DEFAULT NULL COMMENT '角色id',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户角色关联表';
  1. 建实体 在User实体加上List<Role>字段,并在该字段上加上@ManyToMany@JoinTable注解描述多对多关系
@Data
@Entity
@Table(name = "user")
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;

    private String name;

    private String sex;

    private Date birthDay;

//    private Integer accountId;

    /**
     * 使用@JoinColumn时, 字段accountId不能存在,除非将级联新增和更新关闭,如 @JoinColumn(name = "accountId", insertable = false, updatable = false)
     */
    @JoinColumn(name = "accountId")
    @OneToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
    private Account account;

    @JoinTable(name ="user_role",
               joinColumns = @JoinColumn(name="user_id"),
               inverseJoinColumns = @JoinColumn(name="role_id")
    )
    @ManyToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER)
    private List<Role> roleList;

}


@Data
@Entity
@Table(name = "role")
public class Role {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;

    private String roleName;
    
}
  1. 编写Repository Repository还使用之前建的UserRepo

测试

  • 新增

image.png

  • 查询

image.png