关系总体分为三种:一对多,多对多,一对一.在配置映射文件时, 需要选择与保存对象相应的集合标签.
在一对多的关系中,往往只需要一方维护两者的关系,因为在关系型数据库中,只需要有一方存储关系就行了,称为外键,外键列保存在多的一方即可,对于对象来说,双方都设置关联才比较好,这样双方才都知道与对方关联,但是对于数据库来说,只需要一方设置有关联就可以了,不管哪一方,只进行设置,默认就会生成一条update语句来更新外键的值,以保存关联关系(维护关联关系),如果关联一个没有保存的对象(有效的关联),会抛出一个TransientObjectException异常.不管在哪一方都能找到另一方,称为双向关联,单项关联就是指仅仅从某一方才能找到另一方,而另一方不能找到某一方,因为在另一方的数据库中没有维护关系,自然就找不到.关联关系中重要的属性:
<!-- |
不用指定表名,因为根据类,可以自动找到映射文件,映射文件里有描述表的结构 |
cascade="all":将一个关联关系(无论是对值对象的关联,或者对一个集合的关联) |
标记为父/子关系的关联。 |
这样对父对象进行 save/update/delete 操作就会导致子对象也进行 save/update/delete 操作 lazy:默认值为true,开启懒加载.属性interests是set集合的引用,这个集合里面的对象存在数据库中,是person对象的子对象, 如果开启懒加载,且没有在session关闭之前访问interests的成员,则关闭session之后就获取不到了,访问将抛LazyInitializationException异常. |
--> |
<set name="interests" cascade="all"> |
<key column="personID"></key> |
<one-to-many class="..domain.Interest"/> |
</set> |
多的一方:
<!-- |
很贴切的标签 |
name:指定对应的属性 |
column:指明外键列的列名 |
--> |
<many-to-one class="..domain.Person" name="person" column="personID"></many-to-one> |
假设我有一个员工表,和一个部门表,我有15个员工和5个部门,它们的关系在数据库中表示为员工表的外键列,在session开启之后获取到部门的持久对象,也将获取和它关联的员工,在session关闭之前所做的任何修改都将直接同步到数据库中.如果要解除两者的关联关系:
// 解除关联关系 |
@Test |
public void testRemoveRelation() { |
Session session = sessionFactory.openSession(); |
Transaction tx = session.beginTransaction(); |
// -------------------------------------------------------------- |
// Employee employee = (Employee) session.get(Employee.class, 15); |
// employee.setDepartment(null); |
Department department = (Department) session.get(Department.class, 5); |
// department.getEmployees().remove(obj); // 与指定的员工解除关联关系 |
// department.getEmployees().clear(); // 与关联的所有员工解除关联关系 |
// department.setEmployees(null); // 与关联的所有员工解除关联关系 |
department.setEmployees(new HashSet<Employee>()); // 与关联的所有员工解除关联关系 |
// -------------------------------------------------------------- |
tx.commit(); |
session.close(); |
} |
删除部门时,如果有和它关联的员工,则可能会有约束异常,必须先把与员工的关系解除或者连员工一起删除,需要设置cascade属性,然后才能进行:
@Test |
public void testDelete() { |
Session session = sessionFactory.openSession(); |
Transaction tx = session.beginTransaction(); |
// -------------------------------------------------------------- |
/** 删除部门时,如果有员工引用这个部门的id,则: |
a,如果inverse=true,即不能维护关联关系,就会 抛异常: Cannot delete or update a parent row: a foreign key constraint fails |
b,如果inverse=false,即可以维护关联关系,则Hibernate会先取消与所有员工的关联,再删除部门,不会有异常。 |
先获取持久对象,用get()方法或者load()方法,或者自己模拟一下: |
Department department = new Department(); |
department.setId(4); |
只要OID对应即可. |
*/ |
Department department = (Department) session.get(Department.class, 4); |
session.delete(department); |
//department.getEmployees().clear(); // 解除与所有关联员工的关联关系 |
// -------------------------------------------------------------- |
tx.commit(); |
session.close(); |
} |
多对多的关联关系比多对一的关联关系更好理解一些,通常在数据库中有张中间表来描述:
在Hibernate中默认双方都能维护关联关系,但双方都设置关联关系会有异常:java.sql.BatchUpdateException.因为中间表通常以两个外键作为联合主键,在程序中,由于是多对多的关系,双方维护的对象都是一样的,比如:
从任何一方都能关联,任何一方都清楚的展示了对方的属性,任何一方的对象里面都维护着另一方的对象(双向关联),这样一来,在持久化对象的时候,就会在中间表中出现重复记录,当然是不允许的.所以一般做法为:只关联一方或者在某一方设置inverse=”true”即可.多对多是关联关系里面用的最多的:
<!-- 多对多映射(employees : Set<Employee> 属性) |
<key column="..."/>是配置集合外键,即引用当前对象表的主键的那个外键 |
inverse="true"表示放弃维护关联关系(由对方维护)。在多对多中,维护关联关系是指的在中间表中插入一条数据 |
--> |
<set name="employees" table="emmployee_role" inverse="true"> |
<key column="roleId"></key> |
<many-to-many class="Employee" column="employeeId"></many-to-many> |
</set> |
一对一.一对一虽然不难,理解起来比多对多稍微麻烦一点,假设一个部门只能有一个员工,一个员工也只能有一个部门.一对一有两种映射方式:
基于主键(推荐)
这是一种万能方式,因为employee表中的主键是id,属于代理主键,如果开发部被删除了,zhang将不会被删除,因为department_id可能设置允许为null.
有外键的表的映射文件(employee):
<!-- |
这里用到了多对一的标签,但是这里并不是指的多对一,从unique属性就能看出来这一列的记录不能重复,那 |
为什么要用这个标签而不是one-to-one呢?因为这张表需要一个保存其他表数据的外键列,而one-to-one |
标签不会生成一个外键列,所以需要用到many-to-one标签,并且给它设置unique属性让它唯一,因而也可 |
以认为一对一只是多对多的一种特例.给它加上cascade属性是为了更方便的管理对象,如果子对象没有持久 |
化,也自动给它持久化 |
--> |
<many-to-one cascade="all" |
unique="true" name="department" |
class="..domain.Department" |
column="departmentID"> |
</many-to-one> |
无外键表的映射文件(department):
<!-- |
表中不需要维护两个相同的记录,需要获取时一并获取即可,无外键方,使用<one-to-one> |
property-ref:默认与被关联的实体的主键相关联,有了property-ref属性, |
就可以通过它与被指定的实体主键以外的字段相关联 |
--> |
<one-to-one name="employee" property-ref="department" class="..domain.Employee"></one-to-one> |
基于外键
在这种方式下,employee表中的id既当主键又当外键,如果开发部被删除了,zhang也要被删除,因为主键不能为null,与其他对象有多个一对一关系时,如果外键设计在当前表中,则最多只能有一个基于主键的一对一映射.有外键方的对象需要独立存在而不与对方的某一条数据关联时,不能使用基于主键的一对一映射.除非保证employee表中的id始终有值对应.除了这些以外,还需要在两者之间寻找主从关系,比如人是必须存在的,身份证需要依赖人,人就是主,身份证就是从.
主:
<!-- |
只需要描述自己的idCard属性所对应的对象即可 Hibernate会自动找到对应的映射文件 |
--> |
<one-to-one name="idCard" class="..domain.IdCard"></one-to-one> |
从:
<!-- |
因为自己的主键也是外键,一张表中可以存在多个外键, 所以必须说明自己的主键是参照哪个外键 |
--> |
<id name="id"> |
<generator class="foreign"> |
<param name="property">person</param> |
</generator> |
</id> . . . |
<!-- |
constrained:是否加上外键约束,默认值为false |
--> |
<one-to-one name="person" class="..domain.Person" constrained="true"> |
</one-to-one> |
不管关联关系是什么,不是谁都能维护关联关系,关于谁才能维护关联关系:
一对多:
默认双方都能维护
在一方可以通过设置inverse="true"来放弃维护关联关
在多方(有外键方)始终能维护关联关系,因为外键在自己表中
多对多:
默认双方都能维护
在任何一方都可以通过设置inverse="true"来放弃维护关联关系。注意只需设置一边就可以
一对一:
不管是采基于外键的还是基于主键的一对一映射,都是只有有外键方可以维护关联关系,没有外键方不可以维护。而且不可以配置
当采用基于主键的一对一映射时,双方都不能解除关联关系,因为主键值为能为null
单向关联与上述的一致,能维护关系的一方才可以做单向关联到对方.
映射继承结构的时候,一般是整个继承结构使用一个映射文件,名称前缀为超类的名,继承映射有三种方式,全部以论坛的帖子为例:
方式一:
一个继承结构只有一张表,同时只有一个映射文件,那它的子类怎么办?用<subclass>标签配置它的子类.另外分析下,因为是一张表,表中要有所有的字段,但是主题的帖子肯定不需要floor(楼层),只有回复表才需要标注自己是几楼,同理,主题的extra(如精华、置顶...)也是主题表特有的.那么如何判断帖子是什么类型呢?判断帖子有extra,则是主题,有floor,则是回复等,所有可能情况如下:
前两条记录还好说,第三条怎么算呢?可能是一个没有extra的主贴,也有可能是一个没有标注楼层的回复,总之,从设计表的结构上就要把这种可能性排除,所以采用中表中加入一个type来指定具体的类型.那么,如果把类型在添加记录的时候就加入进来呢?不同类型的对象添加的时候自然就是不一样的值,比如主题就是type=topic,回复就是type=reply.如下:
<hibernate-mapping package="..mapping"> |
<!-- 一个继承结构一张表的映射方式: |
需要有一个额外的列用于保存类型标志。 |
每个类都需要指定discriminator-value属性,表示代表当前类型的标志。如果没有指定,默认为当前类的全限定名。 |
父类与子类只需要映射自己的属性就可以了。 |
--> |
<class name="Article" table="Article" discriminator-value="Article"> |
<id name="id"> |
<generator class="identity"></generator> |
</id> |
<!-- 指定用于辨别是什么类型标志列 必须放在property标签之前--> |
<discriminator column="type_type="string"></discriminator> |
<property name="title"></property> |
<property name="content"></property> |
<!-- 子类:Topic --> |
<subclass name="Topic" discriminator-value="Topic"> |
<property name="extra"></property> |
</subclass> |
<!-- 子类:Reply --> |
<subclass name="Reply" > |
<property name="floor"></property> |
</subclass> |
</class> |
</hibernate-mapping> |
方式二:
每个类一张表,抽象类也有表(<joined-subclass>),子类表与父类表是一对一的关系,在映射时,每个类都只映射自己特有的属性,公共的属性值存放在父类表中,父类表的主键和子类表的主键相关联,不相关的子类表,不添加记录:
<hibernate-mapping package="..mapping"> |
<!-- 每个类一张表 |
子类表与父类表是一对一的关系 |
在映射时,每个类都只映射自己特有的属性 |
--> |
<class name="Article" table="Article"> |
<id name="id"> |
<generator class="identity"></generator> |
</id> |
<property name="title"></property> |
<property name="content"></property> |
<!-- 子类:Topic |
需要指定表名。 |
需要指定<key column="id"></key>,表示自己的主键,且是外键引用父类表的主键(一对一的关系) |
--> |
<joined-subclass name="Topic" table="Topic"> |
<key column="id"></key> |
<property name="type"></property> |
</joined-subclass> |
<!-- 子类:Reply --> |
<joined-subclass name="Reply" table="Reply"> |
<key column="id"></key> |
<property name="floor"></property> |
</joined-subclass> |
</class> |
</hibernate-mapping> |
<hibernate-mapping package="..mapping"> |
<class name="Article" table="itcast_Article"> |
<id name="id"> |
<generator class="hilo"> |
<param name="table">hi_value</param> |
<param name="column">next_value</param> |
<param name="max_lo">100</param> |
</generator> |
</id> |
<property name="title"></property> |
<property name="content"></property> |
<!-- 子类:Topic |
需要指定表名。 |
--> |
<union-subclass name="Topic" table="topic"> |
<property name="type"></property> |
</union-subclass> |
<!-- 子类:Reply --> |
<union-subclass name="Reply" table="reply"> |
<property name="floor"></property> |
</union-subclass> |
</class> |
</hibernate-mapping> |
关于Hibernate的查询,有多种方式:
命名查询
Query query = session.getNamedQuery("queryAllEmployees");
List list = query.setParameter(0, 5).list();
对应的配置文件:
<!-- 定义命句查询语句 -->
<query name="queryAllEmployees">
<!--[CDATA[FROM Employee WHERE id>?]]-->
</query>
如果执行一条查询,查询出了多个结果,则会抛NonUniqueResultException异常.