1 Hibernate架构
2 领域模型-Domain Model
2.1 映射类型
广义上,Hibernate将类型分为两类:值类型、实体类型。
2.1.1 值类型
值类型分3种:
2.1.2 实体类型
@Table用于标注类为数据库表队形的实体类
@Id用于标注实体类属性为表id字段
@Columbu用于标注实体类属性为表字段
2.2 命名策略
略
2.3 基本类型-Basic Types
基本类型详解
2.3.1 @Basic注解
此注解可用于标注实体类 (基本数据类型的) 属性为表字段,也可以忽略使用此注解去标注实体类属性因为会自动假设。
@Basic(optional=true,fetch=FETCH.EAGER)定义了optional、fetch两个属性。
//示例
class User {
...
@Basic
private String username;
...
}
2.3.2 @Columbu注解
JPA定义了隐式命名表字段名的规则。
对于基本数据类型的隐式命名规则是:表字段名和属性名相同。
在属性名和表字段名不一致时,可用@Column注解标注实体类属性并显示命名属性对应的表字段名。
//示例
// 表名:Product
@Entity(name = "Product")
public class Product {
...
//字段名:NOTES
@Column( name = "NOTES" )
private String description;
...
}
2.3.n
略
2.4 可嵌入类型-Embeddable Types
略
2.5 实体类型-@Entity-Entity Types
略
2.6 id-Identifiers
略
2.7 关联关系-Associations
两个或以上实体类通过加入语义,在关系数据库中也具有直接等价物(例如外键)。它建立了子实体和父实体之间的关系。
2.7.1 @ManyToOne
@ManyToOne :即子实体类(子实体类——子表——引用父表主键作为外键的表),有一个属性数据类型是父实体类类型,用@ManyToOne标注这个属性建立关联关系。
1 @ManyToOne关联:
Java代码:
@Entity(name = "Person")
public static class Person {
@Id
@GeneratedValue
private Long id;
//Getters and setters are omitted for brevity
}
@Entity(name = "Phone")
public static class Phone {
@Id
@GeneratedValue
private Long id;
@Column(name = "`number`")
private String number;
@ManyToOne
// person_id:外键,PERSON_ID_FK:外键约束名
@JoinColumn(name = "person_id",foreignKey = @ForeignKey(name = "PERSON_ID_FK"))
private Person person;
//Getters and setters are omitted for brevity
}
对应数据库关系:
CREATE TABLE Person (
id BIGINT NOT NULL ,
PRIMARY KEY ( id )
)
CREATE TABLE Phone (
id BIGINT NOT NULL ,
number VARCHAR(255) ,
person_id BIGINT ,
PRIMARY KEY ( id )
)
ALTER TABLE Phone
ADD CONSTRAINT PERSON_ID_FK
FOREIGN KEY (person_id) REFERENCES Person
2 @ManyToOne关联生命周期:
Java代码:
Person person = new Person();
entityManager.persist( person );
Phone phone = new Phone( "123-456-7890" );
phone.setPerson( person );
entityManager.persist( phone );
entityManager.flush();
phone.setPerson( null );
对应数据库关系:
INSERT INTO Person ( id )
VALUES ( 1 )
INSERT INTO Phone ( number, person_id, id )
VALUES ( '123-456-7890', 1, 2 )
UPDATE Phone
SET number = '123-456-7890',
person_id = NULL
WHERE id = 2
2.7.2 @OneToMany
@OneToMany关联将父实体与一个或多个子实体链接。
如果在子端@OneToMany没有镜像@ManyToOne关联,则@OneToMany关联是单向的。如果@ManyToOne子方存在关联,则@OneToMany关联是双向的,应用程序开发人员可以从两端导航此关系。
单向@OneToMany
当使用单向@OneToMany关联时,Hibernate会使用两个连接实体之间的链接表。(即创建第三个表)
1 @OnToMany“单向”关联 关系建立
Java代码
@Entity(name = "Person")
public static class Person {
@Id
@GeneratedValue
private Long id;
//
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
private List<Phone> phones = new ArrayList<>();
//Getters and setters are omitted for brevity
}
@Entity(name = "Phone")
public static class Phone {
@Id
@GeneratedValue
private Long id;
@Column(name = "`number`")
private String number;
//Getters and setters are omitted for brevity
}
对应数据库关系:
CREATE TABLE Person (
id BIGINT NOT NULL ,
PRIMARY KEY ( id )
)
CREATE TABLE Person_Phone (
Person_id BIGINT NOT NULL ,
phones_id BIGINT NOT NULL
)
CREATE TABLE Phone (
id BIGINT NOT NULL ,
number VARCHAR(255) ,
PRIMARY KEY ( id )
)
// 第三个表添加唯一约束
ALTER TABLE Person_Phone
ADD CONSTRAINT UK_9uhc5itwc9h5gcng944pcaslf
UNIQUE (phones_id)
// 第三个表添加外键
ALTER TABLE Person_Phone
ADD CONSTRAINT FKr38us2n8g5p9rj0b494sd3391
FOREIGN KEY (phones_id) REFERENCES Phone
// 第三个表添加外键
ALTER TABLE Person_Phone
ADD CONSTRAINT FK2ex4e4p7w1cj310kg2woisjl2
FOREIGN KEY (Person_id) REFERENCES Person
??? 根据@OneToMany定义,关联是父关联,即使它是单向关联或双向关联。只有关联的父方才有意义将其实体状态转换级联到子级。
2 @OnToMany“级联”关联
Java代码:
Person person = new Person();
Phone phone1 = new Phone( "123-456-7890" );
Phone phone2 = new Phone( "321-654-0987" );
person.getPhones().add( phone1 );
person.getPhones().add( phone2 );
entityManager.persist( person );
// flush方法,刷新
entityManager.flush();
person.getPhones().remove( phone1 );
对应数据库关系:
Java添加属性对象对应的sql过程: 先插入父表记录,再子表插入记录,最后链接表(第三个表)插入记录。
Java删除属性对象对应的sql过程: 先根据父表id删除链接表记录,再链接表插入没被删除的属性对象记录,最后子表删除链接表中被删除子表id对应的记录。
// Java添加属性对象对应的sql过程:先插入父表记录,再子表插入记录,最后链接表(第三个表)插入记录。
INSERT INTO Person
( id )
VALUES ( 1 )
INSERT INTO Phone
( number, id )
VALUES ( '123-456-7890', 2 )
INSERT INTO Phone
( number, id )
VALUES ( '321-654-0987', 3 )
INSERT INTO Person_Phone
( Person_id, phones_id )
VALUES ( 1, 2 )
INSERT INTO Person_Phone
( Person_id, phones_id )
VALUES ( 1, 3 )
// Java删除属性对象对应的sql过程: 先根据父表id删除链接表记录,再链接表插入没被删除的属性对象记录,最后子表删除链接表中被删除子表id对应的记录。
DELETE FROM Person_Phone
WHERE Person_id = 1
INSERT INTO Person_Phone
( Person_id, phones_id )
VALUES ( 1, 3 )
DELETE FROM Phone
WHERE id = 2
当持久化Person实体时,级联也会将持久操作传播给基础Phone子节点。Phone从phone集合中删除a 后,将从链接表中删除关联行,该orphanRemoval属性也将触发Phone删除。
在删除子实体时,“单向关联效率不高”。在上面的示例中,在刷新持久性化上下文时,Hibernate从链接表(例如Person_Phone)中删除与父Person实体关联的所有数据库行,并重新插入仍在@OneToMany集合中找到的那些行。
另一方面,“双向@OneToMany关联更有效”,因为子实体控制关联。
双向@OneToMany
双向@OneToMany关联需要@ManyToOne 在子实体类进行关联。尽管域模型公开了两个方面来导航此关联,但在幕后,关系数据库只有一个用于此关系的外键。
每个双向关联必须只有一个拥有方(子实体类),另一个被称为反方(mappedBy,父实体类)。
1 @OneToMany关联映射到@ManyToOne一边
Java代码:
@Entity(name = "Person")
public static class Person {
@Id
@GeneratedValue
private Long id;
@OneToMany(mappedBy = "person", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Phone> phones = new ArrayList<>();
//Getters and setters are omitted for brevity
// 父类声明添加子类的方法
public void addPhone(Phone phone) {
phones.add( phone );
phone.setPerson( this );
}
// 父类声明删除子类的方法
public void removePhone(Phone phone) {
phones.remove( phone );
phone.setPerson( null );
}
}
@Entity(name = "Phone")
public static class Phone {
@Id
@GeneratedValue
private Long id;
// @NaturalId注解(电话号码是唯一的)
@NaturalId
@Column(name = "number", unique = true)
private String number;
@ManyToOne
private Person person;
//Getters and setters are omitted for brevity
@Override
public boolean equals(Object o) {
if ( this == o ) {
return true;
}
if ( o == null || getClass() != o.getClass() ) {
return false;
}
Phone phone = (Phone) o;
return Objects.equals( number, phone.number );
}
@Override
public int hashCode() {
return Objects.hash( number );
}
}
对应的数据库关系:
CREATE TABLE Person (
id BIGINT NOT NULL ,
PRIMARY KEY ( id )
)
CREATE TABLE Phone (
id BIGINT NOT NULL ,
number VARCHAR(255) ,
person_id BIGINT ,
PRIMARY KEY ( id )
)
ALTER TABLE Phone
ADD CONSTRAINT UK_l329ab0g4c1t78onljnxmbnp6
UNIQUE (number)
ALTER TABLE Phone
ADD CONSTRAINT FKmw13yfsjypiiq0i1osdkaeqpg
FOREIGN KEY (person_id) REFERENCES Person
无论何时形成双向关联,应用程序开发人员都必须确保双方始终保持同步。
addPhone()和removePhone()是同步父类和子类,每当子元素被添加或移除的实用方法。
2 @OneToMany具有所有者@ManyToOne侧生命周期的双向
Java代码:
Person person = new Person();
Phone phone1 = new Phone( "123-456-7890" );
Phone phone2 = new Phone( "321-654-0987" );
person.addPhone( phone1 );
person.addPhone( phone2 );
entityManager.persist( person );
entityManager.flush();
person.removePhone( phone1 );
对应数据库关系:
INSERT INTO Person
( id )
VALUES ( 1 )
INSERT INTO Phone
( "number", person_id, id )
VALUES ( '123-456-7890', 1, 2 )
INSERT INTO Phone
( "number", person_id, id )
VALUES ( '321-654-0987', 1, 3 )
DELETE FROM Phone
WHERE id = 2
与单向@OneToMany不同,双向@OneToMany在管理集合持久性状态时更有效。每个集合元素删除只需要一次更新数据库(其中外键列设置为NULL)。如果子实体生命周期绑定到其拥有的父级,则子级在没有父级的情况下不能存在,那么我们可以使用注解@OneToMany的orphan-removal属性来关联该父类和分离的孩子会触发实际子表行。DELETE语句也是如此。
2.7.3 @OneToOne
@OneToOne关联可以是单向或双向的。单向关联遵循关系数据库外键语义,客户端拥有关系。双向关联也具有@OneToOne的mappedBy属性在父方面。
单向@OneToOne
1 单向@OneToOne
Java代码:
@Entity(name = "Phone")
public static class Phone {
@Id
@GeneratedValue
private Long id;
@Column(name = "`number`")
private String number;
@OneToOne
@JoinColumn(name = "details_id")
private PhoneDetails details;
//Getters and setters are omitted for brevity
}
@Entity(name = "PhoneDetails")
public static class PhoneDetails {
@Id
@GeneratedValue
private Long id;
private String provider;
private String technology;
//Getters and setters are omitted for brevity
}
对应数据库关系:
CREATE TABLE Phone (
id BIGINT NOT NULL ,
number VARCHAR(255) ,
details_id BIGINT ,
PRIMARY KEY ( id )
)
CREATE TABLE PhoneDetails (
id BIGINT NOT NULL ,
provider VARCHAR(255) ,
technology VARCHAR(255) ,
PRIMARY KEY ( id )
)
ALTER TABLE Phone
ADD CONSTRAINT FKnoj7cj83ppfqbnvqqa5kolub7
FOREIGN KEY (details_id) REFERENCES PhoneDetails
从sql语句看,底层架构与单向@ManyToOne关联相同,因为客户端基于外键列控制关系。
双向@OneToOne
1 双向@OneToOne
Java代码:
@Entity(name = "Phone")
public static class Phone {
@Id
@GeneratedValue
private Long id;
@Column(name = "`number`")
private String number;
@OneToOne(mappedBy = "phone",cascade = CascadeType.ALL,orphanRemoval = true,fetch =FetchType.LAZY)
private PhoneDetails details;
//Getters and setters are omitted for brevity
public void addDetails(PhoneDetails details) {
details.setPhone( this );
this.details = details;
}
public void removeDetails() {
if ( details != null ) {
details.setPhone( null );
this.details = null;
}
}
}
@Entity(name = "PhoneDetails")
public static class PhoneDetails {
@Id
@GeneratedValue
private Long id;
private String provider;
private String technology;
@OneToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "phone_id")
private Phone phone;
//Getters and setters are omitted for brevity
}
对应数据库关系:
CREATE TABLE Phone (
id BIGINT NOT NULL ,
number VARCHAR(255) ,
PRIMARY KEY ( id )
)
CREATE TABLE PhoneDetails (
id BIGINT NOT NULL ,
provider VARCHAR(255) ,
technology VARCHAR(255) ,
phone_id BIGINT ,
PRIMARY KEY ( id )
)
ALTER TABLE PhoneDetails
ADD CONSTRAINT FKeotuev8ja8v0sdh29dynqj05p
FOREIGN KEY (phone_id) REFERENCES Phone
PhoneDetails拥有关联,并且,与任何双向关联一样,父端可以通过级联将其生命周期传播到子端。
2 双向@OneToOne生命周期
Java代码:
Phone phone = new Phone( "123-456-7890" );
PhoneDetails details = new PhoneDetails( "T-Mobile", "GSM" );
phone.addDetails( details );
entityManager.persist( phone );
对应数据库关系:
INSERT INTO Phone ( number, id )
VALUES ( '123-456-7890', 1 )
INSERT INTO PhoneDetails ( phone_id, provider, technology, id )
VALUES ( 1, 'T-Mobile', 'GSM', 2 )
当使用双向@OneToOne关联时,Hibernate在获取子端时强制执行唯一约束。如果有多个子节点与同一个父节点关联,则Hibernate将抛出一个org.hibernate.exception.ConstraintViolationException。
双向@OneToOne唯一约束
Java代码:
PhoneDetails otherDetails = new PhoneDetails( "T-Mobile", "CDMA" );
otherDetails.setPhone( phone );
entityManager.persist( otherDetails );
entityManager.flush();
entityManager.clear();
//throws javax.persistence.PersistenceException: org.hibernate.HibernateException: More than one row with the given identifier was found: 1
phone = entityManager.find( Phone.class, phone.getId());
双向@OneToOne懒惰关联
尽管可能使用注解为了延迟提取关联的父方,但Hibernate无法遵守此请求,因为它无法知道关联父方是否存在。
确定子方是否存在关联父方记录的唯一方法是使用辅助查询来获取子关联。因为这会导致N + 1查询问题,所以使用 @OneToOne与@MapsId注释的单向关联会更有效。
如果您确实需要使用双向关联,并且希望确保始终是懒惰地获取它,那么您需要启用延迟状态初始化字节码增强并使用 @LazyToOne注释。
双向@OneToOne懒惰父方关联
Java代码:
@Entity(name = "Phone")
public static class Phone {
@Id
@GeneratedValue
private Long id;
@Column(name = "`number`")
private String number;
@OneToOne(
mappedBy = "phone",
cascade = CascadeType.ALL,
orphanRemoval = true,
fetch = FetchType.LAZY
)
@LazyToOne( LazyToOneOption.NO_PROXY )
private PhoneDetails details;
//Getters and setters are omitted for brevity
public void addDetails(PhoneDetails details) {
details.setPhone( this );
this.details = details;
}
public void removeDetails() {
if ( details != null ) {
details.setPhone( null );
this.details = null;
}
}
}
@Entity(name = "PhoneDetails")
public static class PhoneDetails {
@Id
@GeneratedValue
private Long id;
private String provider;
private String technology;
@OneToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "phone_id")
private Phone phone;
//Getters and setters are omitted for brevity
}
2.7.4 @ManyToMany
@ManyToMany关联需要一个连接两个实体的链接表(第三个表)。与@OneToMany关联一样,@ManyToMany可以是单向的,也可以是双向的。
单向@ManyToMany
1 单向@ManyToMany
Java代码:
@Entity(name = "Person")
public static class Person {
@Id
@GeneratedValue
private Long id;
@ManyToMany(cascade = {CascadeType.PERSIST, CascadeType.MERGE})
private List<Address> addresses = new ArrayList<>();
//Getters and setters are omitted for brevity
}
@Entity(name = "Address")
public static class Address {
@Id
@GeneratedValue
private Long id;
private String street;
@Column(name = "`number`")
private String number;
//Getters and setters are omitted for brevity
}
对应数据库关系:
CREATE TABLE Address (
id BIGINT NOT NULL ,
number VARCHAR(255) ,
street VARCHAR(255) ,
PRIMARY KEY ( id )
)
CREATE TABLE Person (
id BIGINT NOT NULL ,
PRIMARY KEY ( id )
)
CREATE TABLE Person_Address (
Person_id BIGINT NOT NULL ,
addresses_id BIGINT NOT NULL
)
ALTER TABLE Person_Address
ADD CONSTRAINT FKm7j0bnabh2yr0pe99il1d066u
FOREIGN KEY (addresses_id) REFERENCES Address
ALTER TABLE Person_Address
ADD CONSTRAINT FKba7rc9qe2vh44u93u0p2auwti
FOREIGN KEY (Person_id) REFERENCES Person
就像单向@OneToMany关联一样,链接表由拥有方控制。
当从@ManyToMany集合中删除实体时,Hibernate只是删除链接表中的连接记录。但是,此操作需要删除与给定父级关联的所有条目,并重新创建当前运行的持久化上下文中列出的条目。
2 单向@ManyToMany生命周期
Java代码:
Person person1 = new Person();
Person person2 = new Person();
Address address1 = new Address( "12th Avenue", "12A" );
Address address2 = new Address( "18th Avenue", "18B" );
person1.getAddresses().add( address1 );
person1.getAddresses().add( address2 );
person2.getAddresses().add( address1 );
entityManager.persist( person1 );
entityManager.persist( person2 );
entityManager.flush();
person1.getAddresses().remove( address1 );
对应数据库关系:
INSERT INTO Person ( id )
VALUES ( 1 )
INSERT INTO Address ( number, street, id )
VALUES ( '12A', '12th Avenue', 2 )
INSERT INTO Address ( number, street, id )
VALUES ( '18B', '18th Avenue', 3 )
INSERT INTO Person ( id )
VALUES ( 4 )
INSERT INTO Person_Address ( Person_id, addresses_id )
VALUES ( 1, 2 )
INSERT INTO Person_Address ( Person_id, addresses_id )
VALUES ( 1, 3 )
INSERT INTO Person_Address ( Person_id, addresses_id )
VALUES ( 4, 2 )
DELETE FROM Person_Address
WHERE Person_id = 1
INSERT INTO Person_Address ( Person_id, addresses_id )
VALUES ( 1, 3 )
对于@ManyToMany关联,REMOVE实体状态转换没有意义进行级联,因为它将传播到链接表之外。由于另一方可能被父方的其他实体引用,因此自动删除可能会有ConstraintViolationException。
例如,如果@ManyToMany(cascade = CascadeType.ALL)已定义并且第一个人将被删除,则Hibernate会抛出异常,因为另一个人仍然与正在删除的地址相关联。如下代码:
Person person1 = entityManager.find(Person.class, personId);
entityManager.remove(person1);
Caused by: javax.persistence.PersistenceException: org.hibernate.exception.ConstraintViolationException: could not execute statement
Caused by: org.hibernate.exception.ConstraintViolationException: could not execute statement
Caused by: java.sql.SQLIntegrityConstraintViolationException: integrity constraint violation: foreign key no action; FKM7J0BNABH2YR0PE99IL1D066U table: PERSON_ADDRESS
通过简单地删除父端,Hibernate可以安全地删除关联的链接记录,如下:
3 单向@ManyToMany 实体删除
Person person1 = entityManager.find( Person.class, personId );
entityManager.remove( person1 );
对应sql:
DELETE FROM Person_Address
WHERE Person_id = 1
DELETE FROM Person
WHERE id = 1
双向@ManyToMany
双向@ManyToMany关联具有拥有权和支持权mappedBy。为了保持双方的同步性,最好提供辅助方法来添加或删除子实体。
1 双向@ManyToMany
@Entity(name = "Person")
public static class Person {
@Id
@GeneratedValue
private Long id;
@NaturalId
private String registrationNumber;
@ManyToMany(cascade = {CascadeType.PERSIST, CascadeType.MERGE})
private List<Address> addresses = new ArrayList<>();
//Getters and setters are omitted for brevity
public void addAddress(Address address) {
addresses.add( address );
address.getOwners().add( this );
}
public void removeAddress(Address address) {
addresses.remove( address );
address.getOwners().remove( this );
}
@Override
public boolean equals(Object o) {
if ( this == o ) {
return true;
}
if ( o == null || getClass() != o.getClass() ) {
return false;
}
Person person = (Person) o;
return Objects.equals( registrationNumber, person.registrationNumber );
}
@Override
public int hashCode() {
return Objects.hash( registrationNumber );
}
}
@Entity(name = "Address")
public static class Address {
@Id
@GeneratedValue
private Long id;
private String street;
@Column(name = "`number`")
private String number;
private String postalCode;
@ManyToMany(mappedBy = "addresses")
private List<Person> owners = new ArrayList<>();
//Getters and setters are omitted for brevity
@Override
public boolean equals(Object o) {
if ( this == o ) {
return true;
}
if ( o == null || getClass() != o.getClass() ) {
return false;
}
Address address = (Address) o;
return Objects.equals( street, address.street ) &&
Objects.equals( number, address.number ) &&
Objects.equals( postalCode, address.postalCode );
}
@Override
public int hashCode() {
return Objects.hash( street, number, postalCode );
}
}
对应sql:
CREATE TABLE Address (
id BIGINT NOT NULL ,
number VARCHAR(255) ,
postalCode VARCHAR(255) ,
street VARCHAR(255) ,
PRIMARY KEY ( id )
)
CREATE TABLE Person (
id BIGINT NOT NULL ,
registrationNumber VARCHAR(255) ,
PRIMARY KEY ( id )
)
CREATE TABLE Person_Address (
owners_id BIGINT NOT NULL ,
addresses_id BIGINT NOT NULL
)
ALTER TABLE Person
ADD CONSTRAINT UK_23enodonj49jm8uwec4i7y37f
UNIQUE (registrationNumber)
ALTER TABLE Person_Address
ADD CONSTRAINT FKm7j0bnabh2yr0pe99il1d066u
FOREIGN KEY (addresses_id) REFERENCES Address
ALTER TABLE Person_Address
ADD CONSTRAINT FKbn86l24gmxdv2vmekayqcsgup
FOREIGN KEY (owners_id) REFERENCES Person
有了辅助方法,可以简化同步管理,如下:
2 双向@ManyToMany 生命周期
Java:
Person person1 = new Person( "ABC-123" );
Person person2 = new Person( "DEF-456" );
Address address1 = new Address( "12th Avenue", "12A", "4005A" );
Address address2 = new Address( "18th Avenue", "18B", "4007B" );
person1.addAddress( address1 );
person1.addAddress( address2 );
person2.addAddress( address1 );
entityManager.persist( person1 );
entityManager.persist( person2 );
entityManager.flush();
person1.removeAddress( address1 );
对应sql:
INSERT INTO Person ( registrationNumber, id )
VALUES ( 'ABC-123', 1 )
INSERT INTO Address ( number, postalCode, street, id )
VALUES ( '12A', '4005A', '12th Avenue', 2 )
INSERT INTO Address ( number, postalCode, street, id )
VALUES ( '18B', '4007B', '18th Avenue', 3 )
INSERT INTO Person ( registrationNumber, id )
VALUES ( 'DEF-456', 4 )
INSERT INTO Person_Address ( owners_id, addresses_id )
VALUES ( 1, 2 )
INSERT INTO Person_Address ( owners_id, addresses_id )
VALUES ( 1, 3 )
INSERT INTO Person_Address ( owners_id, addresses_id )
VALUES ( 4, 2 )
DELETE FROM Person_Address
WHERE owners_id = 1
INSERT INTO Person_Address ( owners_id, addresses_id )
VALUES ( 1, 3 )
如果@OneToMany在移除或改变子元素的顺序时双向关联执行得更好,则该@ManyToMany关系不能从这种优化中受益,因为外键侧不受控制。要克服此限制,必须直接公开链接表,并将@ManyToMany关联拆分为两个双向@OneToMany关系。
具有链接实体的双向多对多
对于大多数自然@ManyToMany关联遵循数据库模式所采用的相同逻辑,并且链接表具有关联实体,该实体控制需要连接的两侧的关系。
1 具有实体链接的双向多对多
Java:
@Entity(name = "Person")
public static class Person implements Serializable {
@Id
@GeneratedValue
private Long id;
@NaturalId
private String registrationNumber;
@OneToMany(
mappedBy = "person",
cascade = CascadeType.ALL,
orphanRemoval = true
)
private List<PersonAddress> addresses = new ArrayList<>();
//Getters and setters are omitted for brevity
public void addAddress(Address address) {
PersonAddress personAddress = new PersonAddress( this, address );
addresses.add( personAddress );
address.getOwners().add( personAddress );
}
public void removeAddress(Address address) {
PersonAddress personAddress = new PersonAddress( this, address );
address.getOwners().remove( personAddress );
addresses.remove( personAddress );
personAddress.setPerson( null );
personAddress.setAddress( null );
}
@Override
public boolean equals(Object o) {
if ( this == o ) {
return true;
}
if ( o == null || getClass() != o.getClass() ) {
return false;
}
Person person = (Person) o;
return Objects.equals( registrationNumber, person.registrationNumber );
}
@Override
public int hashCode() {
return Objects.hash( registrationNumber );
}
}
@Entity(name = "PersonAddress")
public static class PersonAddress implements Serializable {
@Id
@ManyToOne
private Person person;
@Id
@ManyToOne
private Address address;
//Getters and setters are omitted for brevity
@Override
public boolean equals(Object o) {
if ( this == o ) {
return true;
}
if ( o == null || getClass() != o.getClass() ) {
return false;
}
PersonAddress that = (PersonAddress) o;
return Objects.equals( person, that.person ) &&
Objects.equals( address, that.address );
}
@Override
public int hashCode() {
return Objects.hash( person, address );
}
}
@Entity(name = "Address")
public static class Address implements Serializable {
@Id
@GeneratedValue
private Long id;
private String street;
@Column(name = "`number`")
private String number;
private String postalCode;
@OneToMany(
mappedBy = "address",
cascade = CascadeType.ALL,
orphanRemoval = true
)
private List<PersonAddress> owners = new ArrayList<>();
//Getters and setters are omitted for brevity
@Override
public boolean equals(Object o) {
if ( this == o ) {
return true;
}
if ( o == null || getClass() != o.getClass() ) {
return false;
}
Address address = (Address) o;
return Objects.equals( street, address.street ) &&
Objects.equals( number, address.number ) &&
Objects.equals( postalCode, address.postalCode );
}
@Override
public int hashCode() {
return Objects.hash( street, number, postalCode );
}
}
对应sql:
CREATE TABLE Address (
id BIGINT NOT NULL ,
number VARCHAR(255) ,
postalCode VARCHAR(255) ,
street VARCHAR(255) ,
PRIMARY KEY ( id )
)
CREATE TABLE Person (
id BIGINT NOT NULL ,
registrationNumber VARCHAR(255) ,
PRIMARY KEY ( id )
)
CREATE TABLE PersonAddress (
person_id BIGINT NOT NULL ,
address_id BIGINT NOT NULL ,
PRIMARY KEY ( person_id, address_id )
)
ALTER TABLE Person
ADD CONSTRAINT UK_23enodonj49jm8uwec4i7y37f
UNIQUE (registrationNumber)
ALTER TABLE PersonAddress
ADD CONSTRAINT FK8b3lru5fyej1aarjflamwghqq
FOREIGN KEY (person_id) REFERENCES Person
ALTER TABLE PersonAddress
ADD CONSTRAINT FK7p69mgialumhegyl4byrh65jk
FOREIGN KEY (address_id) REFERENCES Address
既有Person和Address有mappedBy @OneToMany一面,而PersonAddress拥有person和address @ManyToOne协会。因为这种映射是由两个双向关联构成的,所以辅助方法更具相关性。
上述示例对链接实体使用特定于Hibernate的映射,因为JPA不允许从多个@ManyToOne关联中构建复合标识符。
实体状态转换比以前的双向@ManyToMany情况更好地进行管理。
2 具有实体链接声明周期的双向多对多
Java:
Person person1 = new Person( "ABC-123" );
Person person2 = new Person( "DEF-456" );
Address address1 = new Address( "12th Avenue", "12A", "4005A" );
Address address2 = new Address( "18th Avenue", "18B", "4007B" );
entityManager.persist( person1 );
entityManager.persist( person2 );
entityManager.persist( address1 );
entityManager.persist( address2 );
person1.addAddress( address1 );
person1.addAddress( address2 );
person2.addAddress( address1 );
entityManager.flush();
log.info( "Removing address" );
person1.removeAddress( address1 );
对应sql:
INSERT INTO Person ( registrationNumber, id )
VALUES ( 'ABC-123', 1 )
INSERT INTO Person ( registrationNumber, id )
VALUES ( 'DEF-456', 2 )
INSERT INTO Address ( number, postalCode, street, id )
VALUES ( '12A', '4005A', '12th Avenue', 3 )
INSERT INTO Address ( number, postalCode, street, id )
VALUES ( '18B', '4007B', '18th Avenue', 4 )
INSERT INTO PersonAddress ( person_id, address_id )
VALUES ( 1, 3 )
INSERT INTO PersonAddress ( person_id, address_id )
VALUES ( 1, 4 )
INSERT INTO PersonAddress ( person_id, address_id )
VALUES ( 2, 3 )
DELETE FROM PersonAddress
WHERE person_id = 1 AND address_id = 3
只执行了一个删除语句,因为这次关联是由@ManyToOne一方控制的,只需要监视底层外键关系的状态以触发正确的DML语句。