一 、第一个应用实例
1.搭建环境:新建一个名为HibernateDemo的java工程,并导入Hibernate的jar包,特别要注意除了导入lib下的jar包还需导入hibernate3.jar核心jar包。 由于涉及数据库操作,还应导入mysql驱动包。
说明,如果使用最新的hibernate,hibernate开发的基本jar包(7个) 来源:hibernate-distribution-3.3.2.GA.zip
hibernate3.jar
lib/required下的所有jar包
2.简述Hibernate的作用:ORM:Object Relational Mapping,对象关系映射。将java程序中的对象自动持久化到关系数据库中。而Hibernate的作用好比就是在java对象与关系数据库之间的一座桥梁,它主要负责两者之间的映射。在Hibernate内部封装了JDBC技术(但只是一个轻量级的封装,因而可以让程序设计人员更方便的以面向对象的思想操纵数据库),并向外提供API接口。
3.建新一个名为User.java的类,即是上面所说的java对象。我们以后称这种类为实体类(或是持久化类),它的对象为实体对象(或是持久化对象)。User.java内容如下:
package com.asm.hibernate.domain;
import java.util.Date;
public class User {
private int id;
private String name;
private Date date;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Date getDate() {
return date;
}
public void setDate(Date date) {
this.date = date;
}
}
4.编写配置文件:User.hbm.xml。它和User.java放在同一个包下。内容如下:
<?xml version="1.0"?>
<!DOCTYPE hibernate-mapping PUBLIC
"-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">
<hibernate-mapping
package="com.asm.hibernate.domain">
<class name="User">
<id name="id">
<generator class="native"/>
</id>
<property name="name"></property>
<property name="date"></property>
</class>
</hibernate-mapping>
此配置文件,是用来为User.java进行配置的,我们以后称这种文件为实体配置文件(或是持久化类映射文件) <class>用来关联一个java类,注意在前面的根元素下有一个package属性,这样结合这个package和class标签下所指定的类名,就共同关联映射了一个java类。 其实可以这样理解,每一个包下都有实体配置文件,而这个配置文件开始的根元素package指定了此文件所处的位置(或是说它所关联的包),根元素下可以有多个<class>标签(查阅dtd文件),它们可以分别来关联包下的java类文件。
<class>标签,一般建议至少有两个属性:name属性用来关联一个java类,比如这里关联了User类;table属性用来指定这个类所对应的表文件,如果不指定,系统会自动name指定的类文件进行关联(比如上面实际是:<class name="User" table="user">)
<class>标签下的子标签:
? <id>子标签实际就是用来映射主键,<id>下的name就是用来指java类中的id属性,而它可以有一个column属性用来指定表中的主键。同时注意在此标签下有一个<generator class="native"/>标签,它是用来指定主键的生成方式。
? <property>子标签,就是用来指定java类的属性映射到表中的一个字段,默认下此标签没有指定column属性,即是说它会把name所关联的属性名作为字段名。 如果不想java类中的某些属性映射到表中,只要不用这个标签来关联这些属性即可。
? 总结:上面的<class><id><property>的name属性都分别指定了java类,java类的属性。而table,column是用来指定表,字段名
配置文件:hibernate.cfg.xml。它放在当前的项目的根目录下。内容如下:
<!DOCTYPE hibernate-configuration PUBLIC
"-//Hibernate/Hibernate Configuration DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-configuration-3.0.dtd">
<hibernate-configuration>
<session-factory name="foo">
<property name="hibernate.connection.driver_class">com.mysql.jdbc.Driver</property>
<property name="hibernate.connection.url">jdbc:mysql://localhost:3306/test</property>
<property name="hibernate.connection.username">root</property>
<property name="hibernate.connection.password">123456</property>
<property name="hibernate.dialect">org.hibernate.dialect.MySQLDialect</property>
<property name="hibernate.hbm2ddl.auto">create</property>
<mapping resource="com/asm/hibernate/domain/User.hbm.xml"/>
</session-factory>
</hibernate-configuration>
主配置文件,完成了驱动注册,数据库连接,并关联了相应的java对象配置文件。
说明:<mapping>具体指定了关联的所有实体配置文件,关于它的作用可以注释掉此属性看效果。另通过<property name="hibernate.hbm2ddl.auto">create</property>指定了根据实体配置文件来自动生成表,其中包括:create/create-drop/update/validate四种可选方式。
5.编写测试类:UserTest.java 内容如下:
package com.asm.hibernate.test;
import java.util.Date;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.Transaction;
import org.hibernate.cfg.Configuration;
import com.asm.hibernate.domain.User;
public class UserTest {
public static void main(String []args){
Configuration cf=new Configuration();
cf.configure();
SessionFactory sf=cf.buildSessionFactory();
Session s=sf.openSession();
Transaction ts=s.beginTransaction(); //事务
User user=new User();
user.setName("jack");
user.setDate(new Date());
s.save(user);
ts.commit(); //提交事务
s.close();
System.out.println("done");
}
}
6.分析流程:首先抛开Transaction tx=s.beginTransaction()和tx.commit(),因为它们是提交事务得。支持提交事务意味着支持数据回滚。说明,通常情况下,很多数据库都默认支持提交事务,所以加这两句代码非常必要。 下面具体谈流程:
第一步:获取SessionFactory对象,它会首先构建一个Configuration对象,此对象调用可以调用configure()和configure(String resource)这两种方法:这两种方法在Configuration中的源代码如下:
public Configuration configure() throws HibernateException {
configure( "/hibernate.cfg.xml" );
return this;
}
public Configuration configure(String resource) throws HibernateException {
log.info( "configuring from resource: " + resource );
InputStream stream = getConfigurationInputStream( resource );
return doConfigure( stream, resource );
}
分析这两个源代码可以知道:无参调用最终也是调用这个有参数的方法,所以我们也可以直接传参数调用。 现在的重点是读配置文件,这个配置文件我们一般放在eclipse的scr根目录下,而当eclipse编译时会自动把这个目录下的文件编译到bin目录下,而这个bin目录下是被配置成classpath环境变量,而configure方法就是在classpath环境变量下查找配置文件。 再来分析,无参调用configure方法时,默认的是传递的hibernate.cfg.xml配置文件,所以只有取名为这个的配置文件,才可以调用无参的configure方法,如果是其它名字的配置文件,则调用含参的配置文件,并且这个参数名应为这个配置文件的名字。 当读取配置文件后的Configuration对象,才是一个真正意义上可操控的实例对象。 然后,再用这个对象来构建一个SessionFactory对象。 强调说明,这一步整个操作最好是放在类的静态代码块中,因为它只在该类被加载时执行一次。
第二步:得到一个Session实例,以进行数据库CRUD操作
第三步:实例化一个java类
第四步:持久化操作
第五步:后续操作:主要是关闭连接
7.实体类定义规则:
Domain object(java对象)必须要有构造方法,同时建议有一个id属性,为了赖加载,这个java类的声明最好不用final。
8.开发流程:
官方推荐:先Domain object 再mapping,最后是DB。 常用开发方式:DB开始,由工具来生成mapping和Domain object。
9.总结基本步骤:
环境搭建(导入相关包等) —>实体类及配置文件—>主配置文件(完成了数据库的配置及通过设置属性创建了相应的表)—>得到Session测试应用。
二、 优化代码
1.为会么要优化
在前面我们已经知道,获取SessionFactory对象是一个重复的过程。因此我们可以把这个操作写成一Util类。下面我们把这一步写成工具类HibernateUtil,内容如下:
package com.asm.hibernate.utils;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.cfg.Configuration;
public class HibernateUtil {
private static SessionFactory sf;
private HibernateUtil() {
}
static {
Configuration cf = new Configuration();
cf.configure();
sf = cf.buildSessionFactory();
}
public static SessionFactory getSessionFactory() {
return sf;
}
public static Session getSession() {
return sf.openSession();
}
}
2.优化测试类
下面复制UserTest.java代码改为UserTest2.java并进行修改 修改后的内容如下:
package com.asm.hibernate.test;
import java.util.Date;
import org.hibernate.HibernateException;
import org.hibernate.Session;
import org.hibernate.Transaction;
import com.asm.hibernate.domain.User;
import com.asm.hibernate.utils.HibernateUtil;
public class UserTest2 {
static void addUser(User user) {
Session s = null;
Transaction ts = null;
try {
s = HibernateUtil.getSession();
ts = s.beginTransaction();
s.save(user);
ts.commit();
} catch (HibernateException e) {
if (ts != null)
ts.rollback();
throw e;
} finally {
if (s != null)
s.close();
}
}
public static void main(String[] args) {
User user = new User();
user.setName("richie");
user.setDate(new Date());
addUser(user);
}
}
说明,在addUser方法中其实也可以不用catch语句捕获。因为关键的关闭连接已在finally实现。上面的例子可以作为以后Hibenate操作的一个典型模板,只需要修改主方法中的内容即可。
3.get方法:可以在UserTest2.java中增加这个方法:
static User getUser(int id) {
Session s = null;
try {
s = HibernateUtil.getSession();
return (User) s.get(User.class, id);
/*
* User user=(User) s.load(User.class,id);
* System.out.println("----load----"+user);
* System.out.println(user.getName());
* //load只是准备连接到数据库,当增加上面一句操作时表示有真正的数据库操作,这时它才会去连接数据库 return user;
*/
} finally {
if (s != null)
s.close();
}
}
以上的代码,实现了数据库的查询操作,这里的get()方法需要传递两个参数,理解传递的参数:由于Session可以管理多个数据库所对应的多个实体对象,如果只是传递id将不能正确定位表,因而必须传递这个实体对象 ,get方法才能去查找这个实体对象所对应的数据库中的表。 用这个方法得到User对象后,便可以用此对象的方法来得到相关属性(也就是数据库表中的字段)
4.load()方法,懒加载。
它的特点是:只有实际操作才会被加载,且它是生成的这个User.java的子类,可以从打印结果看出。也正因此,所以前面建议实例类不使用final。强调:如果是懒加载,即使数据库中查不到数据,上面的user对象永远不会为空,因为它的内部实现实际上是new了一个User(子)类对象。
下面再在main方法中测试,增加语句如下:
User u = getUser(1);
System.out.println("id=" + u.getId() + "/t name=" + u.getName());
5.控制台显示:
<property name="show_sql">true</property> 在总配置文件中增加这个属性将会在控制台显示数据库操作的“数据库语言”。称这个属性为数据库语言显示。
三 、Session中的主要方法
1.保存数据:save,presist 说明:这两种方法的主要区别主要体现在未开启事务时。save方法如果是没开启事务,会执行相关sql语句,随后再回滚。而presist根本就不执行这些sql语句。
2.删除对象:delete
3.更新数据:update 说明,如果数据库中没有记录将会出现异常
4.查找数据:get,立刻访问数据库 load,返回的是代理,不会立即访问数据库。
5.选择操作:saveOrUpdate,merge,根据id和version的值来确定是save还是update。saveOrUpdate方法的主要作用:可以把瞬时对象或脱管对象转成持久对象,而不需要具体判断对象是处在瞬时态或是脱管态来选择save或update来让对象变成持久态。只要调用此方法就能由id和version来灵活选择是保存或更新。而merge方法一个对象后,对象仍是脱管态。
5.持久对象:lock,把对象变成持久对象,但不会同步对象的状态。
四 、对象三种状态
1.瞬时(transient):数据库中没有数据与之对应,超过作用域会被JVM垃圾回收器回收,一般是new出来的且与Session无关系的对象。
2.脱管-游离(detached):数据库中有数据与之对应,但当前没有Session与之关联:脱管对象状态发生改变,Hibernate不能检测到。
3.持久(persistent):数据库有数据与之对应,当前与Session有关联,并且相关联的Session没有关闭,事务没有提交:持久对象状态发生改变时,在事务提交时会影响到数据库。
理解:与Session是否关联,数据库是否有数据与之对应是判断三种对象状态的依据。比如,瞬时状态跟它们均无关;脱管,只是数据库有数据与之对应,失去了Session对它的管理;而持久与两者者有关。
从过程中理解三种对象状态:结合前面的实例,当我们User user=new User()一个对象时,它表示创建一个瞬时对象,当调用save(user)方法时,这个对象成为持久对象,直到事务提交,数据库连接关闭。在这期间,如果我们user.setXXX()时,会对这个持久对象产生影响,最终它也会被提交到数据库,它的最终提交是在提交事务时。比如save(user)方法后,跟user.setName("new name");和user.setPassword("new password");这两句,这样它会在提交时务时,采取对数据库的更新操作,也就是说数据库连接关闭后,数据库存的是“new name”和“new password” 而如果开启了“数据库语言显示”可以发现执行两次操作:一次是save方法的插入操作,一次是setXXX后提交事务时的更新作(特别说明,持久对象在发生改变时,比如setXXX方法改变对象内容时,会在最后,即提交事务时统一进行更新操作,而并非每一次改变就执行一次更新,这样可以保证与数据库的交互更高效合理)。当执行完save方法后,我们关闭数据库连接时,这时的user对象就是脱管状态,因为它在数据库有数据与之对应 而脱管状态的最好例子是当我们用get方法得到一个对象并关闭连接时
补充说明:既然我们已经知道了持久对象可以被Hibernate检测到进行更新操作,那么update是否还有用了?有,比如脱管对象就可以调用update来更新数据库中的数据,而调用update()方法后的脱管对象又变成了持久对象。 下面是三种对象状态相互转换的图例
再谈saveOrUpdate方法:此方法兼具了save和update两种方法。它根据传递的参数来选择执行其中的一种方法。如果参数对象是瞬时态,则执行save方法,而如果参数对象是脱管态,则执行update方法。最终都是把传递的参数对象转成持久态。 如何判断对象的状态?主要依据是看:实体对象id(或者version)取值与实体配置文件中<id>元素的unsaved-value属性值的匹配情况。 只要满足下面的任一情况则可以说明对象处在瞬时态:情况一,实体对象(持久化对象)的id取值为null形式(比如int型为0,字串为null)。 情况二,实体对象(持久化对象)的id取值与预设的unsaved-value属性值不同。 情况三,实体对象(持久化对象)的具有的versionn属性,并且为null。 情况四,实体对象(持久化对象)的version取值与预设的unsaved-value属性值不同。
五、 完善工具类及HQL QBC初步相关
1.无聊的讨论:
在前面我们写了一个工具类:HibernateUtil。其实我们还可以把CRUD操作封装到这个工具类中,并把它们都做成静态的,这样这个工具类就可以直接调用了。但是这样的操作对查询数据可能不是很好,因为它的查询方式很多,除非我们一一考虑这些可能涉及到查询方式,并能以重载的形式进行统一管理。 其实我也试想过把这此数据库操作方法进行二次封装,在工具类写成如下形式:
public void operate(int i ){
if(i==1){ 调用更新方法,执行查询操作}
if(i==2){ 调用删除方法,执行查询操作}
if(i==3){ 调用插入方法,执行查询操作}
if(i==4){查询?可指定一个惯用的查询方法,但返回值如何处理,所以建议不在此处写查询,可再写一个查询的统一操作方法来总括所有的查询方法}
}
2.HQL的作用概述
数据库的操作,难点主要集中在查询操作中,而HQL就是专门用来为查询服务的。
3.HQL应用的步骤:
假定我们已有一个Session对象s
>>步骤一,获得Query对象:Query query=s.createQuery(“HQL SELECT Sentence”);
>>步骤二,为参数赋值:query.setXXX();
>>步骤三,获得List对象:LIST list=query.list(); 说明,除了此方法外,Query接口还有一个常用的方法uniqueResult,如果明确查询的结果只有一个,便选择使用此方法。如果查询结果有多个使用此方法会报异常。
>>步骤四,遍历查询结果:即遍历上面list对象。
关于步骤二为参数赋值的问题:比如步骤一中的“HQL Sentence”内容为:from User u where u.name=? and u.password=? and ...,如果这里的?较少可以setXXX(0,”...”); setXXX(1,”...”); 但是如果?较多,就容易把这些设置写错,所以可以采取命令参数的方式来决定后面的setXXX的内容。 比如:from User u where u.name=:uname and u.password=:upass and ... ,这样后面就可以写setXXX(“uname”,”...”);
4.一个细节问题:
在前面我们的实体类为User类,而在实体配置文件中<class name="User">意为它所关联的表为user表(没有指定名称table),但如果是oracle数据库,我们知道它本身就有一张user表,这样就产生了冲突,如何解决这种冲突?一种方法是设定table属性为新的名字(首选方法),另一种方法是加`(数字1前的那个符号),即这样<class name="User" table="`user`">写,这样我们的表还是user表。同样如果属性名与数据库中的关键字产生这种冲突,也可以按此方法解决。
5.分页技术:
query.setFirstResult(200);query.setMaxReslut(10);这两句的意思是符合要求的语句有很多条,我们从第200条取,取出10条。我们知道每种数据库的分页语句是不同的,而Hibernate底层判断使用哪种分页语句就是参照前面配置文件的方言属性。
6.QBC条件查询:
与它相关的是Criteria Interface,Criterion Interface,Expressson Class。其实它的操作和HQL很相似。同样我们假定已有一个Session对象s.
>>步骤一,获得Criteria对象:Criteria criteria = s.createCriteria(User.class);
>>步骤二,封装查询条件为一个Criterion对象:Criterion cr = Expression.eq("name", "new name"); (说明Expression继续于org.hibernate.criterion.Restrictions类),所以也可以这样写:Criterion cr=Restrictions.eq("name","new name"); Restrictions类中的封装查询条件的方法都有两个参数:前一个参数是指创建Criteria对象时所使用的参数的属性名,后一个是要与属性名比较的值。比如这里是指User类的name属性是否与“new name”相等
>>步骤三,获得带查询条件的Criteria对象:criteria.add(cr); 执行此步才使这个对象具有一个条件限制的查询操作。
>>步骤四,获得List对象以遍历:List clist = criteria.list(); 补充说明:也可以直接返回一个User对象:User user=(User) criteria.uniqueResult();
特别说明:Criteria对象也具有分页的功能,方式是和上面Query一样 。
六 、基本应用实例:Dao设计
1.总体设计:设计User对象及相关实体配置文件,工具类(得到一个Session对象),UserDao接口(实现此接口即以操作数据库),编写主配置文件,编写测试类。
2.UserDao的设计,最初我想打算设计成通用Object的操作,后来发现它的Session对象操作都要传递一个对象,就设计成如下形式。内容如下:
package com.asm.dao;
import com.asm.domain.User;
public interface UserDao {
public void saveUser(User user);
public User queryById(int id);
public User queryByName(String name);
public void update(User user);
public void delete(User user);
}
按此设计,意思是此类专门针对User对象的数据库操作,传递User对象,所以后面它的实现类的query相关方法可以直接user = (User) s.get(User.class, name);写name为传递的参数,而我们知道操作的是User对象,所以直接可以User.class。 值得一提的是,在JDBC操作中,delete,传递id这种值就可以实现删除,而Hibernate的删除操作,必须传递一个对象,操作过程就是我们通过id查出这个对象,再把这个对象传递给删除方法以供删除。而实事上也可以new一个User对象,并设定的id,然后再把这个对象传递给删除方法。 但需要特别注意new出的对象必须完成符合我们通过id查出的对象。
3.这个实例参照前面的相关,基本可以写出。以下几点需要注意:导包:Hibernate包,数据库包;改写配置文件;查询方法的设计;注意事务,特别是“增删改”要注意事务。
七 、关联关系讨论
1.多对一关系映射:
一个部门有可以有多个员工,而一个员工只属于一个部门。从员工角度看,很多员工会隶属一个部门。 现以实例说明,实例概要:一个部门类,只有id和部门名称两个属性。有一个员工类,有id和员工名称及部门对象三个属性。操作步骤如下:
>>步骤一,建立Depatment.java及实体配置文件:
package com.asm.hibernate.domain;
public class Department {
private int id ;
private String name;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
----同包下的实体配置文件:Depart.hbm.xml
<?xml version="1.0"?>
<!DOCTYPE hibernate-mapping PUBLIC
"-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">
<hibernate-mapping package="com.asm.hibernate.domain">
<class name="Department">
<id name="id">
<generator class="native" />
</id>
<property name="name"></property>
</class>
</hibernate-mapping>
以上的操作,没的什么可多言的,和前面的配置是一样的形式。
>>步骤二,Employee.java内容如下及实体配置文件
package com.asm.hibernate.domain;
public class Employee {
private int id;
private String name;
private Department depart;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Department getDepart() {
return depart;
}
public void setDepart(Department depart) {
this.depart = depart;
}
}
----同包下的实体配置文件:Employee.hbm.xml
<?xml version="1.0"?>
<!DOCTYPE hibernate-mapping PUBLIC
"-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">
<hibernate-mapping package="com.asm.hibernate.domain">
<class name="Employee">
<id name="id">
<generator class="native" />
</id>
<property name="name"></property>
<many-to-one name="depart" column="depart_id" />
</class>
</hibernate-mapping>
先来说这个类文件,它的一个重要属性就是Department对象,这就是它所关联的一个外键,这里我们只必须记住一点,每个实体类对应于一张表,如果一张表想关联另一张表,则只需要在这张表所对应的实体类中引入它想关联表的实体类对象。再进行简单的配置即可。再来看配置文件,这里主要看这个<many-to-one name="depart" column="depart_id"></many-to-one>元素,它的name属性仍然是实体类中的属性。column为对应表的外键。可以留意下数据库表中的相关表。
>>修改主配置文件,增加以下内容以便找到相应的实体配置文件。
<mapping resource="com/dep/emp/Employee.hbm.xml"/>
<mapping resource="com/dep/emp/Depart.hbm.xml"/>
>>步骤四,编写测试类。类中省略导入包的内容。
package com.asm.hibernate.test;
public class ManyToOneTest {
public static void main(String[] args) {
add();
}
static void add() {
Session s = null;
Transaction tx = null;
try {
Department depart = new Department();
depart.setName("departName");
Employee emp = new Employee();
emp.setName("empName");
emp.setDepart(depart);
s = HibernateUtil.getSession();
tx = s.beginTransaction();
s.save(depart);
s.save(emp);
// 交换以上两句的位置,看Hibernate执行的sql语句。会再增加一条更新操作。
tx.commit();
} finally {
if (s != null)
s.close();
}
}
}
说明:以前插入新的记录,都要传递一个实体对象,而这里没有这么做,原因是为了体现出一个插入记录的顺序问题,仔细观察add()方法,发现先保存的是Department对象,再保存的是Employee对象,原因是什么?这里略作说明,我们知道此项目设计的是让Employee表来关联Department表,而要想关联此表,必须保证此表已进行存储,这里暂时这样理解,也可以试试颠倒save方法的顺序,或是不保存depart来看看效果,这样也许有助理解。
简要说明,以上提到的尝试性操作:颠倒顺序,可以通过,只是增加了一条更新操作;不保存dep则不能通过,原因是此实体对象没有保存;再测试,如果注释掉“depart.setDname("deptName");”后面按正常顺序执行,发现是能正确执行的。只是在数据库的employee表中depart_id字段为null,如果我们在前面设置<many-to-one name="depart" column="depart_id">中增加:not-null="true"这个属性,再来执行将会报“不能为空的错误”。通过增加这个属性,可以让我们更好理解如何建立两个对象的关联。
>>步骤五,再探查询方法。接上面,在测试类中增加一个查询方法,内容如下:
static Employee query(int empId) {
Session s = null;
try {
s = HibernateUtil.getSession();
Employee emp = (Employee) s.get(Employee.class, empId);
System.out.println("Department Name:" + emp.getDepart().getName());
return emp;
} finally {
if (s != null)
s.close();
}
}
随后再在主方法中增加如下内容:便可以测试结果:
query(1);
对结果的说明分析:如果在以往的JDBC操作中,我们想得到这个部门的信息,需要查两次才能得到,这里的 Hibernate就实现了这一优化,如果我们开启了控制台的数据库显示功能(主配置文件中开启,前面已提到),便可以查看下数据库的执行操作,并切实感受到这里实际执行了两步查询操作,在以后的操作中,建议开启此属性,以便切实体会数据库操作。
>>步骤六,增加一个新的查询方法query2,内容如下,注意和query区别:
static Employee query2(int empId) {
Session s = null;
try {
s = HibernateUtil.getSession();
Employee emp = (Employee) s.get(Employee.class, empId);
Hibernate.initialize(emp.getDepart());
//上一句作用后面会作说明,这里略作了解
return emp;
} finally {
if (s != null)
s.close();
}
}
在main方法中增加如下内容进行测试:
Employee emp = query2(1);
System.out.println(emp.getDepart().getName());
2.一对多关系映射:
上面提到的多个员工对应于一个部门,是多对一的关系,如果一个部门有多个员工,则用“一对多”的关系来表示这个关系,现接上例继续,以实现一对多的关系映射。具体操作步骤如下:
>>步骤一,在Department.java中增加如下内容:private Set<Employee> emps; 及相应的get/set方法。
>>步骤二,修改Department的实体配置文件。在class元素下增加如下内容:
<set name="emps">
<key column="depart_id"></key>
<one-to-many class="Employee"/>
</set>
说明: <set>元素同样是指定一个属性,只不过是指定一个特殊的属性,key中的column为映射文件的外键,这里是指Employee.java相关的实体对象的外键。<one-to-many class="Employee"/>中的class表示关联的外键为Employee对象,也即Set中泛型机制指定的类。
>>步骤三,修改主配置文件,关联上面的实体配置文件。
>>步骤四,编写测试类,OneToManyTest.java 内容(省略导入的包)如下:
package com.asm.hibernate.test;
public class OneToManyTest {
public static void main(String[] args) {
add();
query(1);
}
static Department query(int departId) {
Session s = null;
try {
s = HibernateUtil.getSession();
Department depart = (Department) s.get(Department.class, departId);
System.out.println("employee size:" + depart.getEmps().size());
return depart;
} finally {
if (s != null)
s.close();
}
}
static void add() {
Session s = null;
Transaction tx = null;
try {
Department depart = new Department();
depart.setName("departName");
Employee emp1 = new Employee();
emp1.setName("empName1");
emp1.setDepart(depart);
Employee emp2 = new Employee();
emp2.setName("empName2");
emp2.setDepart(depart);
// Set<Employee> emps = new HashSet<Employee>();
// emps.add(emp1);
// emps.add(emp2);
// depart.setEmps(emps);
s = HibernateUtil.getSession();
tx = s.beginTransaction();
s.save(depart);
s.save(emp1);
s.save(emp2);
tx.commit();
} finally {
if (s != null)
s.close();
}
}
}
说明:此类比较简单,增加两个员工信息,然后查出,同样可以体现出这种查询的方便,可以查看控制台,发现实质也是进行了两次查询操作。 特别注意,上面注释掉的内容,思考怎样实现映射?如果加上注释掉的内容将会在控制台增加两条更新操作(注意更新的内容)。实际上注释掉的内容和已有内容建立了两种关联:“多对一”、“一对多”。
3.一对一关系映射:
典型的实例:一个人有一个身份证,而一个身份证只属于某一个人。以此为模型实现一对一关系的映射。下面的实例先以主键进行关联:
主键关联:从表的主键同时又作为外键参考主表的主键。比如在下面的实例中,人作为主表,身份证作为从表。
>>步骤一,建立Person类及相关的实体配置文件。
package com.asm.hibernate.domain;
public class Person {
private int id;
private String name;
private IdCard idCard;
} ...省略相应的get/set方法。
配置文件:person.hbm.xml 和前面的配置一样,只需要留意下</one-to-one>元素,内容如下:
<hibernate-mapping package="com.asm.hibernate.domain">
<class name="Person">
<id name="id">
<generator class="native" />
</id>
<property name="name"></property>
<one-to-one name="idCard"></one-to-one>
</class>
</hibernate-mapping>
>>步骤二,建立IdCard类及相关的实体配置文件。
package com.asm.hibernate.domain;
import java.util.Date;
public class IdCard {
private int id;
private Date validity;
private Person person;
} ...省略相应的get/set方法。
从表配置文件:IdCard.hbm.xml ,内容如下:
<hibernate-mapping package="com.asm.hibernate.domain">
<class name="IdCard">
<id name="id">
<generator class="foreign">
<param name="property">person</param>
</generator>
</id>
<property name="validity"></property>
<one-to-one name="person" constrained="true"></one-to-one>
</class>
</hibernate-mapping>
配置文件说明:由于上面提到的是采取主键关联,即是说这里的id即是主键,同时也是(关联相关表的)外键,因此,以下对id的生成采取了”foreign”方式,其实这种方式也就指明了主键同时为外键。下面的<param>指定了外键的参考信息,此元素中的内容指明了它参考<one-to-one>的person。 注意在<one-to-one name="person" constrained="true">中设定了constrained属性,其作用是说明该配置文件所映射表的主键同时作为外键,参照关联类对应表的主键。设定了此属性可用“show create table idcard”查看表idcard的定义发生了变化。
>>步骤三,修改主配置文件,关联上面的实体配置文件。
>>步骤四,编写测试类,OneToOneTest.java 内容如下:省略导入的包。
package com.asm.hibernate.test;
public class OneToOneTest {
public static void main(String[] args) {
add();
}
static void add() {
Session s = null;
Transaction tr = null;
try {
s = HibernateUtil.getSession();
tr = s.beginTransaction();
Person person = new Person();
person.setName("pName");
IdCard idCard = new IdCard();
idCard.setValidity(new Date());
//分别注释掉以下两句,看程序执行情况
person.setIdCard(idCard);
idCard.setPerson(person);
s.save(person);
s.save(idCard);
tr.commit();
} finally {
if (s != null)
s.close();
}
}
}
说明:留意上面的注释,如果注释掉第一句,发现一切正常,因为主对象是可以没有此属性,它的实体配置文件也基本与前面一样。而如果注释掉下面一句,将会报错,原因是“attempted to assign id from null one-to-one property: person”,IdCard的实体配置文件关联了一个表,而它采取主键关联,而主键关联要依赖于person属性的id,如果这里注释掉,即没有了此属性,它也关联不了相应的id。简单的说,IdCard来要关联Person,我们称它为从对象,而person并不关联谁,我们称为主对象。现在只要记住,从对象关联了表(关联了主对象),必须设定它所关联的主对象属性
>>步骤五,编写两个查询方法,一个查询主对象,主要代码:
static Person query(int id) {
Session s = null;
Transaction tr = null;
try {
s = HibernateUtil.getSession();
tr = s.beginTransaction();
Person p = (Person) s.get(Person.class, id);
System.out.println("身份证有效期:" + p.getIdCard().getValidity());
tr.commit();
return p;
} finally {
if (s != null)
s.close();
}
}
然后再在main方法中调用此方法,并开启控制台数据库库语言显示后,可以从控制台看出查询主对象只select一次;再增加一个查询从对象的方法,主要代码:
static IdCard query2(int id) {
Session s = null;
Transaction tr = null;
try {
s = HibernateUtil.getSession();
tr = s.beginTransaction();
IdCard idCard = (IdCard) s.get(IdCard.class, id);
//System.out.println("人的名字:" + idCard.getPerson().getName());
//去掉上一句注释后,发现会查询两次。
tr.commit();
return idCard;
} finally {
if (s != null)
s.close();
}
}
同样在main方法中调用此方法,并开启控制台数据库库语言显示后。从控制台看出也只会查询一次,但是如果去掉注释后发现会查询两次。 接着,在此例的基础上修改成外键关联。
外键关联:从表的主键并不作为外键参考主表的主键,而是将其它字段作为外键参的主键。
其实在上例的基础上,我们只需要修改IdCard.hbm.xml配置文件即可,修改后的内容如下:
<hibernate-mapping package="com.asm.hibernate.domain">
<class name="IdCard">
<id name="id">
<generator class="native" />
</id>
<property name="validity"></property>
<many-to-one name="person" column="person_id" unique="true" />
</class>
</hibernate-mapping>
说明:由于采取了外键关联,所以这里的从表的主键将不再作为外键参考主表的主键,所以它会采取一般的方式生成主键,即<id>生成和以前的那此相同采取“native”方式。 另注意到< many-to-one >,发现增加了unique有属性,这样尽管是多对一,但能有效保证实质是一对一。 这时运行原OneToOneTest,发现仍是和以前一样。 如果我们再修改Person的实体配置文件<one-to-one>如下:
<one-to-one name="idCard" property-ref="person"/> 特别要注意到property-ref 属性。可以结合执行后表的结构来看。其实如果注释掉此句,其结果就是通过身份证可以查看到Person的相关信息,但是通过Person却不能找到身份证信息,因为Hibernate完全依赖实体配置文件(映射文件)。注释掉当然就不能找到。 而事实上这时在OneToOne中调用query方法,会发现出现空指针异常。 其实在前面的关联关系中,最终都是实现了双向关联,而这里如果注释掉此句,正好成了单向关联的一个例证。
4.多对多关系映射:
典型实例:一个学生可以有多个老师,同样一个老师可以有多个学生,对此设计如下:
学生studnet表:
column id name teachers
老师teacher表:
column id name studnets
在利用学生查到他所有的老师,我们一般会设计中间表,来查找,中间表用来把学生和老师关联,通过此表进行学生和老师之间的交互查找。
中间表teacher_student:
column teacher_id studnet_id
复合主键
在数据库操作中,作以下说明:如果通过老师查找学生,过程:先通过teacher表查找到id,再到teacher_student表中以teacher.id==teacher_student.teacher_id为查询条件来查找studnent_id , 查找到studnet_id后,再以teacher_student.stucent_id==student.id为查询条件来查找所有学生的信息。 同样通过学生查老师,也是类似的过程。
>>步骤一、创建实体类Student、Teacher
Student类内容如下:省略getXXX()、setXXX()方法。
package com.asm.hibernate.domain;
import java.util.Set;
public class Student {
private int id;
private String name;
private Set<Teacher> teachers;
}
Teacher类内容如下:省略getXXX()、setXXX()方法。
package com.asm.hibernate.domain;
import java.util.Set;
public class Teacher {
private int id;
private String name;
private Set<Student> students;
}
>>步骤二、为两个实体创建配置文件(省略了前面的xml文档声明内容):
Student.hbm.xml内容如下:
<hibernate-mapping
package="com.asm.hibernate.domain">
<class name="Student">
<id name="id">
<generator class="native"/>
</id>
<property name="name"></property>
<set name="teachers" table="teacher_student">
<key column="student_id" />
<many-to-many class="Student" column="teacher_id"/>
</set>
</class>
</hibernate-mapping>
说明:这里重点说明最后的<set>配置:(1)Student中Set类型的属性配置一个<set>元素,其实在前面的一对多中已经出现了<set>元素的配置,我们可以这样认为“凡是要为某个Set类型的属性进行映射配置,都可以用<set>元素来配置”。 <set>中的table属性为关联表的名称。 (2)它的子元素<key>中的column为关联表中以该映射文件所映射的表的主键为外键的字段名称. (3)<many-to-many> coumn属性为关联表中以欲关联类对应表的主键为外键的字段名称。
Teacher.hbm.xml内容如下:
<hibernate-mapping
package="com.asm.hibernate.domain">
<class name="Teacher">
<id name="id">
<generator class="native"/>
</id>
<property name="name"></property>
<set name="students" table="teacher_student">
<key column="teacher_id" />
<many-to-many class="Student" column="student_id"/>
</set>
</class>
</hibernate-mapping>
由于和Student.hbm.xml是类似的,这里不作说明,这样也就建立起了双向的多对多关联。
要注意他们所依赖的中间表为teacher_student,所以这里的<set>元素中的table属性和teacher映射文件相同,而特别要注意<key>和<many-to-many>中column中属性值的设定。
>>步骤三、在主配置文件中关联实体配置文件:只需要增加如下内容:
<mapping resource="com/asm/hibernate/domain/Teacher.hbm.xml" />
<mapping resource="com/asm/hibernate/domain/Student.hbm.xml" />
>>步骤四、编写测试文件ManyToManyTest.java:省略导入的包。
package com.asm.hibernate.test;
public class ManyToManyTest {
public static void main(String[] args) {
add();
}
static void add() {
Session s = null;
Transaction tr = null;
try {
s = HibernateUtil.getSession();
tr = s.beginTransaction();
Teacher t1 = new Teacher();
t1.setName("t1Name");
Teacher t2 = new Teacher();
t2.setName("t2Name");
Student s1 = new Student();
s1.setName("s1Name");
Student s2 = new Student();
s2.setName("s2Name");
// 再增加如下内容进行测试:
Set<Teacher> ts = new HashSet<Teacher>();
ts.add(t1);
ts.add(t2);
Set<Student> ss = new HashSet<Student>();
ss.add(s1);
ss.add(s2);
t1.setStudents(ss);
t2.setStudents(ss);
// s1.setTeachers(ts);
// s2.setTeachers(ts);
// 增加内容完
s.save(s1);
s.save(s2);
s.save(t1);
s.save(t2);
tr.commit();
} finally {
if (s != null)
s.close();
}
}
}
说明:注意以上注释掉的内容,如果去掉会出现异常。理解:加上增加的内容后再执行发现,在开启“数据库显示”功能后,发现控制台中新增加了四条插入语句,且是插入到中间表teacher_student中,在此处相当于告诉了每个学生所关联到的老师,而如果在mysql客户端执行“show create table teacher_student”,观察它的表结构并结合“去掉注释的报错说明”,就容易理解到为什么不能有注释掉的内容。 另需要说明的是“多对多”在操作和性能方面都不太理想,所以它使用较少,一般我们会选择转换成“一对多”的模型,而Hiberante的“多对多”实现,可能也是转换成两个“一对多”来实现。
5.组件关系映射:
典型实例:每个人有不同的名字,或者多个人可以是同一个名字,对于这种现象可以是“一个一”或者“多对一”,这样会被设计成两张表来映射,而我们采用组件关系来映射就可以在一张表中映射实现。 具体过程如下:
>>步骤一、编写实体文件Name.java 和People.java
Name.java内容如下: 省略getXXX和setXXX()
package com.asm.hibernate.domain;
public class Name {
private String firstName;
private String secondName;
}
Name.java内容如下: 省略getXXX和setXXX()
package com.asm.hibernate.domain;
public class People {
private int id;
private Name name;
}
>>步骤二、编写配置文件People.hbm.xml 内容如下:
<hibernate-mapping
package="com.asm.hibernate.domain">
<class name="People">
<id name="id">
<generator class="native"/>
</id>
<component name="name">
<property name="firstName" column="first_name"></property>
<property name="secondName" column="second_name"></property>
</component>
</class>
</hibernate-mapping>
说明:尽管在上一步,说Name为实体配置文件,但这里并不为为它写配置文件,它实质相当于一个更一般化的属性,且这个属性是在People的实体配置文件中用〈component〉元素来进行配置的,它的name属性是随意随的有意义的名字,而其子元素分别为Name类的属性值。
>>步骤三、在主配置文件中关联此实体配置文件:
<mapping resource="com/asm/hibernate/domain/People.hbm.xml"/>
>>步骤四,编写测试类,内容如下:省略导入的包
package com.asm.hibernate.test;
public class ComponentTest {
public static void main(String[] args) {
Name n = new Name();
n.setFirstName("zhang");
n.setSecondName("san");
People p = new People();
p.setName(n);
add(p);
}
static void add(People p) {
Session s = null;
Transaction ts = null;
try {
s = HibernateUtil.getSession();
ts = s.beginTransaction();
s.save(p);
ts.commit();
} catch (HibernateException e) {
if (ts != null)
ts.rollback();
throw e;
} finally {
if (s != null)
s.close();
}
}
}
//说明:这种映射较简单,执行后可以查看下表结构。
6.总结关联关系:
在理解Hibernate如何处理这些对象之间的关联关系的底层细节时,考虑如何实现把这种关系保存到数据库,考虑如可检索出关联对象。
八、其它集合类型
说明:在前面我们已经使用过set集合类型,其实除了set外,在Hibernate中还有list、Bag、Map这三种集合类型。
1. list集合的使用:
在前面的“一对多的关系”,已经使用过<set>,为了保存以前的操作,现重建一个名为“TheCollectionMapping”的新项目,把以前的“一对多关系”中用到的相关内容复制到此项目下即可。 但有几点需要特别注意:(1)同样注意导入包,并注意主配置文件对实体配置文件的关联 (2)需要修改原Department中的<set>内容为:private List<Employee> emps; 并修改原实体配置文件<set>配置内容为<list>内容如下:
<list name="emps">
<key column="depart_id" />
<list-index column="order_by" />
<one-to-many class="Employee" />
</list>
(3)并注意测试类中相应的修改 (4)强调说明的是list查询时,具有顺序性,可以查看此项目下测试类的说明。(5)再次强调,为了充分理解一些问题,我们可以查看执行后表结构,也可以注释掉一些内容查看异常情况,并学会解决异常。注意配置文件中相关配置的作用,并尝试探索底层实现。
2.bag集合的使用:
如果在实体类中使用了List类型的属性,而我们并不希望保证集合顺序(保证集合顺序会采用排序算法,因而会占一些CPU资源)可以在配置文件中使用<bag>,它的使用情况与<list>唯一不同的就是不保证集合顺序。
3.map集合的使用:
一般是和实体类中java.util.Map属性进行配合使用,而且使用情况较少。除非某些特殊要求。 总结使用规则:大部分情况下使用<set>;需要保证集合顺序时使用<list>;想用java.util.List而不想保证顺序时,使用〈bag〉;<map>作为特殊使用。配置也基本一样,只是在<map>元素下多了<map-key>
4.使用集合中的注意事项:
(1)在实体类中集合只能被定义成接口而不能定义成具体类,因为集合会在运行时被替换成Hibernate的实现,而事实上把实体类的属性定义成接口也更便于扩展及良好的维护。这里不防提出一个原则,“尽量用接口定义”。(2)Hibernate操作List持久对象时,会用org.hibernate.collection.PersistentList重新实现List接口,操作完成后会把这个实现类重新setXXX放置。具体请参见TheCollectionMapping项目下的OneToManyTest类说明。其它的集合对象也是一样的道理。
九、关联关系中的高级应用
1.级联操作:
在前面的一对多操作中已经有所体现,现在回到前面的一对多操作中,进行一些修改,修改内容如下:
Set<Employee> emps = new HashSet<Employee>();
emps.add(emp1);
emps.add(emp2);
depart.setEmps(emps);
去掉这段注释,并把“s.save(emp1); s.save(emp2);”注释掉,然后运行发现出错。原因是:当我们保存Department depart对象时,发现它所关联到的两个Employee emp对象并未保存,而这种要依赖别的对象的操作即是级联操作,级联操作主要体现在关联关系中,且是可配置的,只是默认的级联操作是被设为”none”,而如果我们希望进行级联操作,只需要在<set>配置中增加cascade属性,比如我们现在Department.hbm.xml 的<set>元素下增加cascade="save-update"属性后,再来执行会发现正常,原因是Hibernate进行了级联操作,相当Hibernate帮我们写了注释掉的“s.save(emp1); s.save(emp2)”这两句内容。下面列表说明级联操作的可选值。
cascade属性值 功能作用描述
none 在保存、修改对象时,不对与之关联的对象进行任何操作,它作为cascade默认值。
save-update 当保存或修改对象时,级联保存所有与之关联的临时对象,级联更新所有与之关联的脱管(游离)对象。
delete 当删除时,级联删除所有与之关联的对象
all 包括save-update和delete两种行为
delete-orphan 删除所有和本对象解除关联关系的对象,主要表现在“一对多”中。
all-delete-orphan 包括all和delete-orphan的行为
说明:(1)cascade可以在<set><list><bag><map>中进行配置,即在“多对一”、“一对多”、“多对多”、“一对一”这些关联关系中,“级联”都是可配的在一般的对象中不能配置。(2)可以为其配置多个属性值,中间用逗号分隔。如cascade=”save-update,delete”.(3)一般只在“一对一 一对多”配置级联,而不会在“多对一 多对多”中配置级联,但在配置时最好的方法是根据实际情况进行配置。现举例说明:比如在“部门和员工”之间,如果我们希望部门取消时,属于该部门的员工并不会被取消,而是分到的别的部门,我们就不应为cascade配置“delete”值。
2.inverse属性:
inverse的作用:询问是否放弃维护关系,默认是false,即不放弃维护关系,即要维护关系。下面借助前面的“一对多”实例来详谈“维护关系问题”。
static void add() {
Session s = null;
Transaction tx = null;
try {
Department depart = new Department();
depart.setName("departName");
Employee emp1 = new Employee();
emp1.setName("empName1");
emp1.setDepart(depart);//员工维护它们之间的关系
Employee emp2 = new Employee();
emp2.setName("empName2");
emp2.setDepart(depart);
Set<Employee> emps = new HashSet<Employee>();
emps.add(emp1);
emps.add(emp2); //员工维护它们之间的关系
depart.setEmps(emps); //部门维护它们之间的关系
s = HibernateUtil.getSession();
tx = s.beginTransaction();
s.save(emp2);
s.save(emp1);
s.save(depart);
//以上的代码的书写顺序分两种情况讨论
tx.commit();
} finally {
if (s != null)
s.close();
}
}
下面借助上面的代码来进行分析:一种情况下是员工维护它们之间的关系。其中的关键代码为已在上面说明(即emp1.setDepart(depart),emp2.setDepart(depart)):此时,我们再注释掉部门维护关系的代码(即注释掉depart.setEmps(emps);),然后执行;执行后再交换s.save(emp2); s.save(emp1);s.save(depart);的保存顺序,再执行。
第二种情况是,部门维护它们之间的关系,需注释掉员工维护关系,再分保存的顺序讨论。
第三种情况是,共同维护它们之间的关系。
下面再列表说明:
先保存员工,再保存部门 先保存部门,再保存员工
员工维护关系 由于是员工维护关系,当我们先保存员工再保存部门时,会自动把部门信息更新到员工表中,这时会有两条更新操作。 由于先保存了部门信息,然后再来保存员工时,便能获知部门信息,所以只进行了插入操作,不会有任何更新操作。
部门维护关系 由于是部门来维护关系,不论怎样的保存顺序,最终都会有两条更新操作。原因是我们要更新的是员工表,当我们最终保存好部门和员工后,都会再把部门信息更新到员工表中。
共同维护关系 4条更新操作,结合上面分析 2条更新操作,结合上面分析。
分析了上面的维护关系情况,下面再来讨论由谁来维护关系:举例,在“一对多”中,如果让“一”来维护关系,相对就低效,如果让“多”来维护关系,则会在效率上有很大的提高。再据此举个生活中的例子,一个老师会有多个学生,如果让老师来维护关系,让老师来记住学生,这将会很难,但如果我们让学生来维护关系,即让每个学生来记住老师,这样将会使效率上有很大的提高,所以通常在关联关系中,我们总是会让“多”这方来维护关系,而让“一”这方放弃维护关系。因此我们需要在“一”的一方配置“inverse”属性,让它来放弃维护关系。为什么要一方放弃维护关系?在上面的例子中,我们在讨论让双方共同维护关系时,会发现有重复的操作,这就是为什么要提出放弃维护关系的原因,在前面的“多对多”实例中,如果让双方都来维护关系将会报错,原因很简单:“多对多”采取的中间表关联,而双方维护关系,将会试图在中间表插入重复的记录,当然是不可行的,至于在“一对多”等关联关系中可以双方维护关系是因为它采取的是更新外键操作。 总结:在“一对多”中,维护关系表现为更新外键,而“多对多”,维护关系表现为在“中间表”中增减记录。
下面我们修改Department.hbm.xml文件,在<set>元素下增加“inverse="true"”属性后,再来执行“OneToManyTest”,可以明显感受到“一”放弃了维护关系。
注意:inverse只出现在集合类型的标签中,但不能在list等有序集合中进行配置。
3.继承关系:
在前面的部门员工实例中,我们设定的员工只是普通的员工,现在假如有Sale和Skill两类员工,它们作为Employee的子类。如何实现这些子类的映射?为了方便以后的操作说明和不影响以前的操作,我们把前面用到的员工部门类及相关实体配置文件,主配置文件等拷贝到一个新的项目theExtend下进行操作。
(1)共享一张表:
意思是我们把子类的当作父类来处理共同映射成一张表。
>>步骤一,创建Employee的子类:Sale和Skill。内容如下:
package com.asm.hibernate.domain;
public class Sale extends Employee {
private String signSale;
...省略getXXX setXXX()
}
package com.asm.hibernate.domain;
public class Skill extends Employee{
private String signSkill;
...省略getXXX setXXX()
}
>>步骤二、修改Employee.hbm.xml配置文件:
<hibernate-mapping package="com.asm.hibernate.domain">
<class name="Employee" discriminator-value="0">
<id name="id">
<generator class="native" />
</id>
<discriminator column="sign" type="string" />
<property name="name"></property>
<many-to-one name="depart" column="depart_id" />
<subclass name="Sale" discriminator-value="1">
<property name="signSale"></property>
</subclass>
<subclass name="Skill" discriminator-value="2">
<property name="signSkill"></property>
</subclass>
</class>
</hibernate-mapping>
配置文件说明: 理解<subclass>元素:它作为class的子元素,但是它却和class非常相似,name同样是指所关联到的“类”,下面的<property>也与class下的<property>一样。我们注意到它和class均增加了一个新属性discriminator-value,它的作用就是配置这些类的“鉴别类型”。而discriminator-value 的配置主要参照的是<discriminator>,它通常称为鉴别类型,即是说鉴别属于什么类型,因为在涉及到继承关系的操作时,总会涉及到父子类的关系,比如在上面的配置中,我们设定了sign子段来标识类型,执行后会再对此作说明。 注意:<discriminator>要写在后面元素的前面,因为它的dtd文件是这样规定得。
>>步骤三、修改主配置文件,由于是拷贝的前面的文件,所以需要去掉无关的映射文件,否则会提示找不到这此映射文件。
>>步骤四、编写测试类:
package com.asm.hibernate.test;
public class ManyToOneTest {
public static void main(String[] args) {
add();
Employee emp=query(2);
System.out.println("emp type:"+emp);
}
static Employee query(int empId) {
Session s = null;
try {
s = HibernateUtil.getSession();
Employee emp = (Employee) s.get(Employee.class, empId);
System.out.println("Department Name:" + emp.getDepart().getName());
return emp;
} finally {
if (s != null)
s.close();
}
}
static void add() {
Session s = null;
Transaction tx = null;
try {
Department depart = new Department();
depart.setName("departName");
Employee emp = new Employee();
emp.setName("empName");
emp.setDepart(depart);
Sale emp2 = new Sale();
emp2.setName("saleEmployee");
emp2.setSignSale("saleName");
emp2.setDepart(depart);
Skill emp3 = new Skill();
emp3.setName("skillEmployee");
emp3.setSignSkill("skillName");
emp3.setDepart(depart);
s = HibernateUtil.getSession();
tx = s.beginTransaction();
s.save(emp);
s.save(emp2);
s.save(emp3);
s.save(depart);
tx.commit();
} finally {
if (s != null)
s.close();
}
}
}
说明:没的什么可多言的,只是要注意在查询时能返回其子类型。
下面来看执行后employee表的内容:
+----+------+---------------+-----------+----------+-----------+
| id | sign | name | depart_id | signSale | signSkill |
+----+------+---------------+-----------+----------+-----------+
| 1 | 0 | empName | 1 | NULL | NULL |
| 2 | 1 | saleEmployee | 1 | saleName | NULL |
| 3 | 2 | skillEmployee | 1 | NULL | skillName |
+----+------+---------------+-----------+----------+-----------+
先来看sign这列:由于sign是鉴别类型设定的字段,且分别在前面为Employee、Sale、Skill分别配置了“0、1、2”所以它们会在sign体现出来。其实<discriminator column="sign" type="string"/>也可以把type属性值设为“int”等。
再来看“signSale、signSkill”字段:它们本身是专为特定的类的属性配置的字段(比如在第一个<subclass>元素下的property子元素就配置了Sale类的signSale属性):所以它们只适合特定的类,而不适的类将会以null来填充,这也就是如果采取“共享一张表”的最大缺点,它限制了我们不能在子类属性所映射的字段上设定“非空”。由于查询只涉及到一张表,所以效率较高。
(2)每个子类一张附表:
意思是每个类均会有一张表,但是它不是完整的表,因为它的一些字段还在父类的表中。即是说:公共字段放在父表中,子类子类分别放在子类所映射的表中,它们之间采取主外键关联。这样解决了上面的“不能设定为空”的缺限。接上面只需修改Employee.hbm.xml配置文件,修改后的内容如下:
<class name="Employee">
<id name="id">
<generator class="native" />
</id>
<property name="name"></property>
<many-to-one name="depart" column="depart_id" />
<joined-subclass name="Sale">
<key column="sale_id" />
<property name="signSale" />
</joined-subclass>
<joined-subclass name="Skill">
<key column="skill_id" />
<property name="signSkill" />
</joined-subclass>
</class>
配置文件说明:当每个子类都会有一张表,在class子元素下设定了<joined-subclass>元素,它的意思就是专门指定为子类映射成一张表,特别要说明的是我们为每个子类都配置了<key>元素,它的作用就是作为外键关联employee表,它的值也是参照class元素的id来生成。执行后的表内容如下:
skill表
+----------+-----------+
| skill_id | signSkill |
+----------+-----------+
| 3 | skillName |
+----------+-----------+ sale表
+---------+----------+
| sale_id | signSale |
+---------+----------+
| 2 | saleName |
+---------+----------+
employee表
+----+---------------+-----------+
| id | name | depart_id |
+----+---------------+-----------+
| 1 | empName | 1 |
| 2 | saleEmployee | 1 |
| 3 | skillEmployee | 1 |
+----+---------------+-----------+
执行后请留意hibernate产生的sql语句。
(3)联合使用表:
意思是同时使用(1)(2)的形式,主要目的是为了能使用鉴别类型,但同时也能“设定为非空”。同样只需要修改配置文件,修改后的内容如下:
<class name="Employee" discriminator-value="0">
<id name="id">
<generator class="native" />
</id>
<discriminator column="sign" type="string" />
<property name="name"></property>
<many-to-one name="depart" column="depart_id" />
<subclass name="Sale" discriminator-value="1">
<property name="signSale"></property>
</subclass>
<subclass name="Skill" discriminator-value="2">
<join table="skill">
<key column="skill_id" />
<property name="signSkill" />
</join>
</subclass>
</class>
观察上面的配置文件,不难发现我们对Sale采取了(1)方式,对Skill采取了(3)方式。同样请留意hibernate的sql语句和执行后的表。特别要说明的是:在进行操作时,由于hibernate在进行操作时不能删除前面的相关联的表(主要是和第一种继承关系“共享一张表”时发生冲突,因为在建立的三张表,在未删除sale/skill表时,是不能来删除employee表,因为employee表中的主键被另两张表关联了),所以需要手工删除表或者是直接删除数据库再建数据库。 补充:借助此例我们来看看discriminator-value的默认情况:如果我们在<subclass name="Skill" discriminator-value="2">中不写discriminator-value配置,再来执行发现一切正常,查表发现它的sign值为“com.asm.hibernate.domain.Skill”即完整的类名,属于字串,所以执行正常。但当我们把<discriminator>中的type属性改成int时将会报错,因为默认不写是以完整的类名字串来作为标识,而我们设定type为int,所以将不能匹配类型。
(4)每个具体类一张完整表:
与第(2)种情况相比,它的主要特点就是为每个具体类建立一张表,表的字段对应子类本身的属性,同样也包括父类的所有属性。 同样只需要修改配置文件如下:
<class name="Employee">
<id name="id">
<generator class="hilo" />
</id>
<property name="name"></property>
<many-to-one name="depart" column="depart_id" />
<union-subclass name="Sale">
<property name="signSale" />
</union-subclass>
<union-subclass name="Skill">
<property name="signSkill" />
</union-subclass>
</class>
注意:id生成器选择了“hilo”,由于我们为每个具体类都映射了一张表,所以id不能只在每张表中递增,如果只在每张表中递增,这里的三张表中的id将会出现重复,这样我们在采用多态查询,将会查出多种结果,我们应让id是唯一的,所以采取了hilo的方式来生成id,它能保证id是全局性的递增生成,这样每张表中的id均不会重复。 同样请注意,可能需要删除某些表或者是删库建库才能执行测试类。 执行完成后,需留意hibernate产生的sql语句和表的结构内容。 补充说明:在使用这种方法时,如果父类为抽象类也是可行得,我们可以在<class>元素中配置abstract=”true”来说明父类为抽象类,自然就不会为其建表。
总结:在上面的继续关系中我们多次用到了删库建库,在执行测试类时,如果出现sql不能更新或者sql相关的错误,则不防尝试此方法。另外在学继承关系时,除了注意配置文件外,更应注意hibernate产生的sql语句以及执行后产生表的情况。通常我们建议表的数目不要超过类的数目。
4.懒加载:
在前面我们已经对懒加载有所提及,现在再借助一个简单的实例(它们均位于lazyLoad项目下)再来重新认识懒加载:先看下面的代码:
package com.asm.hibernate.test;
public class UserTest2 {
public static void main(String[] args) {
addUser();
User u = getUser(1);
System.out.println("return type:" + u + "/t name=" + u.getName());
}
static User getUser(int id) {
Session s = null;
try {
s = HibernateUtil.getSession();
User user = (User) s.load(User.class, id);
// 以下的两种方式都可以让懒加载去真正连接数据库。
Hibernate.initialize(user);
// System.out.println(user.getName());
System.out.println("load--User:" + user.getClass());
return user;
} finally {
if (s != null)
s.close();
}
}
static void addUser() {
省略内容:此方法的作用就是插入一条记录到数据库中,以使我们的查询操作可以进行。
}
}
执行后,打印结果如下:
load--User:class com.asm.hibernate.domain.User$$EnhancerByCGLIB$$212cbff4
return type:com.asm.hibernate.domain.User@1f6ba0f name=jack
注意到上面第一行打印结果,可以发现返回的是User的一个子类,借此来看看懒加载的实现,懒加载的意思是只有当我们真正要查询的数据时,它才真正去连接数据库,为什么要提出懒加载呢,我们知道数据库的连接是非常耗资源的,有了懒加载可以从一定程度上降低数据库连接资源的消耗。 懒加载本质是借助asm.jar和cglib.jar这两个jar包,来生成User的一个子类,这就是前面提到“使用懒加载时实体类不能是final”的原因,从这里我们知道hibernate用构建实体类的子类来实现一个更强大的操作功能,这样即使数据库无记录,查询返回的结果对象也永远不会为null,即是说“返回的结果对象==null”永远不能成立,但这并不表示一定查询出了结果。其实如果我们以懒加载查询的结果为空,而我们再对这个对象进行操作时,是会报错得。 概述要点:(1)hibernate构造了实体类的子类来查询(2)要使用赖加载,实体类不能声明为final (3)查询结果对象永不为空。 (4)只要连接未关,如果我们想获知此对象的任何属性信息都会引起懒加载去连接数据库,但是如果我们是企图获取它的id时,却不能让hibernate去连接数据库。
补充说明:其实这种懒加载的使用很少,它通常在需要关联两个实体对象时使用,比如我们希望把查询出的User user对象关联到部门中去时,即我们 Employee emp=new Employee(); emp.setUser(user);在这种情况下我们并不关心它的查询内容,只是想关联到部门中去,所以此时使用懒加载是一个节省“数据库连接资源”的好方法。
5.概述关联关系中的懒加载:
(1)一对一中的懒加载:必需要同时满足三个条件才能实现懒加载:即在<one-to-ont> 元素中配置lazy!=false(即是为true或proxy);constrained=true;fetch=select,但在主表中不会有懒加载,因为主表不能满足constrained=true;其实在主外键关联的“一对一“关联关系中,我们判断主表和从表也是从是否配置“constrained“来判断:因为constrained的配置只会在从表中出现。从上面的分析中可以得知:查询主表永不会使用懒加载,查询从表可选择懒加载。 下面再结合fethch来分析查询从对象时的懒加载:
fetch=join fetch=select
lazy=true 由于采取的是join连接查询,所以会一次关联所有的表查出所有数据,这样懒加载失效。 懒加载有效,实际查询从对象发展时,才去连接数据库。
lazy=false =false意为不使用懒加载。
lazy=proxy 默认设置:使用代理实现懒加载。
提示:fetch的意思是“通过什么方式抓取”,lazy的意思是“什么时候抓取”。“抓取”
意为“获得数据,连接数据库”。 其实在“一对一”中使用懒加载对性能提升不多大的作用。
分析“查询主对象不能使用懒加”:当我们要想获取一个主对象时,仅从查询主表是不能判断出是否有从对象,比如我们在查询主表获取Person对象时,不能从主表中查出是否有有“身份证号“,这样我们便不能正确设置Person对象的身份证属性,所以hibernate采取了连接查询,这也就是为什么主对象不能使用懒加载的原因。但是查询从对象可使用懒加载,原因如下:当我们通过查询从表获知从对象时,可以在从表的主外键中查询这个身份证号是否有Person对象对应,如果没有,设它的person属性为null,如果有我们放置一个代理,当要真正查询时,便通过这个代理来查询。
(2)其它关联关系使用懒加载的条件:在实配置文件中的“关联关系配置”配置元素中配置时满足以下两个条件:lazy!=false;fetch=select
(3)强调:能够懒加载的对象都是被必定过的代理对象,当相关联的session没有关闭时,访问这些懒加载对象(代理对象)的属性会初始化这些代理(便getId和getClass除外)对象,当相关的session关闭后,再访问懒加载对象将会出现异常“could not initialize proxy - no Session”。
(4)灵活选择:在查询方法是我们有时候希望使用代理懒加载,但有时候我们可能又要具体查询出数据。我们可以这样做来灵活选择:
public Object query(int id,boolean signLazy){
....
if(signLady)
Hibernate.initialize(代理对象);
说明:为true,则会初始化代理,连接数据库,且在此方法调用完成后得到的对象是可以进行有关此对象的属性访问得。为false,则不会去真正连接数据库,只是为了建立起某种关联关系服务
....
}
(5)简单的属性也可以使用懒加载,但效果不大,除非是用在大的文本段。
十、缓存
1.模拟缓存并简要说明缓存实现原理
在myhibernate项目下新建一个包com.asm.hibernate.test.cacheTest来说明与缓存有关的问题。首先看下面的一个模拟缓存程序,主要代码如下:
package com.asm.hibernate.test.cacheTest;
public class CacheSimulate {
static Map cache = new HashMap();
public static void main(String[] args) {
addUser();
//第一次查询,会去连接数据库查询
User u1 = getUser(1);
//第二次查询,直接从Map cache中取
User u2 = getUser(1);
//第三次查询,同样从cache中直接取
User u3 = getUser(1);
}
static User getUser(int id) {
String key = User.class.getName() + id;
User user = (User) cache.get(key);
if (user != null)
return user;
user = getUserFromDB(id);
cache.put(key, user);
return user;
}
static void addUser() {
省略代码,此方法的作用主要是向数据库添加一条记录,以方便查询操作
}
static User getUserFromDB(int id) {
省略代码,作用就是真正去查数据
}
}
分析:重点来看getUser方法:当我们查询一个数据时,会首先在cache中查找,如果是第一次查询某数据,cache中没有存这个数据,会去查数据库。但是如果已经查过数据,便会在cache中查找到此数据,然后直接返回。可以从控制台中看到:hibernate只与数据库交互一次。
为什么要提出缓存的概念:在前面已经多次说过与数据库建立连接是非常耗资源,而且相当耗时。为了保证高效的查询性能,才提出了缓存的概念。缓存的原理:当第一次查询时会从数据库中查,当查出数据后会把数据保存在内存中,以后查询时直接从内存中查。当然,实际的缓存要远比此模拟程序复杂,但整个缓存机制是大同小异得,只是它要考虑到更多的细节。 下面来谈谈缓存机制要解决的三个主要问题:
(1)向缓存中放数据:一般是发生在查询数据库时,因为每当我们不能从缓存中得到所需数据,便会去数据库中查找,查找完成后我们自然要把它更新缓存中。
(2)从缓存中取数据:涉及到一个key的设置问题,比如我们在模拟程序中,key的取值来自“id + 类的类型信息”,这样就能保证key值的唯一性,因为如果仅以id作为key,那么其它的类会有相同的id时,在缓存中就不能区分。
(3)清掉缓存中失效的数据:当有其它的操作更新此数据时,原数据将不再正确,这时我们可以选择更新的方式来重新把新的数据更新到缓存中,也可以直接移除原数据,即调用 remove(key)。
2.Hibernate中的一级Session缓存:
package com.asm.hibernate.test.cacheTest;
public class HibernateCacheTest {
public static void main(String[] args) {
addUser();
getUser(1);
}
static User getUser(int id) {
Session s = null;
User user = null;
try {
s = HibernateUtil.getSession();
user = (User) s.get(User.class, id);
System.out.println("userName:" + user.getName());
// session缓存,当session未关闭时,再查询直接从缓存中获得数据。
user = (User) s.get(User.class, id);
System.out.println("userName:" + user.getName());
// 如果我们清掉缓存,再查询时将会重新连库。
s.evict(user);// 清掉指定的数据
// s.clear();//清掉当前session缓存中的所有内容
user = (User) s.get(User.class, id);
System.out.println("userName:" + user.getName());
} finally {
if (s != null)
s.close();
}
// 当上面的session关闭后,如果想再获取前面查询的数据,必须重新查库。
try {
s = HibernateUtil.getSession();
user = (User) s.get(User.class, id);
System.out.println("userName:" + user.getName());
} finally {
if (s != null)
s.close();
}
return user;
}
static void addUser() {
User user = new User();
user.setName("genName");
HibernateUtil.add(user);
}
}
分析:经过上面的测试和相关说明我们可以得知如下结论:
(1)session的缓存只在session未关闭前有效,关闭后再查相同的数据会重新连库
(2)我们可以手工清除session中的缓存:evict和clear
(3)如果我们清掉session中的缓存,或是第一次查询这个数据,都会引起连库
(4)save,update,savaOrUpdate,load,get,list,iterate,lock等方法都会将对象放在一级缓存中,具体可以在上例的基础上进行测试。
(5)session一级缓存不能控制缓存数量,所以在大批量操作数据时可能造成内存溢出,这时我们可以用evict,clear来清除缓存中的内容
(6)session在web开发应用中,一般只在一个用户请求时进行缓存,随后将会关闭,这个session的存活时间很短,所以它的作用不大,因此提出了二级缓存在概念。
3.二级缓存:
二级缓存通常是第三方来实现,而我们使用时只需要对它进行配置即可。下面演示使用二级缓存的具体步骤。
>>步骤一,在主配置文件中指明支持使用二级缓存:
<property name="hibernate.cache.use_second_level_cache">true</property>
我们也可以不配置此属性,因为默认就是打开二级缓存。
>>步骤二、配置第三方缓存机制:
<property name="hibernate.cache.provider_class">
org.hibernate.cache.OSCacheProvider
</property> 由于我们这里选择了OSCacheProvider(它貌似也是hibernate官方开发得缓存机制)来提供缓存,所以还需要把它的缓存配置文件放在src目录下以使配置能被读到,这里即是把hibernate解压下的etc目录中的oscache.properties文件复制到src目录下。
>>步骤三、两种方式指定要缓存的实体类,一种是在主配置文件中配置(注意class是完整的类名):<class-cache class="com.asm.hibernate.domain.User" usage="read-only"/>
另一种是在实体配置文件(映射文件)配置:比如在User.hbm.xml 的class元素下配置如下内容:<cache usage="read-only"/> 关于usage属性值的说明:
read-only:如果你的应用程序只需读取一个持久化类的实例,而无需对其修改,那么就可以对其进行只读缓存。这是最简单,也是实用性最好的方法。
read-write: 如果应用程序需要更新数据,那么使用“读/写缓存”比较合适。 如果应用程序要求“序列化事务”的隔离级别(serializable transaction isolation level),那么就决不能使用这种缓存策略。
nonstrict-read-write: 如果应用程序只偶尔需要更新数据(也就是说,两个事务同时更新同一记录的情况很不常见),也不需要十分严格的事务隔离, 那么比较适合使用非严格读/写缓存策略。
transactional: Hibernate的事务缓存策略提供了全事务的缓存支持,例如对JBoss TreeCache的支持。这样的缓存只能用于JTA环境中,你必须指定为其hibernate.transaction.manager_lookup_class属性。
>>步骤四、测试二级缓存:现在仍用前面的类来测试,尽管第一个session关闭了,但是我们在第二个session查询时,仍不会连库,这也就是二级缓存的作用,通常情况下,hibernate查询时会首先在一级缓存中查询数据,再到二级缓存中查询,如果仍查不到才会连库。 这时请注意,尽管我们在一级缓存中清掉了数据,但是在二级缓存中还存有数据,所以在清掉数据后执行的查询操作也不会引起连库,这就是为什么我们最终只看到一条查询语句的原因。强调,前面说用evict或clear只是清掉一级缓存中的内容。
>>步骤五、感知二级缓存:经过上面的测试我们不能明确感知到二级缓存的作用效果,下面我们配置“统计信息”属性来进行二级缓存信息的获取。首先我们在主配置文件中配置以下属性:<property name="hibernate.generate_statistics">true</property>来打开统计信息,由于统计信息会耗资源,所以一般不打开。然后在测试类的main方法中增加如下代码:
Statistics st = HibernateUtil.getSessionFactory().getStatistics();
System.out.println(st);
System.out.println("put:" + st.getSecondLevelCachePutCount());
System.out.println("hit:" + st.getSecondLevelCacheHitCount());
System.out.println("miss:" + st.getSecondLevelCacheMissCount());
执行后结果为:
put:1
hit:2
miss:1 在进行代码结果分析前先来说两个概念:命中,miss。命中是指在二级缓存中查到数据,没有找到就称为miss. 命中率:在查询时有多少次是从缓存中得到。 下面我们看上面的执行结果put=1,说明hibernate放了一次数据到缓存中,这发生在第一次查询时,当不能在二级缓存中找到(这也是为什么会有一次miss的原因)时,会去连库并把数据放到缓存中去,使put变为1.随后进行的三次查找中:第一次仍是从一级缓存中查找到,后两次查找均在二级缓存中查到,所以命中hit=2。
4.二级缓存中的细节问题:
(1)体会save自动填充缓存,save填充缓存不支持id的native方式生成,所以我们先修改User的实体配置文件让id生成方式为:
<id name="id">
<generator class="hilo"/>
</id>后,这样修改后再来测试执行结果会发现执行结果为:
put:1
hit:3
miss:0 分析:当我们保存User对象到数据库时也会自动把此数据填充到缓存中,所以第一次put实质是发生在保存数据时。这样也就不难解释为什么hit=3,miss=0了。
(2)除了save外,update、saveOrUpdate、list、iterator、get、load(查询时从二级缓存中取数据的三个方法)、Query、Criteria都会填充二级缓存,且它们支持主键的nativa生成方式。
(3)让Query支持二级缓存:首先是主配置中配置如下属性:
<property name="#hibernate.cache.use_query_cache">true</property>因为Query命中率较低,所以默认此属性是关闭的。随后在Query方式查询时设置q.setCacheable(true);这两步执行后便完成了让Query支持二级缓存。
(4)怎样清除二级缓存:HibernateUtil.getSessionFactory().evict(User.class);这样将清除二级缓存中所有的User类相关的数据。
5.分步式缓存:
首先我们用图来模拟分步式缓存:
说明:在大型的web系统中,通常都会采用多个服务器来进行web服务,比如在上面的实例中,我们在服务器一存有“数据data”,在服务器二中也存有这个数据,但当我们在服务器N中更改这个数据时,如果我们继续访问在服务器一或二的数据,将不能得到正确的数据,这时采取的方式就是只要有服务器改变这个数据就在这些服务器组成的内网中广播这个信息来更新每个改变的数据。虽然服务器在内网中通讯,但是这种方式也是非常耗资源的,后来提出了“中央缓存”来解决此问题,如下图
原理:当我们去某个服务器查询数据时,这个服务器会去中央缓存查询,同样如果下面的某个服务器修改数据时,中央缓存也会及时把数据更新到库并重新保存新数据。但是如果数据交互快的话,我们仍不能保证数据这些服务器访问中央缓存时是及时数据。比如在服务器一访问中央缓存修改数据时,其它的几个服务器也能访问修改,这样就不能保证及时获取正确信息。所以使用缓存的条件有如下几点:读取大于写入;数据量不能超过内存容量;对数据要有独立的控制;允许无效的数据存在。
十一、事务
1.事务引发的三层架构MVC讨论:
在前面的程序代码中多次用到了事务:
Transaction tx = s.beginTransaction();
对应的jdbc代码:connection. setAutoCommit(false);
.....
.....数据处理
.....
tx.commit();
对应的jdbc代码:connection.commit();
tx.rollback();
对应的jdbc代码:connection.rollback();
以上的三处事务相关的代码统称为“事务边界”。其实hibernate的事务本质是依赖jdbc的事务来实现,其实对于事务而言,它是应该由业务逻辑层来实现,但是事务所依赖的对象又出现在Model层,所以想严格按照MVC的原理来实现将相当难(在前面我们所写的代码中事务的处理是不符合mvc模式得),但是利用推出的EJB,SPRING这些框架将能达到此目的,下图展示了三层架构,与原始的MVC相比,多了一个持久层。
2.模拟session context[OSIV]:
>>步骤一,重写工具类,核心代码如下:
package com.asm.hibernate.utils;
public class HibernateUtilOSV {
private static SessionFactory sf;
private static ThreadLocal session = new ThreadLocal();
private HibernateUtilOSV() {
}
public static Session getThreadLocalSession() {
Session s = (Session) session.get();
if (s == null) {
s = getSession();
session.set(s);
}
return s;
}
public static void closeSession() {
Session s = (Session) session.get();
if (s != null) {
s.close();
session.set(null);
}
}
static {
Configuration cf = new Configuration();
cf.configure();
sf = cf.buildSessionFactory();
}
public static SessionFactory getSessionFactory() {
return sf;
}
public static Session getSession() {
return sf.openSession();
}
}
>>步骤二、导入servlet包,使用Filter. 内容如下:
package com.asm.hibernate.osv;
public class OpenSessionViewFilter implements Filter {
public void destroy() {
// TODO Auto-generated method stub
}
public void doFilter(ServletRequest arg0, ServletResponse arg1,
FilterChain arg2) throws IOException, ServletException {
Session s = null;
Transaction tx = null;
try {
s = HibernateUtilOSV.getThreadLocalSession();
tx = s.beginTransaction();
arg2.doFilter(arg0, arg1);
tx.commit();
} catch (Exception e) {
if (tx != null)
tx.rollback();
} finally {
HibernateUtilOSV.closeSession();
}
}
public void init(FilterConfig arg0) throws ServletException {
// TODO Auto-generated method stub
}
}
>>步骤三、Userdao:
package com.asm.hibernate.osv;
public class UserDao {
static void addUseDao(User user) {
HibernateUtilOSV.getThreadLocalSession().save(user);
}
}
原理流程分析:当有被Filter监测的请求到达此Filter时,Filter会利用工具类来获得一个Session,并用这个session打开事务,然后再把请求传给下一个Filter.当所有操作执行完成后再返回到这个Filter,如果没有任何异常便可以提交事务,否则回滚事务。这样便可以有效保证事务的完整性,而且有利于懒加载,因为在整个请求的过程中,Session一直未关闭,所以在filter不断向下传递的过程中可以放心的使用session来完成懒加载,这也是解决懒加载的一种方式。其实OpenSessionInView主要解决了两个问题:一是实现事务在逻辑层控制,二是利于懒加载的实现。 下面再来看看工具类:此工具类主要完成了两个工作:一是生成一个线程级的Session,即是说在一个线程的事务开启到事务结束,一直用的是同一个Session,这样有利于保证事务的完整性,而工具类主要借助了ThradLocal来生成这个线程级的Session。 二是完成Session的关闭。
3.使用Hibernate自带的SessionContext
>>步骤一、在主配置文件中增加如下内容:
<property name="current_session_context_class">thread</property>以支持事务边界。 说明:它实质也是利用ThreadLocal来管理Session实现多个操作共享一个Session,避免反复获取Session,并控制事务边界。需要注意的是,当我们打开此功能后再通过SessionFactory.getCurrentSession()来获取一个Session,这个Session不能调用close方法,因为当我们提交事务或者回滚事务都会引起这个Session关闭,而通常的事务最会被提交或回滚,所以我们永远不要在这样的Session中调用close方法。同样的它的一个重要作用就是在生成页面时保持Session打开(即OpenSessionInView),因此为懒加载提供了方便。 除了配置为thread外还可以配置值为“jta” ,配置成jta后主要JTA事务管理器来管理事务,jta事务管理器主要用在有多个数据库的情况
>>步骤二、重写工具类:核心代码如下:
package com.asm.hibernate.utils;
public class HibernateUtilSelf {
private static SessionFactory sf;
private HibernateUtilSelf() {
}
public static Session getThreadLocalSession() {
Session s = sf.getCurrentSession();
if (s == null) {
s = getSession();
}
return s;
}
static {
Configuration cf = new Configuration();
cf.configure();
sf = cf.buildSessionFactory();
}
public static SessionFactory getSessionFactory() {
return sf;
}
public static Session getSession() {
return sf.openSession();
}
}
>>编写测试类:
package com.asm.hibernate.osv;
public class UserTest {
public static void main(String[] args) {
User user = new User();
user.setName("richie");
user.setDate(new Date());
addUser(user);
}
static void addUser(User user) {
Session s = null;
Transaction ts = null;
try {
s = HibernateUtilSelf.getThreadLocalSession();
ts = s.beginTransaction();
s.save(user);
ts.commit();
} catch (HibernateException e) {
if (ts != null)
ts.rollback();
throw e;
} finally {
if (s != null)
// s.close(); //注意这里不能使用close,并注意下面的打印结果
System.out.println("s=" + s);
}
}
}
执行后的结果为:s=SessionImpl(<closed>)
4.悲观锁:
悲观地认为每个事务在操纵数据时,肯定会有其它的事务同时访问该数据资源,当前事务为防止自身受到影响,先将相关数据资源锁定,直到事务结束后释放锁定。
实现方式一:利用LockMode实现数据锁;原理是使用带有LockMode参数的load方法来实现数据加锁,LockMode主要有以下几种锁定方式:
锁定方式 功能
LockMode.NONE Hibernate的默认值,首先到缓存中检索,检测不到时连数据库
LockMode.READ 不管缓存,直接到数据库中加载。如果在实体配置文件中配置了版本元素,则比较缓存中的版本与数据库中的版本是否一致
LockMode.UPGRADE 在READ的基础上,如果数据库支持悲观锁,则执行select...for update,否则执行变通的select语句。
UPGRADE_NOWAIT 主要是针对Oracle数据库,在UPGRADE的基础上,如果执行select语句不能获得锁,则会抛出锁定异常
实现方式二、通过逻辑实现锁定数据;原理是在实体类的数据库表中设定一个boolean的字段,当这个字段为true,表示该数据正在被访问,此时记录被锁定。如果为false,则表示可以访问。 过程:(1)当访问的数据为false时,表示可以访问,此时修改字段为true,以避免其它对象访问,操作完成后再设此字段为false,以让等待的对象访问 (2)当访问数据据为true,表示不可被访问。
5.乐观锁:
乐观地认为每个事务在操纵数据时,很少有或者不会其它事务同时访问数据资源,因而不会在数据库层锁定,而是通过逻辑来实现版本控制。
实现方式一:<version>方式:为了方便操作新建一个名为OptimizeLock的项目.
>>步骤一,拷贝以前的User类及相应的实体配置文件等相关文件。
>>步骤二,在User类中增加private int version;及相应的get/set方法。Version名字可以随意取。
>>步骤三、修改配置文件:增加<version name="version"></version>属性,并注意在<class>元素中使用optimistic-lock="version"属性,这也是官方推荐使用的属性值。但不能使用none,因为它不支持乐观锁,也可这样说,即使我们为乐观锁做好了一切准备,但我们不想在某个实体对象中使用时,只需要配置optimistic-lock为none即可。
>>步骤四、编写测试类
package com.asm.hibernate.test;
public class VersionUserTest {
public static void main(String[] args) {
addUser();
update(1);
}
static void update(int id) {
Session s1 = null;
Session s2 = null;
Transaction tx1 = null;
Transaction tx2 = null;
try {
s1 = HibernateUtil.getSession();
tx1 = s1.beginTransaction();
User user1 = (User) s1.get(User.class, id);
System.out.println(user1.getName());
s2 = HibernateUtil.getSession();
tx2 = s2.beginTransaction();
User user2 = (User) s2.get(User.class, id);
user1.setName("user1Name");
user2.setName("user2Name");
tx1.commit();
tx2.commit();
} catch (HibernateException e) {
....
} finally {
....
}
}
static void addUser() {
...主要作用是增加一条记录以实现操作。
}
}
分析:在进行分析前,先提下笔者曾范的一个测试错误,由于是复制以前的代码,由于以前在User的配置文件中配置了二级缓存的“<cache usage="read-only"/>” ,这样执行时总是报“Can't write to a readonly object” 。由这点大家可以再细想下悲观锁的更多细节问题。下面来谈version,当我们向数据表中添加一个version字段,hibernate会控制此字段,当访问一条记录时,同时访问此记录的版本信息,如果对该记录做了修改,在提交事务时,hibernate会将访问时的版本信息与数据库中的版本信息进行对比,如果一致则提交修改并将原版本信息加1,否则撤销本次修改。 上面的两个session并分别有开启了两个事务,当提交第一个事务时,符合要求进行了提交。但我们再来提交第二个事务时,由于第一个事务修改了版本信息,尽管user1和user2最初获得的版本是一样得,但由于第一个事务提交修改了数据库中版本信息,所以第二个事务将不能被提交(如不能明白,请仔细阅读本段中的黑体字部份)。 请这样测试:执行完后(会报“Transaction not successfully started”异常)查数据表,然后再交换以上两个事务提交的顺序再执行,再查数据表。 另请不要深究异常处理。
实现方式二、<timestamp>
基本和方式一相同,只是在User类中增加一个Date类型的属性,同时在实体配置文件配置如下内容: <timestamp name="实体类的中Date类型的属性"></timestamp>
十二、一些细节问题分析
1.实体配置文件中的类型问题:
在前面的多个实例中配置<property>映射类型时,我们都采取的是hibernate默认的配置,即是说没有在<property>中配置type属性:其实基本类型一般不需要映射文件中配置,只有在一个java类型与多个数据库类型相对应时,交且我们不希望使用默认的配置才会用type来指明类型。 举例:java.util.Date与数据库中的DATE,TIME,DATETIME,TIMESTAMP相对应,如果我们不希望映射成默认的DATATIME,而想映成DATE,便配置成type=“DATE”,另需要说明的是我们也可配置成我们自定义的类型,但前提是我们自定义的类型必须实现org.hibernate.UserType或org.hibernate.CompositeUserType中的任何一个接口。且在配置时指定类的完整名。
2.Session与SessionFactory:
Session是非线程安全的,生命周期较短,代表一个数据库的连接,在B/S系统中一般不会超过一个请求,内部维护一级缓存和数据库连接。通常一个Session的生成也就意味着即将结束。
SessionFactory是线程安全的,一个数据库对应一个SessionFactoy,一般会在整个系统生命周期内有效:SessionFactory保存着和数据库连接相关的信息(user,password,url)和映射信息,以及hibernate运行时用到的一些信息。
3.Session中的flush方法:
此方法的作用是将一级缓存与数据库同步,通常在查询数据或者是提交前(可能会采取批量更新的方式到数据库)会实现同步,这样可有效保证一级缓存中的数据是有效的数据。这个方法是由hibernate自身来维护调用的,因为它的调用意味着与数据库交互,所以不建议我们手工调用,而事实上hibernate对它的调用主要是为了提升性能和保证数据的有效性且hibernate自身的对它的维护调用已近完美。
4.巧用flush解决内存溢出问题:
当我们进行类似如下的操作时:for (int i = 0; i < 10000000; i++) s.save(obj);当i足大到我们的内存不足以承受时,便会出现内存溢出问题,对于此可以这样解决:
for (int i = 0; i < 10000000; i++) {
s.save(obj);
if (i % 30 == 0) {
s.flush();
s.clear();
}
} 分析:当每保存30个数据时,我们便清掉一级缓存中的内容,但是在清掉内容前我们必须让一级缓存中的数据与数据库进行一次交互,这样可以保证数据能被保存到数据库中。 试想下,如果不flush,那么在一级缓存中的数据是不能同步到数据库中的,所以这里flush是非常重要得。 其实只要再来回忆前面所说的一级缓存中的一些知识,便可以发现如果保存的对象的主键是以increme的方式生成的,可以不用flush也能实现数据及时同步到数据库中。
5.用StatelessSession接口解决批量更新问题:
在进行如上的批量操作时,我们通过flush来解决和数据库的同步更新,但是这样会造成缓存的不断更新,因此在进行类似的批量操作时,我们通常会选择使用StatelessSession接口来代替Session,由于这种无状态的Session具有以下特点:它不和一级缓存、二级缓存交互,也不触发任何事件,监听器,拦截器,通过该接口能把这些批量更新直接发送到数据库。说明:StatelessSession的方法也Session的方法相似。 除了此方法外,我们使用Query.executeUpdate(),也可以执行批量更新,但是此方法会清除相关联的类的二级缓存,也可能造成级联,甚至是和乐观锁不相容。
6.离线查询DetachedCriteria:
package com.asm.hibernate.test;
public class DetachedCriteriaTest {
public static void main(String[] args) {
add();
DetachedCriteria dec = DetachedCriteria.forClass(User.class);
String name = "jack";
dec.add(Restrictions.eq("name", name));
List<User> list = detaCri(dec);
for (User u : list)
System.out.println(u);
}
static List detaCri(DetachedCriteria dec) {
Session s = HibernateUtil.getSession();
Criteria cr = dec.getExecutableCriteria(s);
List list = cr.list();
s.close();
return list;
}
static void add() {
...省略内容:主要作用是在数据库中插入两条名为“jack”的记录,以使查询操作可以进行。
}
}
分析:为会么要使用这种离线查询,比如我们可以在业务逻辑层构造出一个DetachedCriteria对象,且它不会依赖于Session,这样当们把这个对象作为参数来传递给M层的方法时便可以实现连库查询。这样可以有效的使用Session,避免与数据库的频繁交互。
7.监听器的使用:
下面首通过一个简单的例子来说明此问题:
>>步骤一、编写监听类:
package com.asm.hibernate.event;
public class SaveUserListener implements SaveOrUpdateEventListener {
public void onSaveOrUpdate(SaveOrUpdateEvent event)
throws HibernateException {
if (event.getObject() instanceof com.asm.hibernate.domain.User) {
User user = (User) event.getObject();
System.out.println("find save User:" + user.getName());
user.setDate(new Date());
}
}
}
分析:当有保存/更新事件发生时,便被此监听类监听到,而我们处理的只是保存/更新User对象,即是说当发生保存/更新一个User对象时,才会被这个监听器监听操作;其它对象的保存/更新操作尽管也会被它监听,但不会有处理动作。
>>步骤二、编写测试类:
package com.asm.hibernate.event;
public class TestListener {
public static void main(String[] args) {
addUser();
}
static void addUser() {
Session s = null;
Transaction tx = null;
try {
User u = new User();
u.setName("jack");
s = HibernateUtil.getSession();
tx = s.beginTransaction();
s.save(u);
tx.commit();
} finally {
if (s != null)
s.close();
}
}
} 分析:执行后,发现没有任何作用。原因是未配置监听器,联想到gui程序监听时,总会在程序中给出相应的代码,而这里我们只需要在主配置文件中配置这个监听器,在由hibernate把这个监听器写成代码到程序中去。在主配置文件中配置的内容如下:
<event type="save">
<listener class="com.asm.hibernate.event.SaveUserListener" />
</event>
这样配置后,尽管监听器类中的方法能实现,但是发现这些数据没有保存到数据库中。因为当们配置一监听器时,相对应的默认监听器就失效了,即是说org.hibernate.event.def.DefaultSaveOrUpdateEventListener这个默认的监听器不起作用了,为了能继续使用默认的监听器,应再配置上这个监听器,即配置成这样:
<event type="save">
<listener class="com.asm.hibernate.event.SaveUserListener" />
<listener class="org.hibernate.event.def.DefaultSaveOrUpdateEventListener" />
</event>
这样配置后,即可以让我们自己的监听器生效,又可以让默认监听器起作用。
8.使用原始的sql查询:
代码如下:
package com.asm.hibernate.test;
public class JdbcSqlSelectTest {
public static void main(String[] args) {
addUsers();
query();
}
static void query() {
Session s = HibernateUtil.getSession();
Query q = s.createSQLQuery("select * from user").addEntity(User.class);
List<User> rs = q.list();
for (User u : rs) {
System.out.println("Result:"+u.getName() + "---" + u.getDate());
}
}
static void addUsers() {
...增加一些记录,以检验查询
}
} 分析:关键是看查询方法:中间用到的查询语句from user,user是指表名,后面增加addEntity方法的主要是了了保证返回的list对象中保存的User,这样方便foreach遍历。 需要说明的是尽管hibernate支持这种sql查询,但是它不具有通用性,即是说我们在写这些查询语句时总会依赖于一个具体的数据库,一旦换了数据库就可能出现问题,因此最好不要使用这种JdbcSql查询。
9.命名查询:
>>步骤一:在User的实体配置文件</hibernate-mapping>的元素下增加如下内容:
<query name="selectUserbyId">
<![CDATA[from User where id=:id]]>
</query>
>>步骤二、编写测试:
static List namedQuery(int id) {
Session s = HibernateUtil.getSession();
Query q = s.getNamedQuery("selectUserbyId");
//Query q=s.getNamedQuery("com.asm.hibernate.domain.User.selectUserbyIdTheSecond");
q.setInteger("id", id);
return q.list();
}
说明:这种方式虽然简单,但是容易在整个系统中引起重名,所以最好的方式是在Use的实体配置文件的<class>元素下配置这个属性,比如我们在<class>元素的最后增加如下内容:
<query name="selectUserbyIdTheSecond">
<![CDATA[from User where id=:id]]>
</query> 则在上面的代码中要想用到这个命名查询,则应写成注释掉的代码那样。如果项目中的命名查询不多,建议写在</hibernate-mapping>元素下,这样引用较方便。
补充说明:我们也可以配置SQl的命名查询,步骤是先在</hibernate-mapping>元素下配置如下内容:
<sql-query name="selectUserSql">
<![CDATA[select * from user]]>
</sql-query> 然后执行下面的代码:
static List namedSqlQuery(int id) {
Session s = HibernateUtil.getSession();
Query q = s.getNamedQuery("selectUserSql");
return q.list();
}
10.N+1查询:
N+1查询:由于缓存,或者lazyload(懒加载)等原因,在查询n个结果,可能要执行n+1次查询数据库操作。 在Hibernate的N+1查询主要表现在:(1)使用iterate查询的时候 (2)查询子对象的时候。其实N+1曾引发了关于效率上的讨论,因此建议到网上查阅有关的资料学习。
11.HQL 、QBC:
在前面曾应用过HQL,QBC,所以对于它们的进一步学习,应该很容易上手,所以同样建议参看官方手册或者查阅其它相关资料进行更深入的学习。
十三、总结
1.主配置与实体(映射)配置:
关于这些配置,可以从网上搜到很多,当然最好的方式是从官方获取hibernate的参考手册,来细细研读这些配置,其实许多常用的配置在前面已基本有所提及,所以再来查看hibernate手册时会比较容易,至于一些不常用的配置,建议只做了解,在真正使用时再去细看。
2.使用最佳实践:
在官方推出的手册中,给出了最佳实践,其中提到了许多思想和建议。我们应该在使用hibernate的过程中尽量按官方的建议来实现,以养成好的习惯。