Hibernate:复杂映射:继承 / 关联 / ValueType / 自定义命名

更多
2021年10月07日 20点26分 作者:叶飞 修改

继承

为了演示,我们在Student的基础上添加了:

  • Person类:将Student的id、name、age、isMale等字段属性移到这里,作为Student和Teacher的基类
  • Teacher类:继承Person,有自己的字段fee(工资)
    @Entity
    public class Teacher extends Person {	
    	private double fee;

三种策略实现映射和存取:

Person vip = new Person();
vip.setName("vip");	

Student atai = new Student();
atai.setName("atai");

Teacher fg = new Teacher();
fg.setName("fg");
em.persist(vip);
em.persist(atai);
em.persist(fg);		
Person vip = em.find(Person.class, 1);
Student atai = em.find(Student.class, 2);
Teacher fg = em.find(Teacher.class, 3);
//运用多态
Person vip = em.find(Teacher.class, 3);

TPH

不管父类子类都注解上@Entity

演示:只生成了1个以基类命名的表Person,包含所有列,用DType列标记类型,值为类名

DType用于子类查询(父类不需要):

where
	student0_.id=? 
	and student0_.DTYPE='Student'

@想一想@:为什么?这样用Teacher的Id去查Student,就只会返回NULL,而不会崩……

演示:多态没有问题

Student s = (Student)whos;    //不会再次查询

TPC

父类上不注解@Entity而是@MappedSuperclass(没有注解会抛异常)

演示:

  • 只生成了2个子类表
  • 父类Person对象既不能存,也不能取,会报异常

TPT

父类上注解@Entity和@Inheritance(strategy = InheritanceType.JOINED)

演示:生成了3个表。父类id自增,子类id不自增

存子类对象的时候,先存父类对象,获得id,再用该id存入子类对象:

    insert 
    into
        Person
        (age, isMale, sname) 
    values
        (?, ?, ?)
2021-十月-11 09:42:54  DEBUG  
    -Natively generated identity: 5
2021-十月-11 09:42:54  DEBUG  
    -
    insert 
    into
        Student
        (created, description, enroll, rest, score, id) 
    values
        (?, ?, ?, ?, ?, ?)

取的时候,使用JOIN:

  • 父类对象
    from
    	Person person0_ 
    left outer join
    	Teacher person0_1_ 
    		on person0_.id=person0_1_.id 
    left outer join
    	Student person0_2_ 
    		on person0_.id=person0_2_.id 
    @想一想@:为什么呢?还是为了多态,为了类型转换的时候不再查询一次数据库
  • 子类对象
    from
    	Student student0_ 
    inner join
    	Person student0_1_ 
    		on student0_.id=student0_1_.id 


关联映射

我们还是用学生和老师演示。(且为了清晰,我们继承使用TPC模式)
Student atai = new Student();
atai.setName("atai");		
Student bo = new Student();
atai.setName("bo");		
Student lang = new Student();
atai.setName("lang");

Teacher fg = new Teacher();
fg.setName("fg");	
Teacher xy = new Teacher();
fg.setName("xy");	

Hibernate提供了4中注解:

@ManyToOne

顾名思义,被注解的这个字段/属性,可以有多个entity指向它,比如:

public class Student extends Person {
	@ManyToOne
	@JoinColumn(foreignKey = @ForeignKey(name = "FK_Person2Teacher"))
	private Teacher teacher;

即:可以有多个Student指向一个Teacher。

或者更简单的记成:标记成@ManyToOne的,就是外键。

我们还添加注解了@JoinColumn(连接列),目的是为了自定义外键的名称,否则就会使用无意义的“编码”……

//所有的entity都要首先persist()
em.persist(atai);
em.persist(bo);
em.persist(fg);
//建立关联关系
atai.setTeacher(fg);
bo.setTeacher(fg);
演示:查看生成的表结构和SQL语句
  • 外键关系
        alter table Student 
            add constraint FK_Person2Teacher 
            foreign key (teacher_id) 
            references Teacher (id)
  • 先insert再update(略)

@想一想@:这样是不是有点傻?

关键在于我们persist()的顺序,应该先Techer再Student呀!

//先保存Teacher,获得其id
em.persist(fg);

//这样atai指向的Teacher就是有id的
atai.setTeacher(fg);
bo.setTeacher(fg);

em.persist(atai);
em.persist(bo);

这样就只有INSERT没有UPDATE的操作了。

@OneToMany:一对多

为一对多建立双向连接,在Teacher一方添加:
public class Teacher extends Person {	
	@OneToMany(mappedBy = "taughtby")
	private Set<Student> students = new HashSet<>();
说明:
  • 关于students:
    1. 总是使用泛型
    2. 可以使用Set,因为集合中元素不会重复
    3. 声明时立即初始化,方便后续调用add()/remove()等方法,避免null值异常
  • mappedBy(被xxx字段映射):
    1. 必须指定,否则会再额外生出一张关系表出来
    2. 值为另一端关联它的字段名:为了和类名区分,改成taughtBy
      private Teacher taughtBy;

演示:仍然是一个外键关系

@OneToMany:不是多对多

当@OneToMany没有mappedBy参数:
@OneToMany
private Set<Student> students = new HashSet<>();

演示:额外生出一张额外的表Teacher_Student

@想一想@:这是不是就是多对多?

仔细观察,发现这张表最大的问题就是它在studentsId上面加了唯一约束

alter table Teacher_Student 
        add constraint UK_aeood6sxmnikrc6y1d80qlemr unique (students_id)
这显然是和多对多的关系表要求相违背的。

@ManyToMany

这才是多对多正确的做法。

还配合@JoinTable指定外键名称:

public class Teacher extends Person {		
	@ManyToMany
	@JoinTable(name = "Teacher2Student",    //指定表名
		joinColumns = @JoinColumn(name="teacher",foreignKey = @ForeignKey(name="FK_TeacherId")),
		inverseJoinColumns = @JoinColumn(name="student", foreignKey = @ForeignKey(name="FK_StudentId")))
	private Set<Student> students = new HashSet<>();

说明(简单理解):

  • joinColumns指代当前entity所代表的那一列
  • inverseJoinColumns指代另一端所代表的那一列

演示:单向多对多的表结构和存储

使用add()方法添加关系:

fg.getStudents().add(atai);
fg.getStudents().add(bo);

可以单向,也可以双向:

@ManyToMany(mappedBy = "students")    //students是字段名
private Set<Teacher> teachers = new HashSet<>();

但是要注意:

  1. 一样不要忘了mappedBy,否则就会额外生出一个关系表。
  2. 仅仅是在inverse的这一端调用add()/remove()方法添加关联是没有用的(按文档是应该双向添加,实践是在owner的一方即可)。
    //lang是Student,非owner的一方,mappedBy的一方
    //所以不会被持久化
    lang.getTeachers().add(fg);	

@OneToOne

单向的一对一:

@OneToOne(optional = false)    
private Teacher taughtBy;

这样就会建立一个taughtBy的外键列,一样可以使用@ForeignKey指定外键名称等……(略)

和@ManyToOne不同的地方是使用了UNIQUE约束

alter table Student 
        add constraint UK_ptebl24wokufgwsgxuoogye5m unique (taughtBy_id)

另:optional指定映射时是否允许该外键列为NULL。(默认为true,可以为NULL)

演示:建立关系

lang.setTeacher(fg);

还可以双向:

@OneToOne(mappedBy = "taughtBy")
private Student student;
  • 一样不要忘了mappedBy
  • 只有owner端添加关联关系才有效


ValueType

以前在Hibernate里被称之为components,JPA中被称之为value type。典型的特征:没有Id。所以需要嵌在其他entity中使用,由其父entity导出

@Embeddable:定义类

@Embeddable
public class Contact {

@Embedded:标记字段

@Embedded
private Contact contact;

演示:Contact映射成一张表,而是“嵌入”到Student表中……

@想一想@:ValueType也可能/可以有一对多么?


自定义命名

Hibernate默认类名对表名,字段/属性名对列名,关系表用下划线连接……

我们也可以使用annotation自定义,但除此以外还有:

@AttributeOverride

比如我们使用TPC继承的时候,在父类中定义的字段,想在不同的子类中映射成不同的列名:

//注意这里的id字段,不是定义在Student而是Person中的
@AttributeOverride(name="id", column = @Column(name="sid"))
public class Student extends Person {


还有当一个entity使用多个embeded的时候,JPA也要求予以区分,需要使用到这个……(略)

命名策略

本质上Hibernate通过NamingStrategy(命名策略)来确定映射生成的表名列名等。

其命名策略又可以分为:

  • Implicit的:当开发人员没有配置/注解时,Hibernate采用的规则
  • Physical的:无论是否有开发人员的配置/注解,都会适用的规则
演示:这里面又出现了Hibernate native和JPA的区别,当前版本的和历史(legacy)版本等的差异

我们这只就ImplicitNamingStrategy的实现做一个介绍。

首先声明一个ImplicitNamingStrategy类(但通常我们不会直接实现ImplicitNamingStrategy,而是继承它的实现类):

public class YQBImplicitNamingStrategy extends ImplicitNamingStrategyJpaCompliantImpl

并(在persistence.xml中)告诉Hibernate使用这个策略:

<property name="hibernate.implicit_naming_strategy" value="YQBImplicitNamingStrategy"/>

然后通过@Override实现自己的命名策略,比如我们来修改外键名称的生成规则:

@Override
public Identifier determineForeignKeyName(ImplicitForeignKeyNameSource source) {
	//toIdentifier()参考基类获得
	return toIdentifier(String.join(
			"_", /* 因为mysql的语法要求,不要使用短横线 */
			"FK", 
			source.getTableName().getText(),
			//为了简便,假设外键列只有一列
			source.getColumnNames().get(0).getText(), 
			source.getReferencedTableName().getText()),
			source.getBuildingContext());
}
生成的外键名:
add constraint FK_Student_taughtBy_id_Teacher



作业

  1. 完成ORM介绍中第4题
  2. 除了课堂上的@ManyToMany,还有没有其他的多对多映射实现?(肯定有的,实现它!^_^)想一想这样实现的好处。
  3. 使用NamingStrategy,为所有映射生成的
    1. 表名加前缀T_,比如:T_User、T_Problem
    2. Id加表名,比如:UserId、ProblemId


Hibernate 关联 继承
赞: 0 踩: 0

打赏
已收到打赏的 帮帮币

你的 打赏 非常重要!
为了保证文章的质量,每一篇文章的发布,都已经消耗了作者 1 枚 帮帮币
没有“帮帮币”,作者无法发布新的文章。

全系列阅读
评论 / 0

持久化


ADO&EF

如何通过C#进行数据库的读取,包含ADO.NET和Entity Framework相关知识……

JDBC&Hibernate

Java连接数据库操作,包括JDBC、Hibernate和mybatis等

全部
关键字



帮助

反馈