Spring Data JPA:關聯關係(外鍵)

JAVA 8

Spring Boot 2.5.3

MySQL 5.7.21

---

 

目錄

0、概述

1、一對一

2、一對多(多對一)

3、多對多

參考文檔

 

0、概述

關聯關係,即外鍵.

包括:一對一、一對多(多對一)、多對多。

難點:級聯更新、級聯刪除

在MySQL中,僅InnoDB支持 外鍵。

本文分別介紹各種關聯關係的使用。

 

外鍵:被引用的列,要麼是 主鍵 ,要麼 具有 唯一性約束(UNIQUE)

 

MySQL的外鍵:

    創建語法:1)CREATE TABLE、2)ALTER TABLE

    查看:1)SHOW CREATE TABLE tbl;、2)SHOW INDEX FROM tbl;、3)DESC tbl;

    刪除:1)ALTER TABLE tbl DROP FOREIGN KEY fk_id;

使用 mysql> help create table; 可以查看創建外鍵的語法:

CREATE [TEMPORARY] TABLE [IF NOT EXISTS] tbl_name
    (create_definition,...)
    [table_options]
    [partition_options]
...
create_definition:
    col_name column_definition
    ...
  | [CONSTRAINT [symbol]] FOREIGN KEY
      [index_name] (index_col_name,...) reference_definition
...
reference_definition:
    REFERENCES tbl_name (index_col_name,...)
      [MATCH FULL | MATCH PARTIAL | MATCH SIMPLE]
      [ON DELETE reference_option]
      [ON UPDATE reference_option]
...
reference_option:
    RESTRICT | CASCADE | SET NULL | NO ACTION | SET DEFAULT

 

外鍵部分簡版如下:

[CONSTRAINT [symbol]] FOREIGN KEY [index_name] (index_col_name,...) 
REFERENCES tbl_name (index_col_name,...)
      [MATCH FULL | MATCH PARTIAL | MATCH SIMPLE]
      [ON DELETE [RESTRICT | CASCADE | SET NULL | NO ACTION | SET DEFAULT]]
      [ON UPDATE [RESTRICT | CASCADE | SET NULL | NO ACTION | SET DEFAULT]]

 

ON DELETE 級聯刪除配置,ON UPDATE 級聯更新配置。相關參數說明:

RESTRICT

默認值

拒絕主表刪除或修改

CASCADE 級聯刪除 或 級聯更新
SET NULL

主表刪除或更新時,使用NULL值替代從表中對應的記錄

注意,從表字段設置爲 NOT NULL時無效。

NO ACTION 同 默認值 RESTRICT
SET DEFAULT

設置默認值

參考文檔1 說 InnoDB不支持(待確定

 

spring boot工程:jpa-mysql

MySQL配置:

# MySQL on Ubuntu
spring.datasource.url=jdbc:mysql://mylinux:3306/jpa?serverTimezone=Asia/Shanghai
spring.datasource.username=springuser
spring.datasource.password=ThePassword
#spring.datasource.driver-class-name =com.mysql.jdbc.Driver # This is deprecated
spring.datasource.driver-class-name =com.mysql.cj.jdbc.Driver
spring.jpa.hibernate.ddl-auto=update
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL5InnoDBDialect
# 打開使用過程中執行的SQL語句
spring.jpa.show-sql: true

 

建立數據庫(jpa)語句:

CREATE DATABASE IF NOT EXISTS jpa DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_general_ci;

 

1、一對一

三個實體類:OtoBasic、OtoExt、OtoName

其中,OtoBasic是主表(被關聯表),OtoExt、OtoName是從表;OtoExt和OtoBasic的id字段建立 一對一關聯,OtoName和OtoBasic的name字段建立 一對一關聯。

建立關聯使用的註解:

@OneToOne、@JoinColumn、@ForeignKey

@Entity
@Data
public class OtoBasic {

	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	private Long id;
	
	/**
	 * 名稱:唯一、非空
	 */
	@Column(columnDefinition = "VARCHAR(100) UNIQUE NOT NULL")
	private String name;
	
	@OneToOne(mappedBy = "basic", cascade = {CascadeType.REMOVE})
	private OtoExt ext;
	
	@OneToOne(mappedBy = "basic", cascade = {CascadeType.ALL})
	private OtoName otoName;
	
}

@Entity
@Data
public class OtoExt {

	/**
	 * 自增
	 */
	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	private Long id;
	
	/**
	 * 詳情
	 */
	@Column(columnDefinition = "VARCHAR(100) NOT NULL")
	private String detail;
	
	// 不能有!不需要!
//	private Long basicId;

	// 1、無法刪除
//	@OneToOne(cascade = {CascadeType.PERSIST, CascadeType.REFRESH, CascadeType.REMOVE})
	// 2、無法刪除 發生異常
//	@OneToOne(cascade = {CascadeType.REFRESH, CascadeType.REMOVE})
	// 3、無法刪除 發生異常
//	@OneToOne(cascade = {CascadeType.REMOVE})
	// 在主表使用 CascadeType.REMOVE 纔有效:刪除主表記錄,級聯刪除從表,,而不是在從表設置
	@OneToOne
	@JoinColumn(name = "basic_id", referencedColumnName = "id", foreignKey = @ForeignKey(name="fk_basic_id", 
        value=ConstraintMode.CONSTRAINT))
	private OtoBasic basic;
	
}

@Entity
@Data
public class OtoName {

	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	private Long id;
	
	// 注意,需要手動設置外鍵,添加選項:ON DELETE CASCADE ON UPDATE CASCADE
	// 纔可以級聯更新
	@OneToOne
	@JoinColumn(name = "basic_name", referencedColumnName = "name", 
		foreignKey = @ForeignKey(name="fk_basic_name", value=ConstraintMode.CONSTRAINT))
	private OtoBasic basic;
	
}

 

上面的 關聯關係 是雙向的:主表用到了 @OneToOne 的 mappedBy 屬性,其值爲 從表的屬性名(對象名)。

除了上面這種 雙向 使用,也可以和從表一樣 使用 @JoinColumn 的name、referencedColumnName 實現,但取值和從表相反(未試驗)。

 

建立實體類的Repository:新建接口,繼承JpaRepository接口即可。

public interface OtoBasicDAO extends JpaRepository<OtoBasic, Long> {

}

public interface OtoExtDAO extends JpaRepository<OtoExt, Long> {

}

public interface OtoNameDAO extends JpaRepository<OtoName, Long> {

}

 

啓動項目,即可在工程中建立三個實體類對應的表&外鍵:

啓動日誌的SQL語句:建表、建約束等
Hibernate: create table oto_basic (id bigint not null auto_increment, name VARCHAR(100) UNIQUE NOT NULL, primary key (id)) engine=InnoDB
Hibernate: create table oto_ext (id bigint not null auto_increment, detail VARCHAR(100) NOT NULL, basic_id bigint, primary key (id)) engine=InnoDB
Hibernate: create table oto_name (id bigint not null auto_increment, basic_name VARCHAR(100) UNIQUE NOT NULL, primary key (id)) engine=InnoDB
Hibernate: alter table oto_basic drop index UK_qamvvb8udcjeoj4q2mv7r63tk
Hibernate: alter table oto_basic add constraint UK_qamvvb8udcjeoj4q2mv7r63tk unique (name)
Hibernate: alter table oto_ext add constraint fk_basic_id foreign key (basic_id) references oto_basic (id)
Hibernate: alter table oto_name add constraint fk_basic_name foreign key (basic_name) references oto_basic (name)

數據表結構:show create table XXX
mysql> show create table oto_basic \G
*************************** 1. row ***************************
       Table: oto_basic
Create Table: CREATE TABLE `oto_basic` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `name` varchar(100) NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `name` (`name`),
  UNIQUE KEY `UK_qamvvb8udcjeoj4q2mv7r63tk` (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
1 row in set (0.00 sec)

mysql>
mysql> show create table oto_ext \G
*************************** 1. row ***************************
       Table: oto_ext
Create Table: CREATE TABLE `oto_ext` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `detail` varchar(100) NOT NULL,
  `basic_id` bigint(20) DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `fk_basic_id` (`basic_id`),
  CONSTRAINT `fk_basic_id` FOREIGN KEY (`basic_id`) REFERENCES `oto_basic` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
1 row in set (0.00 sec)

mysql>
mysql> show create table oto_name \G
*************************** 1. row ***************************
       Table: oto_name
Create Table: CREATE TABLE `oto_name` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `basic_name` varchar(100) NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `basic_name` (`basic_name`),
  CONSTRAINT `fk_basic_name` FOREIGN KEY (`basic_name`) REFERENCES `oto_basic` (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
1 row in set (0.00 sec)

mysql>

 

說明:來自博客園

1、自定義外鍵名稱

外鍵體現在 從表 oto_ext、oto_name 中:fk_basic_id、fk_basic_name (名稱 由 註解 @JoinColumn 的屬性 @ForeignKey 確定,否則是一串無意義字符串,如oto_baisc的 UK_qamvvb8udcjeoj4q2mv7r63tk 鍵)。

2、從表的新字段

建立外鍵後,從表 多了響應的字段:oto_ext 中的 basic_id,oto_name 中的 basic_name,此時不能在 從表實體類中 添加對應的 屬性。

3、級聯刪除

刪除主表oto_baisc中的記錄,此時,級聯刪除 從表oto_ext中的內容。

需要使用 @OneToOne註解 的 cascade屬性,其值爲 CascadeType.ALL 或 包括 CascadeType.REMOVE 都可以。

另外,還有一個 orphanRemoval 屬性,默認爲false——不級聯刪除,設置爲true後,即可 級聯刪除。

cascade 或 orphanRemoval 只要有一個 允許級聯刪除,即可執行 級聯刪除。來自博客園

4、級聯更新

更新主表oto_baisc 的 name字段時,希望從表 oto_name 對應的 basic_name 也更新。

但是,默認建立的外鍵 沒有配置 ON UPDATE CASCADE,導致無法級聯更新,也會阻止 主表更新。

嘗試多種 JPA方式 添加 ON UPDATE CASCADE 選項都失敗了,最終只能手動操作

先刪除oto_name的外鍵 fk_basic_name,再新建一個 同名的外鍵。

ALTER TABLE oto_name DROP FOREIGN KEY fk_basic_name;
ALTER TABLE oto_name ADD CONSTRAINT fk_basic_name FOREIGN KEY (basic_name) REFERENCES oto_basic(name) 
ON DELETE CASCADE ON UPDATE CASCADE;

 這樣就可以級聯刪除了。

 

一對一案例:

1)一夫一妻

2) 

 

2、一對多(多對一)

兩個實體類:OtmBasic、OtmMany,其中,一個OtmBasic可以對應多個OtmMany。來自博客園

建立關聯使用的註解:

@ManyToOne、@OneToMany、@JoinColumn、@ForeignKey、@OrderBy(結合@OneToMany使用)

@Entity
@Data
public class OtmBasic {

	/**
	 * 自增
	 */
	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	private Long id;
	
	@Column(columnDefinition = "VARCHAR(100) UNIQUE NOT NULL")
	private String name;

	@Column(insertable = false, columnDefinition = "DATETIME DEFAULT NOW()")
	private Date createTime;
	
	// 1、不能刪除
//	@OneToMany(mappedBy = "basic", cascade = {})
	// 2、可以刪除
	@OneToMany(mappedBy = "basic", cascade = {CascadeType.REMOVE})
	// 按 創建時間倒序 排列
	@OrderBy(value = "create_time DESC")
	private List<OtmMany> manyList;
	
	// 構造函數
	
	public OtmBasic() {}
	
	public OtmBasic(String name) {
		this.name = name;
	}
	
}

@Entity
@Data
public class OtmMany {

	/**
	 * 自增
	 */
	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	private Long id;

	/**
	 * house:房子
	 */
	@Column(columnDefinition = "VARCHAR(100) UNIQUE NOT NULL")
	private String house;

	@Column(insertable = false, columnDefinition = "DATETIME DEFAULT NOW()")
	private Date createTime;
	
	@ManyToOne
	@JoinColumn(name="basic_id", foreignKey = @ForeignKey(name="fk_basic_id_otm"))
	private OtmBasic basic;
	
}

 

建立實體類的Repository:新建接口,繼承JpaRepository接口即可。來自博客園

public interface OtmBasicDAO extends JpaRepository<OtmBasic, Long> {

}

public interface OtmManyDAO extends JpaRepository<OtmMany, Long> {

}

 

啓動項目,數據庫中建立了實體類相關的表和外鍵:

項目日誌:
Hibernate: create table otm_basic (id bigint not null auto_increment, create_time DATETIME DEFAULT NOW(), 
	name VARCHAR(100) UNIQUE NOT NULL, primary key (id)) engine=InnoDB
Hibernate: create table otm_many (id bigint not null auto_increment, create_time DATETIME DEFAULT NOW(), 
	house VARCHAR(100) UNIQUE NOT NULL, basic_id bigint, primary key (id)) engine=InnoDB
Hibernate: alter table otm_many add constraint fk_basic_id_otm foreign key (basic_id) references otm_basic (id)

數據表:
mysql> show create table otm_basic \G
*************************** 1. row ***************************
       Table: otm_basic
Create Table: CREATE TABLE `otm_basic` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `create_time` datetime DEFAULT CURRENT_TIMESTAMP,
  `name` varchar(100) NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `name` (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
1 row in set (0.00 sec)

mysql> show create table otm_many \G
*************************** 1. row ***************************
       Table: otm_many
Create Table: CREATE TABLE `otm_many` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `create_time` datetime DEFAULT CURRENT_TIMESTAMP,
  `house` varchar(100) NOT NULL,
  `basic_id` bigint(20) DEFAULT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `house` (`house`),
  KEY `fk_basic_id_otm` (`basic_id`),
  CONSTRAINT `fk_basic_id_otm` FOREIGN KEY (`basic_id`) REFERENCES `otm_basic` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
1 row in set (0.00 sec)

mysql>

 

說明:

1、外鍵名稱衝突

在前面試驗 一對一關聯 時,建立了外鍵 fk_basic_id。來自博客園

在本試驗中,最開始也想建立一個 同名的外鍵的,但是,發生了衝突:

Hibernate: alter table otm_many add constraint fk_basic_id foreign key (basic_id) references otm_basic (id)
2021-10-18 20:46:44.332  WARN 8768 --- [           main] o.h.t.s.i.ExceptionHandlerLoggedImpl     : 
GenerationTarget encountered exception accepting command : Error executing DDL "alter table otm_many 
add constraint fk_basic_id foreign key (basic_id) references otm_basic (id)" via JDBC Statement

org.hibernate.tool.schema.spi.CommandAcceptanceException: Error executing DDL "alter table otm_many 
add constraint fk_basic_id foreign key (basic_id) references otm_basic (id)" via JDBC Statement

將其更改爲 fk_basic_id_otm 後,衝突消失:

// OtmMany.java
@ManyToOne
@JoinColumn(name="basic_id", foreignKey = @ForeignKey(name="fk_basic_id_otm"))
private OtmBasic basic;

 2、級聯刪除

使用 主表的@OneToMany 的 cascade屬性,或 orphanRemoval屬性,同 @OneToOne。

3、數據排序

一對多中,“一”的一方可以使用@OrderBy 對結果排序。來自博客園

// OtmBasic.java
@OneToMany(mappedBy = "basic", cascade = {CascadeType.REMOVE})
@OrderBy(value = "create_time DESC")
private List<OtmMany> manyList;

 

一對多案例:來自博客園

1)一個家庭 多套房產

2) 

 

3、多對多

兩個實體類:MtmBlog、MtmBlogTag,其中,博客 和 標籤 是 多對多的關係。

建立關聯使用的註解:來自博客園

@ManyToMany、@JoinTable、@JoinColumn、@ForeignKey、@OrderBy

@Entity
@Data
public class MtmBlog {

	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	private Long id;
	
	@Column(columnDefinition = "VARCHAR(100) NOT NULL")
	private String title;
	
	@Column(columnDefinition = "VARCHAR(1000) NOT NULL")
	private String content;
	
	@ManyToMany(cascade = {CascadeType.ALL})
	@JoinTable(
			name = "mtm_blog_tag_rel",
			joinColumns = @JoinColumn(name = "blog_id", referencedColumnName = "id",
					foreignKey = @ForeignKey(name="fk_blog_tag_rel_blog")),
			inverseJoinColumns = @JoinColumn(name = "tag_id", referencedColumnName = "id",
					foreignKey = @ForeignKey(name="fk_blog_tag_rel_tag"))
			)
	// 排序
	@OrderBy(value = "tag DESC")
	private List<MtmBlogTag> tags;
	
}

@Entity
@Data
public class MtmBlogTag {

	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	private Long id;
	
    // 唯一性約束
	@Column(columnDefinition = "VARCHAR(100) UNIQUE NOT NULL")
	private String tag;

	// 雙向多對多
	@ManyToMany(mappedBy = "tags")
	// 排序
	@OrderBy(value = "title DESC")
	private List<MtmBlog> blogs;
	
}

 

建立實體類的Repository:新建接口,繼承JpaRepository接口即可。

public interface MtmBlogDAO extends JpaRepository<MtmBlog, Long> {

}

public interface MtmBlogTagDAO extends JpaRepository<MtmBlogTag, Long> {

	MtmBlogTag findByTag(String tag);
	
}

 

啓動項目,自動建立數據表:來自博客園

2個實體類,但是建立了3個數據表,其中一個爲 數據關聯表,多對多時需要。

# 項目日誌
Hibernate: create table mtm_blog_tag_rel (blog_id bigint not null, tag_id bigint not null) engine=InnoDB
Hibernate: create table mtm_blog (id bigint not null auto_increment, content VARCHAR(1000) NOT NULL, 
	title VARCHAR(100) NOT NULL, primary key (id)) engine=InnoDB
Hibernate: create table mtm_blog_tag (id bigint not null auto_increment, tag VARCHAR(100) UNIQUE NOT NULL, 
	primary key (id)) engine=InnoDB
Hibernate: alter table mtm_blog_tag_rel add constraint fk_blog_tag_rel_tag foreign key (tag_id) references mtm_blog_tag (id)
Hibernate: alter table mtm_blog_tag_rel add constraint fk_blog_tag_rel_blog foreign key (blog_id) references mtm_blog (id)

數據表:3個
mysql> show create table mtm_blog \G
*************************** 1. row ***************************
       Table: mtm_blog
Create Table: CREATE TABLE `mtm_blog` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `content` varchar(1000) NOT NULL,
  `title` varchar(100) NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
1 row in set (0.00 sec)

mysql> show create table mtm_blog_tag \G
*************************** 1. row ***************************
       Table: mtm_blog_tag
Create Table: CREATE TABLE `mtm_blog_tag` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `tag` varchar(100) NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `tag` (`tag`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
1 row in set (0.00 sec)

mysql>
mysql> show create table mtm_blog_tag_rel \G
*************************** 1. row ***************************
       Table: mtm_blog_tag_rel
Create Table: CREATE TABLE `mtm_blog_tag_rel` (
  `blog_id` bigint(20) NOT NULL,
  `tag_id` bigint(20) NOT NULL,
  KEY `fk_blog_tag_rel_tag` (`tag_id`),
  KEY `fk_blog_tag_rel_blog` (`blog_id`),
  CONSTRAINT `fk_blog_tag_rel_blog` FOREIGN KEY (`blog_id`) REFERENCES `mtm_blog` (`id`),
  CONSTRAINT `fk_blog_tag_rel_tag` FOREIGN KEY (`tag_id`) REFERENCES `mtm_blog_tag` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
1 row in set (0.01 sec)

mysql>

 

說明:來自博客園

1、關聯表不能建立實體類

兩個實體類,生成了3個表:mtm_blog、mtm_blog_tag、mtm_blog_tag_rel,其中,mtm_blog_tag_rel 是 數據關聯表。

注,在 參考文檔1 中,關聯表 也需要建立實體類,但在試驗中發現,工程裏面不能有 關聯表的實體類!否則,啓動報錯

2021-10-20 14:26:12.889 ERROR 10496 --- [           main] j.LocalContainerEntityManagerFactoryBean : 
Failed to initialize JPA EntityManagerFactory: No identifier specified for entity: org.lib.jpamysql.mtm.MtmBlogTagRel

2021-10-20 14:26:12.889  WARN 10496 --- [           main] ConfigServletWebServerApplicationContext : 
Exception encountered during context initialization - cancelling refresh attempt: 
org.springframework.beans.factory.BeanCreationException: Error creating bean with name 
'entityManagerFactory' defined in class path resource 
[org/springframework/boot/autoconfigure/orm/jpa/HibernateJpaConfiguration.class]: Invocation of init method failed; 
nested exception is org.hibernate.AnnotationException: No identifier specified for entity: 
org.lib.jpamysql.mtm.MtmBlogTagRel

工程沒有啓動成功,一個數據表也沒有建立成功。來自博客園

2、數據排序

使用@OrderBy,雙方都可以使用。

3、關聯表默認名稱

@JoinTable 可以不使用 name屬性,此時,建立的 關聯表名稱是 自動的。在本試驗中,會自動建立 關聯表 mtm_blog_tags:

 

多對多案例:來自博客園

1)博客、標籤

2)用戶、角色

3)老師、學生

4)遊客、度假勝地

5)

 

》》》全文完《《《來自博客園

 

源碼:jpa-mysql,其中oto、otm、mtm包下的代碼。來自博客園

 

外鍵 很好用,但是,使用不當會造成嚴重問題。

在《阿里巴巴Java開發手冊》(參考文檔3 可以下載)中,不建議使用外鍵:

 

參考文檔

1、書《Spring Data JPA 從入門到精通》 by 張振華

2、書《MySQL數據庫 原理、設計與應用》 by 黑馬程序員

3、阿里官方Java代碼規範標準《阿里巴巴Java開發手冊 終極版 v1.3.0》下載

4、

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章