Hibernate 快速入门2 - 关联映射和类继承
2 关联映射
我们知道两个表A、B的映射关系有 1-1, 1-N, M-N
对于每种映射关系,还可以分为单向和双向。即单向 1-1, 其中一端不能访问另外一端。 双向1-1,两端能互访。对于其他映射关系也是一样的。
从编码的角度来说,单向的意思是,A含有类型为B的成员变量,而B没有类型为A的成员变量。双向的意思是,,A含有类型为B的成员变量, B也含有类型为A的成员变量。
下面我们来一一讲解
2.1.1 单向1-1 (A含有B,B不含有A)
对于单项1-1映射,我们用@OneToOne
来指明另一端的类,用@JoinColumn来指定外键的关联。
增加一个Professor类。假设一个Professor只教育一个学生,一个学生也只由一个Professor教育。现在我们假设只有Professor能查看学生的信息,学生不能查看Professor的信息.
Student类不变,下面是Professor类:
@Entity
public class Professor {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "professor_id")
private Integer id;
private String name;
@OneToOne(targetEntity = Student.class)
// FOREIGN KEY (student_id) REFERENCES student_info (id);
@JoinColumn(name = "student_id", referencedColumnName = "id")
private Student student;
// getters and setters
}
public static void addProfessor(Integer student_id) {
Configuration conf = new Configuration().configure();
StandardServiceRegistry registry = conf.getStandardServiceRegistryBuilder().build();
try (SessionFactory sessionFactory = conf.buildSessionFactory(registry); Session session = sessionFactory.openSession()) {
Transaction transaction = session.beginTransaction();
Professor professor = new Professor();
professor.setName("james lee");
Student student = session.get(Student.class, student_id);
professor.setStudent(student);
session.save(professor);
transaction.commit();
}
}
public static void main(String[] args) {
addProfessor(10);
}
addProfessor(10)传入10是因为student_info中有id = 10
的数据。
启动的时候,我们会发现抛异常了:
Exception in thread "main" org.hibernate.MappingException: Unknown entity: com.example.test.Professor
原来是我们忘了在hibernate.cfg.xml中添加映射类的了。
<mapping class="com.example.test.Professor"/>
添加完后成功启动,我们可以看到新建的professor表:
mysql> select * from professor;
+--------------+-----------+------------+
| professor_id | name | student_id |
+--------------+-----------+------------+
| 1 | james lee | 10 |
+--------------+-----------+------------+
1 row in set (0.00 sec)
student_info表是没有任何改变的,我这里就不贴出来了。
对Professor.student属性,可以省略@JoinColumn,hibernate会自动生成一个属性来代表Student。但是最好不要这么做。
注意,OneToOne映射,为两个类都建立了表。而在前面一节中,我们把BodyInfo作为Student的一个属性来,BodyInfo用@Embeddable标注了。结果是BodyInfo的所有字段(都是非集合字段)都合并到了Student表中。
2.1.2 双向1-1 (A、B互相包含)
首先,mysql用root登入,先把hibernatedb,删了重新建过,然后再赋予ALL权限给hibernate,向第一章那样。一定要这么做,不然不能正常插入数据。
Professor.java修改如下:
@Entity
public class Professor {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "professor_id")
private Integer id;
private String name;
@OneToOne(targetEntity = Student.class, mappedBy = "professor")
// FOREIGN KEY (student_id) REFERENCES student_info (id);
// @JoinColumn(name = "student_id", referencedColumnName = "id")
private Student student;
}
mappedBy表示,Professor放弃控制权,由Student来控制。在SQL上的表现就是,Professor表没专门为此生成的属性和外键。mappedBy = "professor",这个"professor"表示,Student类中名字为professor的属性存在。即Student.professor这个成员变量存在(其实只要保证Student.getProfessor(), Student.setProfessor(Professor)存在即可)。
Student.java修改如下:
@Entity
@Table(name = "student_info")
public class Student {
@OneToOne(targetEntity = Professor.class)
@JoinColumn(name = "professor_id", referencedColumnName = "professor_id", unique = true)
private Professor professor;
}
HibernateTest.java修改如下:
public static void biOneToOne() {
Configuration conf = new Configuration().configure();
StandardServiceRegistry registry = conf.getStandardServiceRegistryBuilder().build();
try (SessionFactory sessionFactory = conf.buildSessionFactory(registry); Session session = sessionFactory.openSession()) {
Transaction transaction = session.beginTransaction();
Professor professor = new Professor();
professor.setName("professor name one2one");
session.save(professor);
Student student = new Student();
student.setName("student name one2one");
student.setProfessor(professor);
session.save(student);
transaction.commit();
}
}
public static void main(String[] args) {
biOneToOne();
}
MySQL输出如下:
mysql> select * from student_info;
+----+-----+--------+--------+----------------------+--------------+
| id | age | height | weight | name | professor_id |
+----+-----+--------+--------+----------------------+--------------+
| 1 | 0 | NULL | NULL | student name one2one | 1 |
+----+-----+--------+--------+----------------------+--------------+
1 row in set (0.00 sec)
mysql> select * from professor;
+--------------+------------------------+
| professor_id | name |
+--------------+------------------------+
| 1 | professor name one2one |
+--------------+------------------------+
1 row in set (0.00 sec)
mysql> show create table student_info;
CREATE TABLE `student_info` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`age` int(11) NOT NULL,
`height` int(11) DEFAULT NULL,
`weight` int(11) DEFAULT NULL,
`name` varchar(255) COLLATE utf8_unicode_ci DEFAULT NULL,
`professor_id` int(11) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `UK_gg8r834cvr56est49a2tlw8an` (`professor_id`),
CONSTRAINT `FKmudc1uutqukeete7gravfalew` FOREIGN KEY (`professor_id`) REFERENCES `Professor` (`professor_id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci
mysql> show create table professor;
CREATE TABLE `professor` (
`professor_id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(255) COLLATE utf8_unicode_ci DEFAULT NULL,
PRIMARY KEY (`professor_id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci
可以看到professor上面的确没有Student的任何信息,因为它已经用mappedBy声明放弃控制。
2.1.3 单向N - 1 (A含有B,B不含有A)
比如一个教授可以教授多个学生,每个学生只能由一个教授指导。
我们当然可以让一个教授包含多个学生,就像前一章中映射集合一样。但这里,Student是一个实体(Entity),在语境上不符合作为Embeddable。因为Student完全可能和Professor的其他Entity产生关系。
为了简单起见,我们把Student的集合属性和组合属性都删掉。然后重新hibernatedb,重建。
Student.java
@Entity
@Table(name = "student_info")
public class Student {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
private String name;
private int age;
@ManyToOne(targetEntity = Professor.class)
@JoinColumn(name = "professor_id", referencedColumnName = "professor_id", nullable = false) // remove unique=true!!!
@Cascade(org.hibernate.annotations.CascadeType.ALL)
private Professor professor;
// getter and setters
}
Professor.java
@Entity
public class Professor {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "professor_id")
private Integer id;
private String name;
}
HibernateTest.java
public static void uniManyToOne() {
Configuration conf = new Configuration().configure();
StandardServiceRegistry registry = conf.getStandardServiceRegistryBuilder().build();
try (SessionFactory sessionFactory = conf.buildSessionFactory(registry); Session session = sessionFactory.openSession()) {
Transaction transaction = session.beginTransaction();
Professor professor = new Professor();
professor.setName("professor name one2one");
Student student = new Student();
student.setName("student name one2one");
student.setProfessor(professor);
Student student2 = new Student();
student2.setName("student2");;
student2.setProfessor(professor);
session.save(student2);
transaction.commit();
}
}
public static void main(String[] args) {
uniManyToOne();
}
数据库输出:
mysql> select * from professor;
+--------------+------------------------+
| professor_id | name |
+--------------+------------------------+
| 1 | professor name one2one |
+--------------+------------------------+
1 row in set (0.00 sec)
mysql> select * from student_info;
+----+-----+----------------------+--------------+
| id | age | name | professor_id |
+----+-----+----------------------+--------------+
| 1 | 0 | student name one2one | 1 |
| 2 | 0 | student2 | 1 |
+----+-----+----------------------+--------------+
2 rows in set (0.00 sec)
mysql> show create table professor;
CREATE TABLE `professor` (
`professor_id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(255) COLLATE utf8_unicode_ci DEFAULT NULL,
PRIMARY KEY (`professor_id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci
mysql> show create table student_info;
CREATE TABLE `student_info` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`age` int(11) NOT NULL,
`name` varchar(255) COLLATE utf8_unicode_ci DEFAULT NULL,
`professor_id` int(11) NOT NULL,
PRIMARY KEY (`id`),
KEY `FKmudc1uutqukeete7gravfalew` (`professor_id`),
CONSTRAINT `FKmudc1uutqukeete7gravfalew` FOREIGN KEY (`professor_id`) REFERENCES `Professor` (`professor_id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci
可以看到professor是没有任何外键的,student_info有一个外键,引用了professor.id。这就是对于关系映射N-1的情况下,我们应该设计的数据的样子。
@Cascade: 指定映射的级联。如果没有这个注解,那我们就必须先save(professor),才能save(student)。
2.1.4 单向 1 - N
hibernate不推荐控制权在1端,而是推荐控制权在N端,也就是推荐使用前面的单向N-1。
2.1.5 双向1-N (A含有元素类型为B的属性,B含有A)
双向1-N和双向N-1都是一样的。
先删除前面建的表(drop table xxx)。
双向的1-N和前面的单向N-1十分类似。只需要修改Professor类:
@Entity
public class Professor {
@OneToMany(targetEntity = Student.class, mappedBy = "professor")
private List<String> studentList;
// private Student student;
}
输出结果和上面都是一样的。这里就不重复了。
值得提一下的是:
HibernateTest.java
public static void searchProfessor(Integer id) {
Configuration conf = new Configuration().configure();
StandardServiceRegistry registry = conf.getStandardServiceRegistryBuilder().build();
try (SessionFactory sessionFactory = conf.buildSessionFactory(registry); Session session = sessionFactory.openSession()) {
Transaction transaction = session.beginTransaction();
Professor professor = session.get(Professor.class, id);
if (professor != null) {
System.out.println("student name: " + professor.getStudentList());
}
transaction.commit();
}
}
其中参数Id是数据库中的一个id。如果运行这个函数的话,能正确的输出Professor对应的student list。
2.1.6 单向N-M (A含有B)
在控制的一段添加ManyToMany
@Entity
@Table(name = "student_info")
public class Student {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
private String name;
private int age;
@ManyToMany(targetEntity = Professor.class)
@JoinColumn(name = "professor_id", referencedColumnName = "professor_id", nullable = false)
private List<Professor> professorList = new ArrayList<>();
}
@Entity
public class Professor {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "professor_id")
private Integer id;
private String name;
}
Transaction transaction = session.beginTransaction();
Professor professor = new Professor();
professor.setName("professor1");
session.save(professor);
Professor professor2 = new Professor();
professor2.setName("professor2");
session.save(professor2);
Student student = new Student();
student.setName("student name one2one");
student.getProfessorList().add(professor);
student.getProfessorList().add(professor2);
session.save(student);
Student student2 = new Student();
student2.setName("student2");
student2.getProfessorList().add(professor);
student2.getProfessorList().add(professor2);
session.save(student2);
transaction.commit();
数据库输出:
mysql> select * from student_info;
+----+-----+----------------------+
| id | age | name |
+----+-----+----------------------+
| 1 | 0 | student name one2one |
| 2 | 0 | student2 |
+----+-----+----------------------+
2 rows in set (0.01 sec)
mysql> select * from professor;
+--------------+------------+
| professor_id | name |
+--------------+------------+
| 1 | professor1 |
| 2 | professor2 |
+--------------+------------+
2 rows in set (0.00 sec)
mysql> select * from student_info_Professor;
+------------+----------------------------+
| Student_id | professorList_professor_id |
+------------+----------------------------+
| 1 | 1 |
| 1 | 2 |
| 2 | 1 |
| 2 | 2 |
+------------+----------------------------+
4 rows in set (0.00 sec)
mysql> show create table student_info_Professor;
CREATE TABLE `student_info_Professor` (
`Student_id` int(11) NOT NULL,
`professorList_professor_id` int(11) NOT NULL,
KEY `FKoboctn2nj4vqepv9lngaatdc9` (`professorList_professor_id`),
KEY `FKh6rxyclu4wmcpuqi215m6rwd9` (`Student_id`),
CONSTRAINT `FKh6rxyclu4wmcpuqi215m6rwd9` FOREIGN KEY (`Student_id`) REFERENCES `student_info` (`id`),
CONSTRAINT `FKoboctn2nj4vqepv9lngaatdc9` FOREIGN KEY (`professorList_professor_id`) REFERENCES `Professor` (`professor_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci
2.1.7 双向N-N (A含有B、B也含有A)
@Entity
public class Professor {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "professor_id")
private Integer id;
private String name;
@ManyToMany(targetEntity = Student.class)
@JoinTable(joinColumns = @JoinColumn(name = "professor_id", referencedColumnName = "professor_id"))
private List<String> studentList;
}
@Entity
@Table(name = "student_info")
public class Student {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
private String name;
private int age;
@ManyToMany(targetEntity = Professor.class)
@JoinTable(joinColumns = @JoinColumn(name = "id", referencedColumnName = "id"))
// @Column(name = "student_id")
private List<Professor> professorList = new ArrayList<>();
其他的和单向N-N一样。
@JoinTable:指定用连接表来保存映射关系(像前面的student_info_Professor)。需要注意的是,里面的@JoinColumn,为什么这里和以前的不一样,以前的JoinColumn(name, referencedColumnName)
都是不同的,而这里为什么不一样?JoinColumn的意思是,本段的name属性应用了另一个表的referencedColumnName。由于这里指定了@JoinTable,也就是指定了用一个连接表,那么另一个表指的就是连接表。我们当然应该表这个表中的name属性和连接表中的name属性映射起来。即A.a 映射到连接表AB.a, B.b映射到连接到AB.b。
3 类继承
比如我们前面的Student代表的是大学生,它在数据库中有一个对应的表。 现在新建它的一个子类PostGraduate(研究生),那么对于PostGraduate在数据库中的存在有几种呢?
对于类的继承,我们有如下几种方式:
- 子类和父类处于同一个表内(这样是hibernate默认的实现方式)。这需要修改父类的表以适应子类。
- 子类处和父类各自处于单独的表。
- 连接子类。子类新增的属性处于一个单独的表,同时修改父类的表,增加一个外键引用子类的表。
3.1 子类和父类共用一表 (Hibernate默认实现方式)
考虑一下,如果子类和父类共用一个表,那我们势必需要在表中添加一个属性,标识是父类还是子类。这个属性叫做辨别者列(discriminator),我们用@DiscriminatorColumn
注解来标识辨别者列。对于每个类在辨别者列上的取值,我们用@DiscirminatorValue
指定。
先来看看重定义的Student:
@Entity
@DiscriminatorColumn(name = "student_type", discriminatorType = DiscriminatorType.STRING)
@DiscriminatorValue("student")
@Table(name = "student_info")
public class Student {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
private String name;
private int age;
}
新定义一个PostGraduate继承Student:
@Entity
@DiscriminatorValue("post_gradudate")
public class PostGraduate extends Student {
private String finalPaper;
}
HibernateTest.java
public static void inheritance() {
Configuration conf = new Configuration().configure();
StandardServiceRegistry registry = conf.getStandardServiceRegistryBuilder().build();
try (SessionFactory sessionFactory = conf.buildSessionFactory(registry); Session session = sessionFactory.openSession()) {
Transaction transaction = session.beginTransaction();
Student student = new Student();
student.setName("student name");
student.setAge(18);
session.save(student);
Student student1 = new Student();
student1.setName("student name1");
student1.setAge(19);
session.save(student1);
PostGraduate postGraduate = new PostGraduate();
postGraduate.setAge(23);
postGraduate.setFinalPaper("introduction to linux");
session.save(postGraduate);
PostGraduate postGraduate1 = new PostGraduate();
postGraduate1.setAge(24);
postGraduate1.setFinalPaper("java io");
session.save(postGraduate1);
transaction.commit();
}
}
public static void main(String[] args) {
inheritance();
}
sql输出:
mysql> select * from student_info;
+----------------+----+-----+---------------+-----------------------+
| student_type | id | age | name | finalPaper |
+----------------+----+-----+---------------+-----------------------+
| student | 1 | 18 | student name | NULL |
| student | 2 | 19 | student name1 | NULL |
| post_gradudate | 3 | 23 | NULL | introduction to linux |
| post_gradudate | 4 | 24 | NULL | java io |
+----------------+----+-----+---------------+-----------------------+
4 rows in set (0.00 sec)
可以看到student_info
增加了PostGraduate的属性。student_type
是辨别者列,通过这个列上的属性来判断是哪个类。对于子类新增的属性,父类对应的值为NULL。
3.2 子类、父类各自处于单独的表
考虑一下,前面我们生成主键的时候,指定了@GeneratedValue(strategy = GeneratedType.IDENTITY),表示自增。在只有一张表的情况下,这是OK的。但对于我们现在描述的情况,父类子类处于不同的表,这种自增策略就不行了。此时我们要使用Hibernate自带生成策略。这里我们使用hibernate hilo
生成策略,策略的意思以及其他策略请参考文档。
同时,为了让父类、子类各自处于单独的表,我们还需要用@Inheritance
替换@Discriminator
Student.java
@Entity
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
@Table(name = "student_info")
public class Student {
@Id
@GenericGenerator(name = "student_uuid", strategy = "org.hibernate.id.UUIDGenerator")
@GeneratedValue(generator = "student_uuid")
private String id;
private String name;
private int age;
}
需要注意的是,我们把id从Integer变成了String。因为UUIDGenerator生成的主键不可能用integer保存。
PostGraudate.java
@Entity
public class PostGraduate extends Student {
private String finalPaper;
}
sql输出:
mysql> select * from PostGraduate;
+--------------------------------------+-----+------+-----------------------+
| id | age | name | finalPaper |
+--------------------------------------+-----+------+-----------------------+
| ad0f4762-8d8e-459e-a6b3-bb219d7658c7 | 24 | NULL | java io |
| e83572ce-a814-4e85-b8ba-79dd298b1240 | 23 | NULL | introduction to linux |
+--------------------------------------+-----+------+-----------------------+
2 rows in set (0.00 sec)
mysql> select * from student_info;
+--------------------------------------+-----+---------------+
| id | age | name |
+--------------------------------------+-----+---------------+
| 3c96ba4f-e719-491f-bd96-76811d832bf5 | 19 | student name1 |
| c5299329-90bf-4dbf-9085-cd366da0b407 | 18 | student name |
+--------------------------------------+-----+---------------+
可以看到,的确是在两个不同的表中。
3.3 连接子类映射策略
这种策略只需要把上面的@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
修改为@Inheritance(strategy = InheritanceType.JOINED)
这种策略由于查找的时候,要join一次,性能比前面两种低,所以这里暂时不讲解。有兴趣的可以自己跑一下看输出。