一、学习Hibernate的官方Reference文档
从3.6版本之后,Hibernate的xml推荐使用如下的声明
即,用
http://www.hibernate.org/dtd/
替换原来sourceforge那种格式。
以前C3P0驱动是如下配置的:
在新版本中应该用如下配置:
org.hibernate.service.jdbc.connections.internal.C3P0ConnectionProvider
Hibernate官方文档从一开始就开始介绍Hibernate是和JBoss数据库结合在一起的,Hibernate的required包里面就有两个是JBoss相关的。
同时它一开始就讲到,Hibernate是用Maven来管理的(JBoss Maven repository)。
Hibernate的jar包目录如下
其中required里面包括
除了required目录下的jar,还有jpa目录下的jar以及optional里面的c3p0和ehcache都是常用的。
关于数据库方言(dialect)的配置,Hibernate官方文档是这样说的:
In most cases, Hibernate is able to properly determine which dialect to use. This is particularly useful if your application targets multiple databases.
也就是说如果不是多数据库,就可以不配置dialect。
关于实体(Entity)的定义,用标准的JavaBean,建议将其属性(fields)设置为私有(private)的。要有setter和getter方法,构造函数必须是无参(no-argument)的,而且建议设置为public的。Hibernate会用Java Reflection去为你create objects。Hibernate会自动到classpath下去找Event.hbm.xml定义文件。
实体的hbm.xml文件定义格式如下:
<class name="Event" table="EVENTS">
...
</class>
class中,name和table对应。
然后用id定义数据表的主键:
<id name="id" column="EVENT_ID">
...
</id>
主键不是必须的,但是Hibernate强烈的要求你使用主键。(it is strongly recommend that all schemas define proper referential integrity)
可以用generator定义主键的生成策略。例如:
<id name="id" type="java.lang.Integer">
<column name="id" />
<generator class="identity" />
</id>
用property定义table的其他字段。例如:
<property name="date" type="timestamp" column="EVENT_DATE"/>
<property name="title"/>
第二个property没有指定column,那么name就会被看作是默认的column。同时注意到第一个property,由于其name的值(date)在很多数据库中是一个关键字,所以此时column是不能省略的。
注意到property中有一个属性为type,它既不是Java数据类型,也不是SQL数据库类型,
而是Hibernate映射类型(The types declared and used in the mapping files are neither Java data types nor SQL database types. Instead, they are Hibernate mapping types)。
如果不指定这个类型,Hibernate将根据Java反射自动决定(if the type attribute is not present in the mapping, by using Java reflection to determine the Java type of the declared property and using a default mapping type for that Java type)。
注意到上面的type=timestamp,这个timestamp就是Hibernate定义的映射类型(Hibernate cannot know if the property, which is of type java.util.Date, should map to a SQL DATE, TIME, or TIMESTAMP datatype. Full date and time information is preserved by mapping the property to a timestamp converter, which identifies an instance of the class org.hibernate.type.TimestampType)。
二、Hibernate及Hibernate与 Struts 2 的整合
Hibernate的主要作用是操作数据库的,一方面方便建立数据库的连接,另一方面把关系数据映射成面向对象的数据,通过HQL来操作数据库的增删查改。
首先把Hibernate需要的jar文件hibernate3.jar拷贝到WEB-INF/lib目录下。
然后配置Hibernate,使其能够连接数据库。在没有使用Spring框架时,Hibernate的配置文件就放在/src文件夹下,命名为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>
<!-- 数据库连接URL -->
<property name="connection.url">
jdbc:mysql://localhost/javaweb
</property>
<!-- 数据库连接驱动 -->
<property name="connection.driver_class">
com.mysql.jdbc.Driver
</property>
<!-- 数据库用户名 -->
<property name="connection.username">root</property>
<!-- 数据库用户密码 -->
<property name="connection.password">admin</property>
<!-- 数据库方言 -->
<property name="dialect">
org.hibernate.dialect.MySQLDialect
</property>
<!-- 指定映射文件 -->
<mapping resource="com/javaweb/po/Product.hbm.xml"/>
</session-factory>
</hibernate-configuration>
如果使用了Spring框架,则不需要这个文件,直接交由Spring托管,在Spring的配置文件\WebRoot\WEB-INF\applicationContext.xml中配置类似的内容。
配置好后,就可以使用了。假如有一个数据文件User.java,里面定义id,name等数据,这时要把这些数据映射成面向对象的,只需要在User.java的同级目录下建立一个User.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>
<!-- 每个class对应一个持久化对象 -->
<class name="com.javaweb.po.User">
<!-- id元素用来定义主键标识,并指定主键自动递增 -->
<id name="id">
<generator class="identity"></generator>
</id>
<!-- 定义其他属性 -->
<property name="username"></property>
<property name="password"></property>
<property name="age" type="int"></property>
<property name="birth" type="date"></property>
<property name="email"></property>
</class>
</hibernate-mapping>
此时配置便完成了,在其他java文件中引入相应的Hibernate类文件,就可以以面向对象的方式使用数据了。例如,通常引用到的为
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.cfg.Configuration;
import org.hibernate.Query;
import org.hibernate.Session;
等等,非常多,而如果每个java文件都要引用的话就显得非常累赘,所以通常把一些基础数据库操作封装在HibernateUtil.java文件中。
如果使用了Spring框架,则不需要引入这么多文件,在Spring框架中,直接使用dao文件就可以了,在dao文件中引入如下Spring的hibernate3.suppor.HibernateDaoSupport即可:
import org.springframework.orm.hibernate3.support.HibernateDaoSupport;
Hibernate与 Struts 2 的整合其实没什么可讲的,就是简单的把两个的功能叠加在一起,类似于1+1=2。Hibernate负责DAO操作,DAO通常分为3个部分,DAO接口,DAO实现类,DAO工厂类,接口就是声明一些方法,例如delete,实现类利用Hibernate的方式去实现这个方法,而工厂类就是简单的new一个实现类再返回,工厂类示例如下:
package com.javaweb.factory;
import com.javaweb.dao.ProductDao;
import com.javaweb.dao.ProductDaoImpl;
public class DaoFactory {
public static ProductDao getDaoInstance(){
return new ProductDaoImpl();
}
}
仅在有很多个DAO的实现类时才能体现工厂类的作用,因为工厂类把这些实现类都封装在了一个DaoFactory类里面,只需要调用不同的方法即可new一个不同的实现类。
Hibernate的配置之Configuration对象和SessionFactory对象
通常写在HibernateSessionFactory.java中:
private static org.hibernate.cfg.Configuration configuration = new Configuration();
private static org.hibernate.SessionFactory sessionFactory;
private static String configFile = "/hibernate.cfg.xml;
configuration.configure(configFile);
sessionFactory = configuration.buildSessionFactory();
创建Session:
Session session1 = sessionFactory.openSession() ;
// Session session2 = sessionFactory.getCurrentSession() ;
官方的写法:
public static Session getSession() throws HibernateException {
Session session = (Session) threadLocal.get();
if (session == null || !session.isOpen()) {
if (sessionFactory == null) {
rebuildSessionFactory();
}
session = (sessionFactory != null) ? sessionFactory.openSession()
: null;
threadLocal.set(session);
}
return session;
}
Transaction的用法:
Transaction trans = session1.beginTransaction();
trans.begin();
trans.commit();
遇到异常:
trans.rollback();
持久化状态:
session1.save(obj); //可使对象由临时状态,转换成持久化状态。
session1.delete(); //同上相反,把对象欢迎成临时状态
session1.close(); //可使持久化状态称为游离状态。
session1.update(); //同上相反,可使游离状态变成持久化状态。
三、Hibernate缓存
一级缓存是session类的缓存,是自动缓存的,无需配置。
对于一级缓存:
(1)save、update、saveOrupdate、load、get、list、iterate、lock方法都会向缓存中存对象.
(2)可以从缓存中读数据的只有: get、load、iterate
(3)Query对象默认情况下不读缓存,如果要使其支持缓存(也就是new两个相同的Query时,第二次不再去执行sql语句),则要通过语法: query.setCacheable(true);(并且要在hibernate.cfg.xml中配置:
<property name="hibernate.cache.use_query_cache">true</property>)
PS:list方法好像有点怪,其存入缓存中的数据无法clear?
注意事项:
1.Session 级别的缓存,它同session邦定。它的生命周期和session相同。Session消毁,它也同时消毁;管理一级缓存,一级缓存无法取消,用两个方法管理,clear(),evict()
2.两个session 不能共享一级缓存,因它会伴随session的生命周期的创建和消毁;
3.Session缓存是实体级别的缓存 ,就是只有在查询对象级别的时候才使用 ,如果使用HQL和SQL是查询属性级别 的,是不使用一级缓存的!切记!!!!
查询单个对象的两种方法:
User user1 = (User) session.load(User.class, 1);
User user2 = (User) session.get(User.class, 53);
前一种load的时候不会执行sql语句,user1是空对象,只有用到user1时才会去查询,第二种get方法,就是直接查询,把结果赋值给user2。两种方式都会一级缓存。
查询多个对象时,有两种方法:
第一种(List方法):
List<User> list = query.list();//第一次查询
list = query.list(); //还会去查询一次,但是将结果全部缓存到list中
for (User user : list) {
System.out.println(user); //直接从缓存中取
}
第二种(Iterate方法)
Iterator list = query.iterate(); //只查询id,而不是把所有值都查出
list = query.iterate(); //还会去查询一次id
while (list.hasNext()) {
System.out.println(list.next()); //每次都要用sql去查询指定id的记录
}
可以看出,其实iterate的效率很低,但是list占用内存较大。所以一般用list,并且数据量大时一定要配合用分页查询(否则不能用list)。
另外注意,在增删改时涉及到事务(Transaction),当事务没有commit时,所有的增删改的数据都是存在代理那里的,实际上这也是一种缓存。例如要save一千万个对象,那么肯定会塞满缓存,一般是每save二十个对象就flush+clear,然后再继续。
二级缓存是sessionFactory下的缓存,各个session都共享缓存,需要配置插件。一般用hibernate自带的这个ehcache,需要引入下面这个包:
ehcache-1.2.3.jar
首先在hibernate.cfg.xml中配置:
<property name="hibernate.cache.provider_class">org.hibernate.cache.EhCacheProvider</property>
<property name="hibernate.cache.use_second_level_cache">true</property>
<property name="hibernate.cache.use_query_cache">true</property>
<property name="current_session_context_class">thread</property>
在src目录下新建ehcache.xml,这个是可选的,在这个文件中可以自定义cache的参数,例如下面的文件中自定义了一个name为userCache的配置:
<?xml version="1.0" encoding="UTF-8"?>
<ehcache>
<diskStore path="java.io.tmdir"/>
<defaultCache
maxElementsInMemory="10000"
eternal="false"
overflowToDisk="true"
timeToIdleSeconds="300"
timeToLiveSeconds="600"
diskPersistent="false"
memoryStoreEvictionPolicy="LRU"
/>
<cache name="userCache"
maxElementsInMemory="5000"
eternal="false"
overflowToDisk="true"
timeToIdleSeconds="300"
timeToLiveSeconds="600"
diskPersistent="false"
memoryStoreEvictionPolicy="LRU"
/>
</ehcache>
可以为每个entity配置二级缓存:
比如在User.hbm.xml中,做如下设置:
<class name="User" table="t_user">
<cache usage="nonstrict-read-write" region="userCache"/>
<id name="id" type="java.lang.Integer">
<column name="id" />
<generator class="sequence">
<param name="sequence">SEQ_ID_ALL</param>
</generator>
</id>
那个cache标签即给User设置缓存,必须放在首位(紧跟class标签),其属性region指定了缓存的配置,这里其值是userCache,对应ehcache中的配置。也可以在hibernate.cfg.xml中配置:
<class-cache usage="nonstrict-read-write" region="userCache" class="com.zollty.hibc.entity.User"/>
对于以上配置中的usage,有nonstrict-read-write,read-write,read-only和transactional四种,关于这四种事务隔离级别的描述如下:
事务型:仅仅在托管环境中适用。它提供了Repeatable Read事务隔离级别。对于经常被读但很少修改的数据,可以采用这种隔离类型,因为它可以防止脏读和不可重复读这类的并发问题。
读写型:提供了Read Committed事务隔离级别。仅仅在非集群的环境中适用。对于经常被读但很少修改的数据,可以采用这种隔离类型,因为它可以防止脏读这类的并发问题。
非严格读写型:不保证缓存与数据库中数据的一致性。如果存在两个事务同时访问缓存中相同数据的可能,必须为该数据配置一个很短的数据过期时间,从而尽量避免脏读。对于极少被修改,并且允许偶尔脏读的数据,可以采用这种并发访问策略。
只读型:对于从来不会修改的数据,如参考数据,可以使用这种并发访问策略。
事务型并发访问策略是事务隔离级别最高,只读型的隔离级别最低。事务隔离级别越高,并发性能就越低。(一般用“非严格读写型”甚至“只读型”)
使用方法(调用Query的setCacheable(true)方法),例如:
public void QueryCache() {
Session session1 = HibernateSessionFactory.getSession();
List list2 = session1.createQuery("from User").setCacheable(true).list();
session1.close();
Session session2 = HibernateSessionFactory.getSession();
// User user2 = (User) session2.load(User.class, 22); //不会查询,直接用session1的缓存
List list3 = session2.createQuery("from User").setCacheable(true).list();
session2.close();
// 只查询一遍
}
在配置中的region属性可以不写,可以到程序中再指定:
List list2 = session1.createQuery("from User")
.setCacheable(true).setCacheRegion("userCache").list();
四、Hibernate锁,悲观锁(Pessimistic),乐观锁(Optimistic)
悲观锁:本次事务提交之前(事务提交时会释放事务过程中的锁),外界无法修改这些记录。
其实现方法是转换成数据库的锁来实现的,例如:
select * from account where name='zollty' for update
Hibernate的加锁模式有多种,我们常用的是:
LockMode.UPGRADE:利用数据库的for update子句加锁。
LockMode.UPGRADE_NOWAIT:Oracle的特定实现,利用Oracle的for update nowait子句实现加锁。
加锁一般通过以下方法实现:
Criteria.setLockMode
Query.setLockMode
Session.lock
悲观锁的实现(for update):
//也就是在执行数据库select语句时跟上for update
query.setLockMode("user", LockMode.UPGRADE);
/**
* 当有LOCK冲突时会提示错误并结束STATEMENT而不是在那里等待(比如:要查的行已经被其它事务锁了,
* 当前的锁事务与之冲突,加上nowait,当前的事务会结束会提示错误并立即结束 STATEMENT而不再等待).
* 关于NOWAIT(如果一定要用FOR UPDATE,我更建议加上NOWAIT)
* 如果加了for update后 该语句用来锁定特定的行(如果有where子句,就是满足where条件的那些行)。
* 当这些行被锁定后,其他会话可以选择这些行,但不能更改或删除这些行,直到该语句的事务被commit语句或rollback语句结束为止。
*/
乐观锁的实现(Version):
<class name="xx" table="t_user"
dynamic-update="true"
dynamic-insert="true"
optimistic-lock="version">
<id>...</id>
<!-- version必须紧跟id之后 -->
<version name="version" type="java.lang.Integer"/>
五、Hibernate实体配置高级篇
sequence的写法
<id name="id" type="java.lang.Integer">
<column name="ID" precision="22" scale="0" />
<generator class="sequence">
<param name="sequence">SEQ_DEPT_ID</param>
</generator>
</id>
员工和部门的关系配置:
员工deptInfo对象属性:
private DeptInfo deptInfo;
对应:
<many-to-one name="deptInfo" class="DeptInfo" fetch="select">
<column name="DEPT_ID" precision="22" scale="0" />
</many-to-one>
而部门中员工是以set来存放的:
private Set<EmpInfo> empInfos = new HashSet<EmpInfo>(0);
对应:
<set name="empInfos" inverse="true">
<key>
<column name="DEPT_ID" precision="22" scale="0" />
</key>
<one-to-many class="EmpInfo" />
</set>
这个inverse是做什么的?答:通常放在一对多的一方并设置为true,代表“主外键的关系由对方来维护”,打个比方:在一个公司中,是老板认识所有的员工容易,还是所有员工认识老板容易?
员工配置里面是<many-to-one>多个员工对应一个部门
而部门配置里面是<one-to-many>是一个部门对应多个员工。这个标签有一个class属性放对方的类名。
关于inverse的详细解释:
设STUDENT表中存在一个名为CLASS_ID的字段,它和CLASS表中的ID字段是主外键关系。那个inverse属性就是用来规定是由谁(Student或Class)来维护这个主外键关系的。
inverse的默认值为false。
在处理逻辑代码中,如下:
Class c1 = new Class();
c1.setName("一班");
Student s1 = new Student();
Student s2 = new Student();
s1.setName("Jason");
s2.setName("Tom");
c1.getStudents().add(s1);
c2.getStudents().add(s2);
s1.setClass(c1);
s2.setClass(c1); //注释1
session.save(c1);
上面的代码会使Hibernate执行五条SQL语句,其中前三条是insert插入语句,后两条是update更新语句。插入就不用说了,那么为什么还要有更新语句呢?这是因为Class类映射文件的<set>元素中指定了inverse="false",这就告之Hibernate:STUDENT表与CLASS表的主外键关系是由Class类来维护的。当执行save后,执行了三条insert语句,这三条语句中的后两条是插入到STUDENT表的,它们的CLASS_ID字段是通过s1.getClass().getID()取出的,假如我将上面“注释1”处修改为s2.setClass(c2);(c2是另一个Class对象,可能是持久化对象),这样,主外键关系不就乱了吗。为了保证主外键关系,Hibernate在这种情况下会再执行两条update语句来更改STUDENT表中两个新插入记录的CLASS_ID字段,当然,这时CLASS_ID字段的取值是从c1对象中直接取得,而不再是s1.getClass().getID()方式了。
如果我们将Class类映射文件的<set>元素中的inverse属性修改为true,这就是告诉Hibernate:Class类不维护主外键关系了,这个任务就交给了Student类。于是,我们再执行上面的代码,Hibernate就会只执行三条insert语句,而不会执行任何update语句。因为Hibernate会通过Student类的s1.getClass().getID()和s2.getClass().getID()来确定CLASS_ID字段的值。
故,为了节省数据库资源,省却不必要的update语句,我们一般建议在一对多双向关联关系中,将一方的inverse属性设置为true,即将主外键的关系交由多方来维护。
打个比方:在一个公司中,是老板认识所有的员工容易,还是所有员工认识老板容易?