Hibernate的继承映射可以立即为两个持久化类之间的映射关系,例如老师和人之间的关系,老师继承了人,如果对人进行查询,老师实例也将被查到,而无须关注人和老师在数据库底层的存储。
Hibernate支持多种继承映射策略。常见的有三种,将所有类树的实体映射到同一张表;每个子类实体只保存自身属性,最后连接所有表;每个子类实体保存父类及本身的属性,一个实体对应一张表。
下面用一个例子说明这三种继承策略。假如现在一个系统中有四个实体为Person, Employee, Manager, Customer, 这是四个持久化类,另外Person还有一个Address属性,Address只是一个单纯的Java类。
上面4个持久化类之间的继承关系是,Person派生了Employee和Customer,而Employee又派生了Manager。
上面4个实体之间的关联关系是,Employee和Manager之间存在双向N-1关联,Employee和Customer之间存在双向的1-N关联。
下图显示了这4个实体间的继承和关联关系,
下面分别给出上面提到的三种继承策略的实现方式,
首先给出Address类,是个通用普遍类,在三种策略中都会使用到。
Address类,仅仅是个普通Java类,
1 package inh; 2 3 public class Address { 4 private String detail; 5 private String zip; 6 private String country; 7 public Address() {} 8 public Address(String country, String detail, String zip) { 9 this.detail = detail; 10 this.zip = zip; 11 this.country = country; 12 } 13 public String getDetail() { 14 return detail; 15 } 16 public void setDetail(String detail) { 17 this.detail = detail; 18 } 19 public String getZip() { 20 return zip; 21 } 22 public void setZip(String zip) { 23 this.zip = zip; 24 } 25 public String getCountry() { 26 return country; 27 } 28 public void setCountry(String country) { 29 this.country = country; 30 } 31 32 }
一、整个类层次对应一个表的映射策略
这是Hibernate的默认继承映射测率,以上4个实体都被保存在同一张表中,这张表的所有数据列的总和就是所有实体属性的总和。
在这种继承映射策略下,如何区分各个实体呢?Hibernate的解决方案就是为表格专门增加一列,用来区分当前这一行数据属于哪个实体的实例,这个列被称为辨别者列(discriminator)
在根类上,需要使用@DiscriminatorColumn来配置辨别者列,需要配置列名,类型。在根类和子类中都要使用@DiscriminatorValue来指定当前类在辨别者列中的值,用来区分其他实体类。
下面给出根类Person实体类的实现,
1 package inh; 2 3 import javax.persistence.*; 4 5 @Entity 6 @DiscriminatorColumn(name="person_type", discriminatorType=DiscriminatorType.STRING) 7 @DiscriminatorValue("普通人") 8 @Table(name="person_inf") 9 public class Person { 10 @Id @Column(name="person_id") 11 @GeneratedValue(strategy=GenerationType.IDENTITY) 12 private Integer id; 13 private String name; 14 private String gender; 15 @Embedded 16 @AttributeOverrides({ 17 @AttributeOverride(name="detail", column=@Column(name="address_detail")), 18 @AttributeOverride(name="zip", column=@Column(name="address_zip")), 19 @AttributeOverride(name="country", column=@Column(name="address_country")) 20 }) 21 private Address address; 22 public Person() {} 23 public Person(String name, String gender, Address address) { 24 this.name = name; 25 this.gender = gender; 26 this.address = address; 27 } 28 public Integer getId() { 29 return id; 30 } 31 public void setId(Integer id) { 32 this.id = id; 33 } 34 public String getName() { 35 return name; 36 } 37 public void setName(String name) { 38 this.name = name; 39 } 40 public String getGender() { 41 return gender; 42 } 43 public void setGender(String gender) { 44 this.gender = gender; 45 } 46 public Address getAddress() { 47 return address; 48 } 49 public void setAddress(Address address) { 50 this.address = address; 51 } 52 53 }
加粗部分就是辨别者类的配置,其中第一行加粗的只会在根类中配置,第二行加粗的则需要在每个类(包括根类)中配置,下面是Employee类的实现,
1 package inh; 2 3 import java.util.HashSet; 4 import java.util.Set; 5 6 import javax.persistence.*; 7 8 @Entity 9 @DiscriminatorValue("员工") 10 public class Employee extends Person { 11 private String title; 12 private double salary; 13 @OneToMany(cascade=CascadeType.ALL, mappedBy="employee", targetEntity=Customer.class) 14 private Set<Customer> customers = new HashSet<>(); 15 @ManyToOne(cascade=CascadeType.ALL, targetEntity=Manager.class) 16 @JoinColumn(name="manager_id", nullable=true) 17 private Manager manager; 18 public Employee() {} 19 public Employee(String title, double salary) { 20 this.title = title; 21 this.salary = salary; 22 } 23 public String getTitle() { 24 return title; 25 } 26 public void setTitle(String title) { 27 this.title = title; 28 } 29 public double getSalary() { 30 return salary; 31 } 32 public void setSalary(double salary) { 33 this.salary = salary; 34 } 35 public Set<Customer> getCustomers() { 36 return customers; 37 } 38 public void setCustomers(Set<Customer> customers) { 39 this.customers = customers; 40 } 41 public Manager getManager() { 42 return manager; 43 } 44 public void setManager(Manager manager) { 45 this.manager = manager; 46 } 47 48 }
Employee需要同时与Customer和Manager关联,与Customer是双向1-N关联,因此用@OneToMany注解,同时可以配置级联更新且不控制关联关系;与Manager是双向N-1关联,因此用@ManyToOne注解,并且作为Manger的从表,在Employee中配置好外键。
由于这种情况只有一个数据表,所以在子类中不需要指定@Table注解,只需要让子类继承即可,同时配置好辨别者列的值。
下面是Manager实体类的代码,
1 package inh; 2 3 import java.util.HashSet; 4 import java.util.Set; 5 6 import javax.persistence.*; 7 8 @Entity 9 @DiscriminatorValue("经理") 10 public class Manager extends Employee { 11 private String department; 12 @OneToMany(cascade=CascadeType.ALL, mappedBy="manager", targetEntity=Employee.class) 13 private Set<Employee> employees = new HashSet<>(); 14 public Manager() {} 15 public Manager(String department) { 16 this.department = department; 17 } 18 public String getDepartment() { 19 return department; 20 } 21 public void setDepartment(String department) { 22 this.department = department; 23 } 24 public Set<Employee> getEmployees() { 25 return employees; 26 } 27 public void setEmployees(Set<Employee> employees) { 28 this.employees = employees; 29 } 30 31 }
Manager与Employee成双向1-N关联,因此用@OneToMany注解表示,并设置好级联更新,且不控制关系。
下面是Customer的代码,
1 package inh; 2 3 import javax.persistence.*; 4 5 @Entity 6 @DiscriminatorValue("顾客") 7 public class Customer extends Person { 8 private String comments; 9 @ManyToOne(targetEntity=Employee.class) 10 @JoinColumn(name="employee_id", nullable=true) 11 private Employee employee; 12 public Customer() {} 13 public Customer(String comments) { 14 this.comments = comments; 15 } 16 public Employee getEmployee() { 17 return employee; 18 } 19 public void setEmployee(Employee employee) { 20 this.employee = employee; 21 } 22 public String getComments() { 23 return comments; 24 } 25 public void setComments(String comments) { 26 this.comments = comments; 27 } 28 }
Customer与Employee成双向N-1关联,因此用@ManyToOne注解,并作为从表使用外键关联Employee。
下面是一个测试类,
1 package inh; 2 3 import java.util.Date; 4 import java.util.HashSet; 5 import java.util.Set; 6 7 import map.six11.Order; 8 import map.six11.OrderItem; 9 import map.six11.Product; 10 11 import org.hibernate.Session; 12 import org.hibernate.SessionFactory; 13 import org.hibernate.Transaction; 14 import org.hibernate.cfg.Configuration; 15 16 public class Test { 17 public static void doTest1() { 18 Configuration conf = new Configuration().configure(); 19 //conf.addClass(Person.class); 20 conf.addAnnotatedClass(Person.class); 21 conf.addAnnotatedClass(Customer.class); 22 conf.addAnnotatedClass(Employee.class); 23 conf.addAnnotatedClass(Manager.class); 24 SessionFactory sf = conf.buildSessionFactory(); 25 Session sess = sf.openSession(); 26 Transaction tx = sess.beginTransaction(); 27 28 Address a = new Address("中国","广州","111111"); 29 Person p = new Person("张三","男", a); 30 sess.save(p); 31 32 Manager m = new Manager("研发部"); 33 /* 34 * Manager与Employee成双向N-1关联,因为在1的一端(Manager)设置了不控制关系(mappedBy="manager") 35 * 因此不使用Manger实例主动关联Employee实例,而是由Employee实例主动关联Manager实例 36 */ 37 //m.getEmployees().add(e1); 38 //m.getEmployees().add(e2); 39 m.setName("魏七"); 40 m.setGender("女"); 41 m.setAddress(a); 42 m.setTitle("CEO"); 43 m.setSalary(500000); 44 m.setDepartment("总裁部"); 45 //Employee中设置了级联更新,因此这里不需要显示持久化Manager实体 46 //sess.save(m); 47 48 Employee e1 = new Employee("软件开发",20000); 49 e1.setName("王五"); 50 e1.setGender("女"); 51 e1.setAddress(new Address("法国","巴黎","333333")); 52 e1.setManager(m); 53 /* 54 * Employee与Customer成双向1-N关联,因为在1的一端(Employee)设置了不控制关系(mappedBy="employee") 55 * 因此不使用Employee实例主动关联Customer实例,而是由Customer实例主动关联Employee实例 56 */ 57 //e1.getCustomers().add(c); 58 59 Employee e2 = new Employee("项目经理",30000); 60 e2.setName("向六"); 61 e2.setGender("男"); 62 e2.setAddress(a); 63 //Employee实例作为N的一端主动关联Manager实例 64 e2.setManager(m); 65 //e1将被下面的Cusotmer实例级联更新,因此不需要在这里显示持久化 66 //sess.save(e1); 67 sess.save(e2); 68 69 Customer c = new Customer("喜欢购物"); 70 c.setName("李四"); 71 c.setGender("女"); 72 c.setAddress(new Address("美国","纽约","222222")); 73 //Customer实例作为N的一端主动关联Employee实例 74 c.setEmployee(e1); 75 sess.save(c); 76 77 tx.commit(); 78 sess.close(); 79 sf.close(); 80 } 81 82 public static void main(String[] args) { 83 doTest1(); 84 } 85 }
上面的测试类有几个注意的地方,
- mappedBy关系控制。例如在第37,38,及54行都被注释了,取而代之的是把关联控制放在了N的一端(即第49,61,和70行)。
如果在删除Employee.java第13行的mappedBy,即Employee和Customer的关系由1的一方(Employee)来控制,这会发生什么事呢?经过测试后发现,这种情况下Hibernate又生成了另外一张表叫做person_inf_person_inf,专门用来管理Employee和Customer的关联关系,这显然不是Hibernate推荐的方式。
- cascade级联更新。例如上面的43行和63行都注释掉了,但是最终数据表中依然会插入这两条数据,因为被关联的实体类级联更新了。
Hibernate执行的SQL日志,
Hibernate: insert into person_inf (address_country, address_detail, address_zip, gender, name, person_type) values (?, ?, ?, ?, ?, '普通人') Hibernate: insert into person_inf (address_country, address_detail, address_zip, gender, name, manager_id, salary, title, department, person_type) values (?, ?, ?, ?, ?, ?, ?, ?, ?, '经理') Hibernate: insert into person_inf (address_country, address_detail, address_zip, gender, name, manager_id, salary, title, person_type) values (?, ?, ?, ?, ?, ?, ?, ?, '员工') Hibernate: insert into person_inf (address_country, address_detail, address_zip, gender, name, manager_id, salary, title, person_type) values (?, ?, ?, ?, ?, ?, ?, ?, '员工') Hibernate: insert into person_inf (address_country, address_detail, address_zip, gender, name, comments, employee_id, person_type) values (?, ?, ?, ?, ?, ?, ?, '顾客')
执行测试类,生成person_inf表如下,
表数据,
表字段
执行测试类,生成一张表person_inf,发现一共有13个字段,分别是,
Person实体中, person_id , name, gender 以及Address组件中的 address_detail , address_zip , address_country 共6个字段。
Employee实体中, title,salary及外键manager_id共3个字段,
Manager实体中,department,共1个字段
Customer实体中, comments,及外键employee_id共2个字段,
再加上辨别者字段person_type,所以总数就是6+3+1+2+1 = 13个字段。
表关联
mysql中person_inf表内部的关联如下,可见manager_id与employee_id都同时与person_id关联
从Hibernate创建外键约束的日志也可以看到,将manager_id与person_id,以及employee_id与person_id进行了外键关联,
Hibernate: alter table person_inf add constraint FK_jhxtklndcclgtvht206hmdgwx foreign key (employee_id) references person_inf (person_id) Hibernate: alter table person_inf add constraint FK_s4pegdx1wxrglarcckeqvs062 foreign key (manager_id) references person_inf (person_id)
在Hibernate中,我们将Employee实体与Manager实体通过manager_id关联,在数据库中,manager_id又与person_id关联,因此Employee对应的manager_id值,其实就是person_id值,
同理,在Hibernate中,我们将Customer实体与Employee通过empolyee_id关联,在数据库中,employee_id又与person_id关联,因此Customer对应的employee_id值,其实就是person_id值,
这种继承策略的优点是所有字段都放在同一个表中,因此对每一个实体查询都只需要在单张表查询就行了,但是这种策略还有个劣势就是不允许非空约束。
非空约束
这种单表映射所有继承实体的策略,有一个局限性,就是无法设置非空约束。
从上面的数据表可以看到有很多NULL值,这是因为父类相对于子类来说,根本没有这个字段,但是Hibernate强行将它们放在同一个表中,那么对应的不存在的字段只能是NULL了,因此也就不允许将这个字段设置非空, 因为对与父类来说,这个字段根本不存在。
连接子类的映射策略
这种策略更像是面向对象的继承关系,父类的属性放在父类表中,子类的新的属性放在子类表中,父类表+子类表则是子类的完整属性,这就是连接子类的映射策略。
使用这种策略时,需要在根类上使用@Inheritance注解,要设置注解中的strategy属性,有三种值可选
InheritanceType.SINGLE_TABLE
InheritanceType.JOINED
InheritanceType.TABLE_PER_CLASS
其中第二个值就是我们要选的值。
下面是实现代码,这里仅给出每个类的开头几行,因为后面部分不需要做改变,
Person实体类作为所有类的根类,需要@Inheritance注解,其他子类则不需要,仅指定表名即可,
1 @Entity 2 @Inheritance(strategy=InheritanceType.JOINED) 3 @Table(name="person_inf") 4 public class Person { 5 ...
Employee实体类,
1 @Entity 2 @Table(name="employee_inf") 3 public class Employee extends Person {
Manager实体类,
1 @Entity 2 @Table(name="manager_inf") 3 public class Manager extends Employee {
Customer实体类,
1 @Entity 2 @Table(name="customer_inf") 3 public class Customer extends Person {
测试类不需要做任何修改,执行测试类,得到4个表,表中数据如下,
MariaDB [test]> select * from person_inf; +-----------+-----------------+----------------+-------------+--------+------+ | person_id | address_country | address_detail | address_zip | gender | name | +-----------+-----------------+----------------+-------------+--------+------+ | 1 | 中国 | 广州 | 111111 | 男 | 张三 | | 2 | 中国 | 广州 | 111111 | 女 | 魏七 | | 3 | 中国 | 广州 | 111111 | 男 | 向六 | | 4 | 法国 | 巴黎 | 333333 | 女 | 王五 | | 5 | 美国 | 纽约 | 222222 | 女 | 李四 | +-----------+-----------------+----------------+-------------+--------+------+ 5 rows in set (0.00 sec) MariaDB [test]> select * from employee_inf; +--------+----------+-----------+------------+ | salary | title | person_id | manager_id | +--------+----------+-----------+------------+ | 500000 | CEO | 2 | NULL | | 30000 | 项目经理 | 3 | 2 | | 20000 | 软件开发 | 4 | 2 | +--------+----------+-----------+------------+ 3 rows in set (0.00 sec) MariaDB [test]> select * from manager_inf; +------------+-----------+ | department | person_id | +------------+-----------+ | 总裁部 | 2 | +------------+-----------+ 1 row in set (0.00 sec) MariaDB [test]> select * from customer_inf; +----------+-----------+-------------+ | comments | person_id | employee_id | +----------+-----------+-------------+ | 喜欢购物 | 5 | 4 | +----------+-----------+-------------+ 1 row in set (0.00 sec)
表关联
Hibernate生成了以下关联约束,
Hibernate: alter table customer_inf add constraint FK_rvtbr9yxt4vdyxguao23xsyrk foreign key (employee_id) references employee_inf (person_id) Hibernate: alter table customer_inf add constraint FK_m0r5gi2t5o2uxpircjftmy0ot foreign key (person_id) references person_inf (person_id) Hibernate: alter table employee_inf add constraint FK_mao9jvcblb8qlcpyxa3n7av9p foreign key (manager_id) references manager_inf (person_id) Hibernate: alter table employee_inf add constraint FK_92r30s2ul49wv9wsk2b3qau9c foreign key (person_id) references person_inf (person_id) Hibernate: alter table manager_inf add constraint FK_lfvhjsli64txae1hq3lyq57g9 foreign key (person_id) references employee_inf (person_id)
可以看到每个表除了在实体类中通过JPA注解定义的表关联之外,Hibernate还将每个子类都与根类Person进行了关联,这种策略下,子表只存储了自身定义的属性,继承来的属性则依然保持在父表中,如果需要查询一个实体的所有属性,则需要进行表关联查询之后再连接接起来。在mysql中这几个表的关系图如下,
从mysql数据表关系图上来看,hibernate的这种继承映射策略更像是面向对象的继承,对于这种继承策略的查询,在数据库底层会涉及到多多表关联,根据子类层次的多少,要关联多少个数据表不一定,所以性能可能会有所影响。
每个具体类对应一个表的映射策略
这种映射策略与第二种策略比起来,其实就是将父类的属性也加入到子类表中,使得每个实体类本身定义的好属性以及从父类继承来的属性全都放在一个表中,即具体类对应具体表,无需表关联查询。
要启用这种映射策略,也需要在根类使用@Inheritance注解,并且指定属性strategy=InheritanceType.TABLE_PER_CLASS。
但是在这种继承映射下,不能指定数据表的主键为自增(GnertationType.IDENTITY),而需要使用别的如hilo生成策略。
下面是实现代码,仅仅需要修改Person实体类名注解以及主键生成策略,其他部分以及子类都不需要修改,
1 @Entity 2 @Inheritance(strategy=InheritanceType.TABLE_PER_CLASS) 3 @Table(name="person_inf") 4 public class Person { 5 @Id @Column(name="person_id") 6 @GenericGenerator(name="person_hilo", strategy="hilo") 7 @GeneratedValue(generator="person_hilo") 8 private Integer id;
执行测试类,生成4张表(另外一张hibernate_unique_key是hilo主键生成器的表,与业务无关),
MariaDB [test]> select * from person_inf; +-----------+-----------------+----------------+-------------+--------+------+ | person_id | address_country | address_detail | address_zip | gender | name | +-----------+-----------------+----------------+-------------+--------+------+ | 1 | 中国 | 广州 | 111111 | 男 | 张三 | +-----------+-----------------+----------------+-------------+--------+------+ 1 row in set (0.00 sec) MariaDB [test]> select * from employee_inf; +-----------+-----------------+----------------+-------------+--------+------+--------+----------+------------+ | person_id | address_country | address_detail | address_zip | gender | name | salary | title | manager_id | +-----------+-----------------+----------------+-------------+--------+------+--------+----------+------------+ | 2 | 中国 | 广州 | 111111 | 男 | 向六 | 30000 | 项目经理 | 3 | | 5 | 法国 | 巴黎 | 333333 | 女 | 王五 | 20000 | 软件开发 | 3 | +-----------+-----------------+----------------+-------------+--------+------+--------+----------+------------+ 2 rows in set (0.00 sec) MariaDB [test]> select * from manager_inf; +-----------+-----------------+----------------+-------------+--------+------+--------+-------+------------+------------+ | person_id | address_country | address_detail | address_zip | gender | name | salary | title | manager_id | department | +-----------+-----------------+----------------+-------------+--------+------+--------+-------+------------+------------+ | 3 | 中国 | 广州 | 111111 | 女 | 魏七 | 500000 | CEO | NULL | 总裁部 | +-----------+-----------------+----------------+-------------+--------+------+--------+-------+------------+------------+ 1 row in set (0.00 sec) MariaDB [test]> select * from customer_inf; +-----------+-----------------+----------------+-------------+--------+------+----------+-------------+ | person_id | address_country | address_detail | address_zip | gender | name | comments | employee_id | +-----------+-----------------+----------------+-------------+--------+------+----------+-------------+ | 4 | 美国 | 纽约 | 222222 | 女 | 李四 | 喜欢购物 | 5 | +-----------+-----------------+----------------+-------------+--------+------+----------+-------------+ 1 row in set (0.00 sec)
观察前3个表会发现,由于manager, employee, person 在业务上就是存在继承关系的,在数据库表上也成继承关系,employee包含了person表所有字段并新增了字段,manager表包含了employee所有字段并新增了字段,即每一个实体类对应的数据表已经包含了自身及继承来的完整的属性。
另外,由于主键生成器采用的是hilo,可以看到所有记录的主键加起来是连续的。