Hibernate上手指南
本文着重讲述了为什么要使用Hibernate,此外也简单的介绍了如何使用Hibernate,以及Hibernate中的一些基本概念。借这篇文章来向还没有接触过Hibernate的开发者推荐款优秀的开源ORM产品。
一、WhyHibernate?
现在流行“测试驱动开发”,相似的我觉得“目的驱动学习”是一种比较好的接受新技术,新知识的途径。在学习一样新的技术之前,首先得明确到底有没有必要学习,已有的技术是否已经工作的很好,学习这个新的技术是为了解决什么问题。如果您明确了以上问题,那么寻找并学习新的技术将会事半功倍,并且能快速应用到实际的开发当中来提高效益。
要说Hibernate,就得先介绍一下Object/RelationMapper(ORM),中文翻译为对象关系映射。之所以会产生这样的概念是源于目前软件开发中的一些不协调的思想。目前流行的编程模型是OOP(ObjectOrientedProgramming),面向对象的编程,而目前流行的数据库模型是RelationalDatabase,这两者思考的方式不一样,这必然产生了开发过程中的不协调。ORM框架(也称为持久层框架,)的出现就是为了解决这样的问题,屏蔽底层数据库的操作,以面向对象的方式提供给开发者操作数据库中数据的接口。目前流行的ORM框架有 ApachOJB,Hibernate,iBatis等等,当然最完善,最好用的是Hibernate,至少我这样认为。或许您对“持久层”感到迷惑,其实说白了很简单,把数据放到数据库中叫做持久化(内存种的数据当然不是持久的),那么负责这一操作的结构层面就叫做持久层。您以前应该听说过表现层,业务层,数据层,那么持久层是在业务层和数据层之间的一层,或者说持久层是数据层的一部分。
接下来,我想通过一个实际开发中的例子来说明ORM带给我们的好处。先来讲一下我们的需求,数据库中有三张表,一张student,一张course,另外一张course_slection。其中student用来保存学生信息,course用来表示课程信息,course_selection用来表示学生的选课信息。(表的详细结构这里我就省略了,因为这并不重要)现在要求编写一个程序,用来选出指定学号学生所选的课程名字,那么可能会出现以下几种程序编写的方式:
1.菜鸟级
代码片段1:
publicListselectCourses(StringstudentId)
{
Connectioncon=null;
Statementsta=null;
try
{
Class.forName("oracle.jdbc.driver.OracleDriver");
con=DriverManager.getConnection(
"jdbc:oracle:thin:@10.85.33.199:1521:glee",
"test","test");
Stringsql="select*fromcourse_selection";
Stringsql2="selectnamefromcoursewhereid='";
sta=con.createStatement();
ResultSetrs=sta.executeQuery(sql);
Listlist=newLinkedList();
while(rs.next())
{
ResultSetrs2=sta.executeQuery(sql2+
rs.getString("course_id")+"'");
if(rs2.next())
{
list.add(rs2.getString("name"));
}
}
returnlist;
}
catch(Exceptione)
{
e.printStackTrace();
}
returnnull;
}
这段程序您一定看的很晕吧,什么乱七八糟的都搞在一起,那么接下来看一段改进过的程序。
2.改进后的代码
代码片段2:
classDBHelper
{
publicstaticConnectiongetConnection()
{
try
{
Class.forName(Constants.DB_DRIVER);
returnDriverManager.getConnection(Constants.DB_URL,
Constants.DB_USER,Constants.DB_PWD);
}
catch(Exceptione)
{
e.printStackTrace();
}
returnnull;
}
}
publicListselectCourses(StringstudentId)
{
Connectioncon=null;
Statementsta=null;
try
{
con=DBHelper.getConnection();
Stringsql="select*fromcourse_selection";
Stringsql2="selectnamefromcoursewhereid='";
sta=con.createStatement();
ResultSetrs=sta.executeQuery(sql);
Listlist=newLinkedList();
while(rs.next())
{
ResultSetrs2=sta.executeQuery(sql2+rs.getString("course_id")+"'");
if(rs2.next())
{
list.add(rs2.getString("name"));
}
}
returnlist;
}
catch(Exceptione)
{
e.printStackTrace();
}
returnnull;
}
这段代码的形式是一种被广泛采用的形式,相对第一段代码来说,应该已经有所进步,分离了数据库连接操作,并把数据库连接信息交给单独的类完成(一般放在配置文件里面),往往在开发中还会引入数据库连接池(ConnectionPool)来提高性能,我这里都尽量简化了。但这些并不能从根本上改善程序的结构,在业务代码中仍然混杂了很多数据库操作,结构不清晰。下面来看一段彻底分离数据库操作的代码:
3.DAO模式
代码片段3:
publicListselectCourses(StringstudentId)
{
StudentDAOsd=newStudentDAO();
Studentstudent=sd.findById(studentId);
Setset=student.getCourseSelections();
ListcourseNames=newLinkedList();
for(Iteratoriter=set.iterator();iter.hasNext();)
{
CourseSelectionelement=(CourseSelection)iter.next();
courseNames.add(element.getCourse()。getName());
}
returncourseNames;
}
是不是感觉代码少了很多?或许您对这段代码有点迷惑,没关系,后文会详细解释。我想先解释一下DAO。其实DAO和Hibernate没有必然联系,只不过一般用Hibernate的程序都用DAO模式。DAO的全称是DataAccessObject,程序要访问数据库中的数据(包括获取,更新,删除)都通过DAO来访问,实际上DAO才是真正屏蔽了所有数据库操作的东西,这样在业务代码中就可以完全隔离数据层的代码。如果我告诉您,在真正用 Hibernate开发的时候,要完成上文提到的功能,需要手写的代码就是“代码片段3”这么多,甚至更少,您是不是有很大的动力去学习 Hibernate?那么好吧,让我们开始Hibernate之旅。
二、持久层的组成
这一节的名字应该换成“基于Hibernate的持久层的组成”更合适一点,可是它太长了。既然Hibernate是用来开发持久层,那么我先介绍一下这个持久层中的各个元素。
1.POJO:PlainOldJavaObject,您可以把它看作是简单的JavaBean。一般说来,一张数据库表对应一个POJO,也就是对象/关系的一一映射。
2.DAO:对于每一个POJO,一般都有一个DAO与之对应,承担所有关于该POJO的访问控制。实际上也就是控制了对数据库中一张表的访问控制。
3.*.hbm.xml文件:这个文件定义了POJO和数据库中的表是如何映射的,比如POJO中的字段对应数据库表中的哪个字段等等。一般每个映射都用单独的文件来描述,也就是有一个POJO就有一个*.hbm.xml文件。
4.*.cfg.xml文件:这个文件定义了Hibernate的基本信息,比如数据库驱动,用户名,密码等等连接信息,也包括了所有要用的*.hbm.xml文件,在初始化的时候,Hibernate会读取这个文件来找相应的映射文件完成对象/关系。
我们还是以上文的例子来详细描述一下这里提到的各个元素的内容。
1.Student.java:
代码片段4:
publicclassStudentimplementsjava.io.Serializable
{
privateStringid;
privateStringname;
privateSetcourseSelections=newHashSet(0);
publicStudent()
{
}
publicStringgetId()
{
returnthis.id;
}
publicvoidsetId(Stringid)
{
this.id=id;
}
publicStringgetName()
{
returnthis.name;
}
publicvoidsetName(Stringname)
{
this.name=name;
}
publicSetgetCourseSelections()
{
returnthis.courseSelections;
}
publicvoidsetCourseSelections(SetcourseSelections)
{
this.courseSelections=courseSelections;
}
}
这个类就是一个POJO,您可以很明显的看出来它就是一个JavaBean。我想解释它的courseSelection字段。很显然,在数据库表 student中,没有这个字段。这里的这个字段是因为一个外键引用,course_selection的student_id是一个外键,引用了 student表中的id字段。那么在Student类中courseSelection来记录这样的外键关系,也就是说,当我们获取了Student对象以后,就可以直接获取他的选课记录,这样就为上层的调用提供了很大的方便。这里有点模糊没关系,我在介绍映射定义文件(*.hbm.xml)的时候还会提到这个问题。
2.StudentDAO.java
代码片段5:
publicclassStudentDAO
{
Sessionsession;
publicStudentDAO()
{
Configurationcfg=newConfiguration();
cfg.configure("/hibernate.cfg.xml");
SessionFactorysessionFactory=cfg.buildSessionFactory();
session=sessionFactory.openSession();
}
publicvoidsave(StudenttransientInstance)
{
session.save(transientInstance);
}
publicvoiddelete(StudentpersistentInstance)
{
session.delete(persistentInstance);
}
publicStudentfindById(java.lang.Stringid)
{
Listlist=session.createCriteria(Student.class)。add(
Expression.eq("id",id))。list();
if(list.size()>0)
{
return(Student)list.get(0);
}
returnnull;
}
}
这里的构造函数是用来启动Hibernate,并获取session。打开一个session就相当于打开了一个数据库连接,然后我们就可以对这个 session进行操作,完成数据库操作,完全不用写SQL语句。我这里Hibernate的启动方式写的很不规范,系统应该只需要完成一次 Hibernate启动就可以在不同的DAO中使用,我把它写在构造函数里面纯粹是为了简化演示代码。
您可以看到save和delete方法都很简单直接对对象操作,而findById就有些麻烦,因为这里有一个查询过程在里面。Hibernate里面查询可以用Criteria这个类来完成,我们也常用Hibernate独有的HQL(HibernateQueryLanguage)来完成查询。当然Hibernate也是支持原生SQL的。关于查询的详细信息请参考其他文章或书籍,我只是演示一个流程,介绍一些概念。
3.Student.hbm.xml
代码片段6:
<hibernate-mapping>
<classname="Student"table="STUDENT">
<idname="id"type="string">
<columnname="ID"length="10"/>
<generatorclass="assigned"/>
</id>
<propertyname="name"type="string">
<columnname="NAME"not-null="true"/>
</property>
<setname="courseSelections"inverse="true">
<key>
<columnname="STUDENT_ID"length="10"
not-null="true"/>
</key>
<one-to-manyclass="CourseSelection"/>
</set>
</class>
</hibernate-mapping>
这个文件定义了Student类和Student表是如何映射的。class元素定义了Sudent类和STUDENT表映射,然后就定义了各个属性是如何映射的。如果一个属性是数据库的key,那么会用id标签来定义,column定义了当前类的属性和数据库中的哪个字段对应,generator是 id特有的。一般来说id是自增的,由于我的数据库是用的Oracle,它没有自增字段,要实现自增必须用Sequence,这超出了本文的范围,所以我就用assigned来简化示例代码。assigned表示id是用户给定的。
有一个比较特别的标签是set,它对应着数据库中的外键关系,上文我提到的通过Student对象可以获得所有相关的选课记录就是通过这里的定义实现的。name属性对应了Student类中的字段名,key表示哪个字段是外键,one-to-many表示Student和 CourseSelection是一对多关系,这和事实相符。类似的还有many-to-one,many-to-many,不过这些都不常用,我不介绍了。Hibernate根据这个映射定义文件,在实例化一个POJO(比如Student)的时候,会自动的把定义过映射的属性用数据库中的数据填充,set也包括在内。
4.hibernate.cfg.xml
代码片段7:
<hibernate-configuration>
<session-factory>
<propertyname="connection.username">test</property>
<propertyname="connection.url">
jdbc:oracle:thin:@10.85.33.199:1521:glee</property>
<propertyname="dialect">
org.hibernate.dialect.Oracle9Dialect</property>
<propertyname="connection.password">test</property>
<propertyname="connection.driver_class">
oracle.jdbc.OracleDriver</property>
<mappingresource="Student.hbm.xml"></mapping>
<mappingresource="CourseSelection.hbm.xml"></mapping>
<mappingresource="Course.hbm.xml"></mapping>
</session-factory>
</hibernate-configuration>
这个文件我不解释了,自己看吧。结合上文StudentDAO的例子,我想您应该能看明白。
看了这么多,或许您会有点头皮发麻,POJO,DAO,配置文件…好像要写的东西还是很多。值得庆幸的是现在Hibernate已经发展的比较成熟了,有很多工具来帮助我们完成这些工作,比如MiddleGen,HibernateSynchronizer等等。我使用的开发工具是 Eclipse+MyEclipse,我所要做的只是把数据库表建好,然后MyEclipse提供的工具会自动根据数据库表生成 POJO,DAO,*.hbm.xml,甚至hibernate.cfg.xml都是自动完成的(前提是MyEclipse知道您的数据库连接信息)。我并不打算介绍如何用IDE来开发Hibernate,您可以参考IDE的帮助文档。
到这里为止,使用Hibernate进行开发的基本组成元素我都介绍好了,强烈建议您马上实践一遍,即使有些不理解,也先依葫芦画瓢一个。对了,别忘了把Hibernate的包down下来放到classpath里面。
三、Session与SessionFactory
Session可以说是Hibernate的核心,Hibernate对外暴露的接口就是Session。所以我这里讲一下有关Session的常用函数和特性。
在讲Session之前,我想先提一下SessionFactory,这个东西不复杂,只要配置好就行了。顾名思义,SessionFactory就是用来创建Session的。SessionFactory是线程安全的,也就是说对于同一个数据库的所有操作共享一个SessionFactory就行了。回头看代码片段5,我们可以看到SessionFactory的常用配置方式。
代码片段8:
Configurationcfg=newConfiguration();
cfg.configure("/hibernate.cfg.xml");
SessionFactorysessionFactory=cfg.buildSessionFactory();
我们通过Configuration来读取配置文件,然后就可以创建SessionFactory,这段代码在所有系统中都大同小异,一般就是xml配置文件的名字不一样,所以也没什么好说的。
当我们有了SessionFactory以后就可以获取Session了。调用SessionFactory.openSession()就会返回一个 Session实例,然后我们操作这个Session来访问数据库。值得一提的是Session并不是线程安全的,也就是每一个线程都必须有自己的 Session。所以我们一般通过以下方法来获取和关闭Session:
代码片段9:
publicstaticSessioncurrentSession()throwsHibernateException
{
Sessionsession=(Session)threadLocal.get();
if(session==null||!session.isOpen())
{
if(sessionFactory==null)
{
try
{
cfg.configure(CONFIG_FILE_LOCATION);
sessionFactory=cfg.buildSessionFactory();
}
catch(Exceptione)
{
e.printStackTrace();
}
}
session=(sessionFactory!=null)?
sessionFactory.openSession():null;
threadLocal.set(session);
}
returnsession;
}
publicstaticvoidcloseSession()throwsHibernateException
{
Sessionsession=(Session)threadLocal.get();
threadLocal.set(null);
if(session!=null)
{
session.close();
}
}
可以看到,我们通过threadLocal来保存每个线程的session,这样就保证了各个线程之间的互不干扰,也保证了系统只有一个 SessionFactory实例(对于大多数应用来说已经足够了)。如果您使用MyEclipse进行开发的话,它会自动生成一个 HibernateSessionFactory.java,其中就包含了以上代码。
好了,现在我们已经获得了Session,下面我来介绍以下Session的常用函数,这些函数都有很多重载函数,我只介绍以下大概是干嘛的,不一一解释,详细信息您可以查看Hibernate的API。
1.Session.get(),获取某个类的实例,一般都是通过id来获取比如
Session.get(Student.class,"0361095");
这句话的意思就是获取id(primarykey)为“0361095”的Student对象。这里要注意的是第二个参数必须是Object,也就是说,如果是long类型的1,那么必须转换成newLong(1)再传入。
2.Session.load(),用法和意义都和get一样,不过它们还是有点区别,我稍后解释。
3.Session.save(),将某个实例保存到数据库中去(往往在数据库中形成一条新的记录)。
4.Session.update(),更新某个实例,这个实例必须和数据库中有对应,否则会报错。
5.Session.delete(),删除某个实例,也就是删除这个实例对应的数据表中的数据。
6.Session.saveOrUpdate(),保存或者更新某个实例,调用这个方法您就不用去关心到底是save还是update了,它会自己判断,然后调用相应的函数。其实save和update涉及到实体对象生命周期中的三种状态,这个比较重要,我在后面会单独讲的。
对于get和load的区别,很难讲清楚,这里涉及到Hibernate的缓存机制,是一个非常复杂的话题,我不打算深入讨论这个内容。简单的来讲,Session实现了Hibernate的一级缓存,SessionFactory实现了Hibernate的二级缓存。load方法会先查找一级缓存再查找二级缓存,最后再去数据库中找,而get只会查找一级缓存,然后就去数据库中找了。这只是是get和load的一个区别,另外的区别如果您有兴趣的话自己去google吧。关于Hibernate的缓存机制,如果您只是一般用用Hibernate的话没有必要深入研究,就当它不存在好了,get和 load到底用哪个也不用非常讲究,如果您用工具生成DAO的话,生成的代码用什么就用什么吧。
四、关键概念的理解
在使用Hibernate进行开发的时候有几个概念在我看来是必须理解的,否则在开发的时候会遇到很多问题而摸不着头脑。
1.Lazyloading,懒加载(延迟加载)
这个技术在很多地方被应用,比如Eclipse的插件管理也是用的延迟加载。在Hibernate中,所谓延迟加载就是返回一个POJO但是某些数据(往往是实体类型或者Set类型)并没有被真正的被填充,直到POJO的某个字段真正被引用的时候才从数据库中读取相应的数据来填充POJO中的字段。这样就能有效的避免很多不必要的数据库操作,因为POJO的有些数据我们并不需要,而且数据库操作是很费时间的。在Hibernate2中,默认是非延迟加载的,而在Hibernate3中,默认就是延迟加载了。
如果使用了延迟加载,那么在读取数据的时候有一个问题必须注意,那就是在数据真正被加载之前,Session不能被关闭。您可以回头看一下代码片段5,我在构造函数里面open了session以后就没有关闭这个session,所以我在使用的时候没有什么问题,但这样总占着数据库连接也不好,用好了应该及时关闭给别人用。我上文给的例子中没有关闭Session的代码,要加的话给DAO加一个方法调用Session.close(),然后在代码片段1 中的return之前调用这个方法就行了。
Hibernate的延迟加载机制远不是这么简单,但是普通的应用没有必要去深究这些东西,了解这么多就够了。
2.Objectlifecycle,对象生命周期
在Hibernate中,对象分为三种状态,Transient(自由状态)、Persistent(持久状态),Detached(游离状态),下面我分别解释一下。
1、自由状态:所谓自由状态就是说这个对象是自由的,与Hibernate无关,比如:
Studentstudent=newStudent();
student.setId("0361095");
这里的student就是一个普通的对象与hibernate无关,称为自由状态。
2、持久状态:所谓持久状态就是指对象和数据库中的数据(持久状态的数据)有关联,也就是说对象被Hibernate所管理了,比如:
session.save(student);
这样student对象就从自由状态变为持久状态了。持久状态的对象在Session与数据库中的数据进行同步时(比如commit)会把数据更新到数据库。而其他状态的则不会。我觉得可以这样来理解持久状态,可以看成Hibernate也拥有一份对象的引用,那么如果您对持久状态对象的属性进行更改的话,Hibernate看到的对象的状态也更改了,而Hibernate所看到的对象和数据库中的数据是等价的。也正是这样,Hibernate才实现了 Object/Relation的映射。类似的,load和get方法也一样会获取Persistent状态的对象。
3、游离状态:每次调用Session.close以后,所有跟这个session有关的处于
Persistant的对象就变成了游离状态。也许您要问Detached和Transient有什么区别。其实从我给的例子来说看不出什么区别,因为我这里ID是给定的,而真正开发的时候ID往往是自增的,那么Transient的对象是没有ID的,当save了以后就有了,显而易见Detached 的对象也是有ID,只不过这个对象已经和Hibernate脱离了关系。但是游离状态的对象仍然和数据库中的记录有一定联系,至少游离状态的对象知道数据库中有条记录的ID为xxx。从这一点上来讲,游离状态是可以自己创造出来的,只要您知道数据库中的主键信息。
在使用Hibernate开发的时候要分清楚这三种状态,否则很容易出错。比如不能去save一个游离状态的对象,不能去update一个自由状态的对象等等。
五、结束语
这篇文章不是详细介绍Hibernate,只是总结了一些学习和使用Hibernate的一些感受和经验,希望能给没有用过Hibernate的开发者一个上手的指引。