Hibernate - 繼承關聯關係對映
對於面向物件的程式設計語言而言,繼承和多型是兩個最基本的概念。Hibernate 的繼承對映可以理解持久化類之間的繼承關係。例如:人和學生之間的關係。學生繼承了人,可以認為學生是一個特殊的人,如果對人進行查詢,學生的例項也將被得到。
Hibernate支援三種繼承對映策略:
- 使用 subclass 進行對映: 對於繼承關係中的父類子類使用同一個表,這就需要在資料庫表中增加額外的區分子類型別的欄位。
- 使用 joined-subclass 進行對映: 將域模型中的每一個子類分別對映到一個獨立的表中,通過關係資料模型中的外來鍵來描述表之間的繼承關係。這也就相當於按照域模型的結構來建立資料庫中的表,並通過外來鍵來建立表之間的繼承關係。
- 使用 union-subclass 進行對映:域模型中的每個類分別單獨對映到一個單獨表,關係資料模型不用考慮域模型中的繼承關係和多型。
【1】採用 subclass 元素的繼承對映
採用 subclass 的繼承對映可以實現對於繼承關係中父類和所有子類使用同一張表。
因為父類和子類的例項全部儲存在同一個表中,因此需要在該表內增加一列,使用該列來區分每行記錄到底是哪個類的例項----這個列被稱為辨別者列(discriminator)。
在這種對映策略下,使用 subclass 來對映子類,使用 class 或 subclass 的 discriminator-value 屬性指定辨別者列的值。
所有子類定義的欄位都不能有非空約束。如果為那些欄位新增非空約束,那麼父類的例項在那些列其實並沒有值,這將引起資料庫完整性衝突,導致父類的例項無法儲存到資料庫中。
① Person類如下:
public class Person { private Integer id; private String name; private int age; public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } @Override public String toString() { return "Person [id=" + id + ", name=" + name + ", age=" + age + "]"; } }
② Student類如下
public class Student extends Person{
private String school;
public String getSchool() {
return school;
}
public void setSchool(String school) {
this.school = school;
}
@Override
public String toString() {
return "Student [school=" + school + ", toString()=" + super.toString() + "]";
}
}
③ Person.hbm.xml如下:
<hibernate-mapping package="com.jane.subclass">
<class name="Person" table="PERSONS" discriminator-value="PERSON">
<id name="id" type="java.lang.Integer">
<column name="ID" />
<generator class="native" />
</id>
<!-- 配置辨別者列 -->
<discriminator column="TYPE" type="string"></discriminator>
<property name="name" type="java.lang.String">
<column name="NAME" />
</property>
<property name="age" type="int">
<column name="AGE" />
</property>
<!-- 對映子類 Student, 使用 subclass 進行對映 -->
<subclass name="Student" discriminator-value="STUDENT">
<property name="school" type="string" column="SCHOOL"></property>
</subclass>
</class>
</hibernate-mapping>
④ 持久化測試
對於子類物件只需把記錄插入到一張資料表中,辨別者列由 Hibernate 自動維護。
程式碼如下:
@Test
public void testSave(){
Person person = new Person();
person.setAge(11);
person.setName("AA");
session.save(person);
Student stu = new Student();
stu.setAge(22);
stu.setName("BB");
stu.setSchool("JANE");
session.save(stu);
}
測試結果如下:
Hibernate:
create table PERSONS (
ID integer not null auto_increment,
TYPE varchar(255) not null,
NAME varchar(255),
AGE integer,
SCHOOL varchar(255),
primary key (ID)
) engine=InnoDB
Hibernate:
insert
into
PERSONS
(NAME, AGE, TYPE)
values
(?, ?, 'PERSON')
Hibernate:
insert
into
PERSONS
(NAME, AGE, SCHOOL, TYPE)
values
(?, ?, ?, 'STUDENT')
⑤ 查詢操作
程式碼如下:
@Test
public void testQuery(){
List<Person> persons = session.createQuery("FROM Person").list();
System.out.println(persons.size());
List<Student> stus = session.createQuery("FROM Student").list();
System.out.println(stus.size());
}
測試結果如下:
Hibernate:
select
person0_.ID as ID1_8_,
person0_.NAME as NAME3_8_,
person0_.AGE as AGE4_8_,
person0_.SCHOOL as SCHOOL5_8_,
person0_.TYPE as TYPE2_8_
from
PERSONS person0_
2
Hibernate:
select
student0_.ID as ID1_8_,
student0_.NAME as NAME3_8_,
student0_.AGE as AGE4_8_,
student0_.SCHOOL as SCHOOL5_8_
from
PERSONS student0_
where
student0_.TYPE='STUDENT'
1
查詢父類時,將所有屬性都查詢出來,查詢SQL中不需要where條件。查詢子類時,只查詢子類繼承父類屬性和子類自有屬性,並且查詢SQL使用了where條件。
不過相同的是,查詢父類或者子類只需要查詢同一張表。
SubClass缺點 :
- 使用了辨別者列。
- 子類獨有的欄位不能新增非空約束。
- 若繼承層次較深, 則資料表的欄位也會較多。
【2】採用 joined-subclass 元素的繼承對映
採用 joined-subclass 元素的繼承對映可以實現每個子類對應一張表。
採用這種對映策略時,父類例項儲存在父類表中,子類例項由父類表和子類表共同儲存。
因為子類例項也是一個特殊的父類例項,因此必然也包含了父類例項的屬性。於是將子類和父類共有的屬性儲存在父類表中,子類增加的屬性,則儲存在子類表中。
在這種對映策略下,無須使用鑑別者列,但需要為每個子類使用 key 元素對映共有主鍵。
子類增加的屬性可以新增非空約束。因為子類的屬性和父類的屬性沒有儲存在同一個表中。
① 修改Person.hbm.xml如下:
<hibernate-mapping package="com.jane.joined.subclass">
<class name="Person" table="PERSONS">
<id name="id" type="java.lang.Integer">
<column name="ID" />
<generator class="native" />
</id>
<property name="name" type="java.lang.String">
<column name="NAME" />
</property>
<property name="age" type="int">
<column name="AGE" />
</property>
<joined-subclass name="Student" table="STUDENTS">
<key column="STUDENT_id"></key>
<property name="school" type="string" column="SCHOOL"></property>
</joined-subclass>
</class>
</hibernate-mapping>
② 持久化測試
程式碼如下:
@Test
public void testSave(){
Person person = new Person();
person.setAge(11);
person.setName("AA");
session.save(person);
Student stu = new Student();
stu.setAge(22);
stu.setName("BB");
stu.setSchool("JANE");
session.save(stu);
}
測試結果如下:
Hibernate:
create table PERSONS (
ID integer not null auto_increment,
NAME varchar(255),
AGE integer,
primary key (ID)
) engine=InnoDB
Hibernate:
create table STUDENTS (
STUDENT_id integer not null,
SCHOOL varchar(255),
primary key (STUDENT_id)
) engine=InnoDB
Hibernate:
alter table STUDENTS
add constraint FK1v1p0142kff4r512ncp4prcy5
foreign key (STUDENT_id)
references PERSONS (ID)
Hibernate:
insert
into
PERSONS
(NAME, AGE)
values
(?, ?)
Hibernate:
insert
into
PERSONS
(NAME, AGE)
values
(?, ?)
Hibernate:
insert
into
STUDENTS
(SCHOOL, STUDENT_id)
values
(?, ?)
可以看到,插入父類是直接插入了persons表;插入子類的時候則插入了兩張表中,將繼承父類的屬性插入到父表中,子類私有屬性插入到子類表中。且子類表中的Student_id與父類表中的主鍵有外來鍵約束關係。
③ 查詢獲取操作
程式碼如下:
@Test
public void testQuery(){
List<Person> persons = session.createQuery("FROM Person").list();
System.out.println(persons.size());
List<Student> stus = session.createQuery("FROM Student").list();
System.out.println(stus.size());
}
測試結果如下:
Hibernate:
select
person0_.ID as ID1_0_,
person0_.NAME as NAME2_0_,
person0_.AGE as AGE3_0_,
person0_1_.SCHOOL as SCHOOL2_1_,
case
when person0_1_.STUDENT_id is not null then 1
when person0_.ID is not null then 0
end as clazz_
from
PERSONS person0_
left outer join
STUDENTS person0_1_
on person0_.ID=person0_1_.STUDENT_id//左外連線查詢
2
Hibernate:
select
student0_.STUDENT_id as ID1_0_,
student0_1_.NAME as NAME2_0_,
student0_1_.AGE as AGE3_0_,
student0_.SCHOOL as SCHOOL2_1_
from
STUDENTS student0_
inner join
PERSONS student0_1_
on student0_.STUDENT_id=student0_1_.ID//內連線查詢
1
Person表查詢語句執行結果如下:
查詢父類記錄, 使用一個左外連線查詢; 對於子類記錄, 使用一個內連線查詢。
joined-subclass優點是:
- 不需要使用了辨別者列.
- 子類獨有的欄位能新增非空約束.
- 沒有冗餘的欄位.
【3】採用 union-subclass 元素的繼承對映
採用 union-subclass 元素可以實現將每一個實體物件分別對映到一個獨立的表中。
子類增加的屬性可以有非空約束 — 即父類例項的資料儲存在父表中,而子類例項的資料儲存在子類表中。
子類例項的資料僅儲存在子類表中, 而在父類表中沒有任何記錄。
在這種對映策略下,子類表的欄位會比父類表的對映欄位要多,因為子類表的欄位等於父類表的欄位、加子類增加屬性的總和。
在這種對映策略下,既不需要使用鑑別者列,也無須使用 key 元素來對映共有主鍵。
使用 union-subclass 對映策略時不可使用 identity 的主鍵生成策略, 因為同一類繼承層次中所有實體類都需要使用同一個主鍵種子, 即多個持久化實體對應的記錄的主鍵應該是連續的。受此影響, 也不該使用 native 主鍵生成策略, 因為 native 會根據資料庫來選擇使用 identity 或 sequence。
① 修改Person.hbm.xml如下:
<hibernate-mapping package="com.jane.union.subclass">
<class name="Person" table="PERSONS">
<id name="id" type="java.lang.Integer">
<column name="ID" />
<generator class="increment" />
</id>
<property name="name" type="java.lang.String">
<column name="NAME" />
</property>
<property name="age" type="int">
<column name="AGE" />
</property>
<union-subclass name="Student" table="STUDENTS">
<property name="school" column="SCHOOL" type="string"></property>
</union-subclass>
</class>
</hibernate-mapping>
② 持久化測試
程式碼如下:
@Test
public void testSave(){
Person person = new Person();
person.setAge(11);
person.setName("AA");
session.save(person);
Student stu = new Student();
stu.setAge(22);
stu.setName("BB");
stu.setSchool("JANE");
session.save(stu);
}
測試結果如下:
Hibernate:
select
max(ids_.mx)
from
( select
max(ID) as mx
from
STUDENTS
union
select
max(ID) as mx
from
PERSONS
) ids_
Hibernate:
insert
into
PERSONS
(NAME, AGE, ID)
values
(?, ?, ?)
Hibernate:
insert
into
STUDENTS
(NAME, AGE, SCHOOL, ID)
values
(?, ?, ?, ?)
可以看到父類和子類各自插入了一張表中,子類屬性包含父類屬性,子類和父類直接沒有外來鍵關係。
③ 查詢物件測試
程式碼如下:
@Test
public void testQuery(){
List<Person> persons = session.createQuery("FROM Person").list();
System.out.println(persons.size());
List<Student> stus = session.createQuery("FROM Student").list();
System.out.println(stus.size());
}
測試結果如下:
Hibernate:
select
person0_.ID as ID1_0_,
person0_.NAME as NAME2_0_,
person0_.AGE as AGE3_0_,
person0_.SCHOOL as SCHOOL1_1_,
person0_.clazz_ as clazz_
from
( select
ID,
NAME,
AGE,
null as SCHOOL,
0 as clazz_
from
PERSONS
union
select
ID,
NAME,
AGE,
SCHOOL,
1 as clazz_
from
STUDENTS
) person0_
2
Hibernate:
select
student0_.ID as ID1_0_,
student0_.NAME as NAME2_0_,
student0_.AGE as AGE3_0_,
student0_.SCHOOL as SCHOOL1_1_
from
STUDENTS student0_
1
可以看到,查詢父類記錄, 需把父表和子表記錄彙總到一起再做查詢–如上所示做了子查詢union。查詢子類記錄則只需要從一張表中簡單查詢即可。
④ 更新物件測試
程式碼如下:
@Test
public void testUpdate(){
String hql = "UPDATE Person p SET p.age = 20";
session.createQuery(hql).executeUpdate();
}
測試結果如下:
Hibernate:
create temporary table if not exists HT_PERSONS (ID integer not null)
Hibernate:
insert
into
HT_PERSONS
select
person0_.ID as ID
from
( select
ID,
NAME,
AGE,
null as SCHOOL,
0 as clazz_
from
PERSONS
union
select
ID,
NAME,
AGE,
SCHOOL,
1 as clazz_
from
STUDENTS
) person0_
Hibernate:
update
PERSONS
set
AGE=20
where
(
ID
) IN (
select
ID
from
HT_PERSONS
)
Hibernate:
update
PERSONS
set
AGE=20
where
(
ID
) IN (
select
ID
from
HT_PERSONS
)
Hibernate:
update
STUDENTS
set
AGE=20
where
(
ID
) IN (
select
ID
from
HT_PERSONS
)
Hibernate:
drop temporary table HT_PERSONS
更新是相當麻煩。
union-subclass優點:
- 無需使用辨別者列.
- 子類獨有的欄位能新增非空約束.
union-subclass缺點:
- 存在冗餘的欄位
- 若更新父表的欄位, 則更新的效率較低。
【4】三種繼承對映方式分析比較
比較方面 | union-subclass | subclass | joined-subclass |
---|---|---|---|
建立關係模型的原則 | 每個具體類對應一張表 | 父類和子類共用一張表 | 父類單獨一張表,每個子類一張表,子類表不包含父類屬性列 ;子類和父類有外來鍵關聯關係 |
關係模型的優缺點 | 表中存在重複欄位 | 表中引入了區分子類的欄位;不能為子類屬性設定非空約束 | 符合關係模型設計原則,且無欄位重複 |
可維護性 | 如果需要對基類進行修改,則需要對基類及其子類所對應的所有表都進行修改 | 只需要修改一張表,很方便 | 維護比較方便,對每個類的修改只需要修改其所對應的表 |
靈活性 | 對映的靈活性很大,子類可以對包括基類屬性在內的每一個屬性進行單獨的配置 | 靈活性差,表中的冗餘欄位會隨著子類的增多而增多 | 靈活性很好,完全是參照物件繼承的方式進行對映配置 |
查詢的效能 | 對於子類的查詢只需要訪問單獨的表;但對父類的查詢需要訪問所有表 | 在任何情況下的查詢都需要處理這一張表 | 對於父類的查詢需要使用左外連線;而子類查詢則需要使用內連線 |
維護的效能 | 對於單個物件的持久化操作只需要處理一個表 | 對於單個物件的持久化操作只需要處理一個表 | 對於子類的持久化操作至少需要處理兩個表;對於父類的持久化操作只需要處理一個表 |