最近工作的项目中,用的 ORM 技术是 Hibernate,学习了一下它的用法,正好 PyCon 上我有一个演讲主题是介绍 Django 的 ORM,可以拿来比较一下。这篇文章介绍了 Hibernate 的定位,基本的概念,以及用代码演示了如何使用 Hibernate。本文的内容参考了 jboss 上的一篇教程 ,所有的代码直接下载: hibernate-tutorials.zip
Java 是一个面向对象的编程语言,数据库提供的数据结构只有 Table。所以我们在读写数据库的数据的时候不可避免的要进行结构的转换,保存数据的时候,将 Object 变成 Table 可以保存的形式,读回数据的时候要转换回来。
这样每次在读写数据的时候做转换,是非常重复的,开发成本很好。Hibernate 就是一个 对象/关系映射 的转换解决方案。在 Java 程序中,我们只写类,数据在保存的时候,Hibernate 负责将类的属性转换成 Table 的 Column。更确切的说,如果没有 Hibernate,我们就需要通过写 SQL 和用 JDBC 来跟数据库交互。
和其他 ORM 不同,Hibernate 没有完全屏蔽 SQL。并承诺你的关系型数据库的知识,在 Hibernate 下依然有价值( 来源 )。
ORM 需要做的最终要的一件事情是负责 Class 和 Table 的数据结构的转换,在 Hibernate 中,定义这种转换有两种方式:通过配置文件,或者通过注解。
Bundle/resources 下面的 hibernate.cfg.xml
是 Hibernate 的配置文件。有数据库地址,连接参数等配置。其中 dialect
定义了使用哪种SQL方言,在使用特定数据库的时候可以使用。
auto 可以指定启动的时候自动创建表结构:
<!-- Drop and re-create the database schema on startup --> <property name="hbm2ddl.auto">create</property>
这个值可选的参数如下:
<mapping />
可以告诉 Hibernate,去哪里找描述 Class 和 Table 对应的 mapping 文件。
<mapping resource="org/hibernate/tutorial/hbm/Event.hbm.xml"/>
对应路径的 mapping 文件,描述了 Java 的 Class 如何和数据库的 Table 对应:
<hibernate-mapping package="org.hibernate.tutorial.hbm"> <class name="Event" table="EVENTS"> <id name="id" column="EVENT_ID"> <generator class="increment"/> </id> <property name="date" type="timestamp" column="EVENT_DATE"/> <property name="title"/> </class> </hibernate-mapping>
Hibernate 会使用 java.lang.ClassLoader
去加载这些类。
有关 Entity 有两点需要注意:
父节点的属性中, name
结合 package
定义个 FQN,对应 java 的 class; table
定义了数据库的表名字;
子节点中:
<id/>
Element 来告诉 Hibernate 如何找到表中的唯一的一个 row;推荐使用 Primary key <property/>
定义了属性和表字段的 mapping, column
定义了表 Column 的名字,如果不写的话 Hibernate 默认会使用 property 的 name; type
是数据类型。这个数据类型既不是 SQL 的类型,也不是 Java 的类型,而是 Hibernate 的类型,负责在 Java 和 SQL 之间做转换。如果这个 type 没写的话,Hibernate 会尝试通过 Java 的反射,按照 Java 的类型找到对应的 mapping 类型; 示例代码如下( 不要复制粘贴,如果需要运行请直接复制本文开头提供的附件 ):
package org.hibernate.tutorial.hbm; import java.util.Date; import java.util.List; import org.hibernate.Session; import org.hibernate.SessionFactory; import org.hibernate.boot.MetadataSources; import org.hibernate.boot.registry.StandardServiceRegistry; import org.hibernate.boot.registry.StandardServiceRegistryBuilder; import junit.framework.TestCase; /** * Illustrates use of Hibernate native APIs. * * @author Steve Ebersole */ public class NativeApiIllustrationTest extends TestCase { private SessionFactory sessionFactory; @Override protected void setUp() throws Exception { // A SessionFactory is set up once for an application! final StandardServiceRegistry registry = new StandardServiceRegistryBuilder() .configure() // configures settings from hibernate.cfg.xml .build(); try { sessionFactory = new MetadataSources( registry ).buildMetadata().buildSessionFactory(); } catch (Exception e) { // The registry would be destroyed by the SessionFactory, but we had trouble building the SessionFactory // so destroy it manually. StandardServiceRegistryBuilder.destroy( registry ); } } @Override protected void tearDown() throws Exception { if ( sessionFactory != null ) { sessionFactory.close(); } } @SuppressWarnings("unchecked") public void testBasicUsage() { // create a couple of events... Session session = sessionFactory.openSession(); session.beginTransaction(); session.save( new Event( "Our very first event!", new Date() ) ); session.save( new Event( "A follow up event", new Date() ) ); session.getTransaction().commit(); session.close(); // now lets pull events from the database and list them session = sessionFactory.openSession(); session.beginTransaction(); List result = session.createQuery( "from Event" ).list(); for ( Event event : (List<Event>) result ) { System.out.println( "Event (" + event.getDate() + ") : " + event.getTitle() ); } session.getTransaction().commit(); session.close(); } }
下面来分析代码。
在 setUp 中,首先构建了一个 org.hibernate.boot.registry.StandardServiceRegistry
,用来处理 hibernate.cfg.xml 等配置信息(sessionFactory 会使用 serviceRegistry 中的配置信息)。
sessionFactory = new MetadataSources( registry ).buildMetadata().buildSessionFactory();
在 setUp 中,首先构建了一个 org.hibernate.boot.registry.StandardServiceRegistry
,用来处理 hibernate.cfg.xml 等配置信息(sessionFactory 会使用 serviceRegistry 中的配置信息)。
我们使用 org.hibernate.boot.MetadataSource
先创建了 org.hibernate.boot.MetadataSources
,这个类表示了 domain Model。然后基于此创建了 SessionFactory。
最后一步就是创建 SessionFactory
了,这是一个全局单例的类,线程安全。
SessionFactory
是 Session 的工厂类,每次使用一个 Session 来完成一块任务:
Session session = sessionFactory.openSession(); session.beginTransaction(); session.save( new Event( "Our very first event!", new Date() ) ); session.save( new Event( "A follow up event", new Date() ) ); session.getTransaction().commit(); session.close();
以上代码中, testBasicUseage()
先创建了一个 Event 对象,然后交给 Hibernate 处理,Hibernate 的 save() 方法将其插入数据库中。
获取对象的代码如下:
session = sessionFactory.openSession(); session.beginTransaction(); List result = session.createQuery( "from Event" ).list(); for ( Event event : (List<Event>) result ) { System.out.println( "Event (" + event.getDate() + ") : " + event.getTitle() ); } session.getTransaction().commit(); session.close();
我们通过写 Hibernate Query Language 向 Hibernate 表达查询,Hibernate 将其转换成 SQL 执行查询,然后将结果转换成我们需要的 class。
使用注解API,在配置文件中我们的 mapping 字段需要设置为:
<!-- Names the annotated entity class --> <mapping class="org.hibernate.tutorial.annotations.Event"/>
其他的配置项和前面一样。然后,我们在定义 class 和 table 的映射的时候,不再使用 xml 文件了,而是直接在 class 上面注释:
@Entity @Table( name = "EVENTS" ) public class Event { ... }
@Entity
的注解和 xml 中的 <class />
作用一样。这里显示地指定了表的名字,如果不指定的话,就会默认使用类名 EVENT
。
字段的定义如下:
@Id @GeneratedValue(generator="increment") @GenericGenerator(name="increment", strategy = "increment") public Long getId() { return id; }
@javax.persistence.Id
定义了实体的 ID;
@javax.persistence.GeneratedValue
和 @org.hibernate.annotations.GenericGenerator
指示 Hibernate 应该用 increment
生成器自动生成这个字段。
public String getTitle() { return title; } @Temporal(TemporalType.TIMESTAMP) @Column(name = "EVENT_DATE") public Date getDate() { return date; }
同 xml 配置方式一样, date
也需要特殊处理一下。
实体的属性默认将会持久化,所以这个类中我们没有写 title 的 annotation,但是 title
也会被持久化。
示例代码:这部分的例子和上面一样。
上面我们使用的配置文件是 hibernate.cfg.xml
,而 JPA 定义了自己的配置方式,叫做 persistence.xml
。启动方式是 JPA 定义的规范,Hibernate 作为持久化的提供者,需要读取并应用 META-INF/persistence.xml
里面的设置。
persistence.xml 文件范例如下:
<persistence xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd" version="2.0"> <persistence-unit name="org.hibernate.tutorial.jpa"> ... </persistence-unit> </persistence>
对于每一个 persistence-unit 需要提供一个 unique name。应用在获得 javax.persistence.EntityManagerFactory
引用的时候,使用这个 unique name 来获得配置的引用。
之前的配置文件中的配置项,先在要使用 javax.persistence
前缀来区分开。对于 Herbenate 特殊肚饿配置项要用 hibernate.
前缀。
<persistence-unit name="org.hibernate.tutorial.jpa"> <description> Persistence unit for the JPA tutorial of the Hibernate Getting Started Guide </description> <class>org.hibernate.tutorial.em.Event</class> <properties> <property name="javax.persistence.jdbc.driver" value="org.h2.Driver" /> <property name="javax.persistence.jdbc.url" value="jdbc:h2:mem:db1;DB_CLOSE_DELAY=-1;MVCC=TRUE" /> <property name="javax.persistence.jdbc.user" value="sa" /> <property name="javax.persistence.jdbc.password" value="" /> <property name="hibernate.show_sql" value="true" /> <property name="hibernate.hbm2ddl.auto" value="create" /> </properties> </persistence-unit>
除此之外, <class />
的内容和我们在前面看到的 Hibernate 配置文件一样。
前面的例子中使用的是 Hibernate 的 native API,这里我们使用 JPA 的 API 。
获取 SessionFactory:
protected void setUp() throws Exception { sessionFactory = Persistence.createEntityManagerFactory( "org.hibernate.tutorial.jpa" ); }
注意这里使用的 persistence unit name 是 org.hibernate.tutorial.jpa
,和上面提到的配置问题件对应。
使用 javax.persistence.EntityManager
保存实体。Hibernate 中 save 的步骤,在 JPA 中叫做 persist
.
EntityManager entityManager = sessionFactory.createEntityManager(); entityManager.getTransaction().begin(); entityManager.persist( new Event( "Our very first event!", new Date() ) ); entityManager.persist( new Event( "A follow up event", new Date() ) ); entityManager.getTransaction().commit(); entityManager.close();
Hibernate 有一个功能,叫做 Envers,就是可以将数据的历史版本都保存下来。数据及时更改了,也可以找到曾经的状态。
这个功能也不是无成本的,需要一些保存历史版本,保存和读取的时候,也需要额外的计算,所以我们只在需要的时候这么做。
我们通过 @org.hibernate.envers.Audited
这个注解告诉Hibernate需要保留这个类的历史版本,然后,我们可以 org.hibernate.envers.AuditReader
来读取数据的历史版本。
public void testBasicUsage() { // create a couple of events EntityManager entityManager = entityManagerFactory.createEntityManager(); entityManager.getTransaction().begin(); entityManager.persist( new Event( "Our very first event!", new Date() ) ); entityManager.persist( new Event( "A follow up event", new Date() ) ); entityManager.getTransaction().commit(); entityManager.close(); // now lets pull events from the database and list them entityManager = entityManagerFactory.createEntityManager(); entityManager.getTransaction().begin(); List<Event> result = entityManager.createQuery( "from Event", Event.class ).getResultList(); for ( Event event : result ) { System.out.println( "Event (" + event.getDate() + ") : " + event.getTitle() ); } entityManager.getTransaction().commit(); entityManager.close(); // so far the code is the same as we have seen in previous tutorials. Now lets leverage Envers... // first lets create some revisions entityManager = entityManagerFactory.createEntityManager(); entityManager.getTransaction().begin(); Event myEvent = entityManager.find( Event.class, 2L ); // we are using the increment generator, so we know 2 is a valid id myEvent.setDate( new Date() ); myEvent.setTitle( myEvent.getTitle() + " (rescheduled)" ); entityManager.getTransaction().commit(); entityManager.close(); // and then use an AuditReader to look back through history entityManager = entityManagerFactory.createEntityManager(); entityManager.getTransaction().begin(); myEvent = entityManager.find( Event.class, 2L ); assertEquals( "A follow up event (rescheduled)", myEvent.getTitle() ); AuditReader reader = AuditReaderFactory.get( entityManager ); Event firstRevision = reader.find( Event.class, 2L, 1 ); assertFalse( firstRevision.getTitle().equals( myEvent.getTitle() ) ); assertFalse( firstRevision.getDate().equals( myEvent.getDate() ) ); Event secondRevision = reader.find( Event.class, 2L, 2 ); assertTrue( secondRevision.getTitle().equals( myEvent.getTitle() ) ); assertTrue( secondRevision.getDate().equals( myEvent.getDate() ) ); entityManager.getTransaction().commit(); entityManager.close(); }