一、修改一条记录
更新数据库中的一条记录有两种方式:
① 加载持久化对象,修改对象属性,然后session清理缓存的时候会自动同步更新数据库中的数据。
② 更新游离对象,通过session的update方法更新数据库中的相应数据。
二、Hibernate与数据库触发器协同工作的问题及解决办法
问题1:触发器使session缓存中的持久化对象与数据库中数据不一致。
解决方法:执行完持久化操作后马上调用session的flush方法将改变同步到数据库,然后调用refresh方法重新加载持久化对象。
问题2:session的update方法盲目激发触发器。
产生问题的原因:使用update或者saveOrUpdate方法更新游离对象时,session无法判断对象属性是否和数据库中一致,为保险起见,session总是会将游离对象的属性更新到数据库中。实际上,如果游离对象的属性和数据库中是一致的,这条update语句就是多余的,触发器也不应该被激发。
解决方法:在映射文件的<class>元素中设置select-before-update属性为true,从而在session的update和saveOrUpdate方法更新一个游离对象的时候,先执行查询语句,比较游离对象的属性和数据库是否一致,不一致的时候才执行更新语句。这样会避免盲目激发触发器,但是会有一定程度的效率损失。
三、批量处理数据
一般说来,应该尽量避免在应用层进行批量操作,而应该在数据库层直接批量操作。如果批量操作的逻辑比较复杂,则应该通过运行存储过程来完成。
在应用层进行批量操作主要有以下方式:
① 通过session批量操作
通过session执行批量操作具有以下约束。
1)需要在Hibernate配置文件中设置单次批量处理的数目,合理的取值通常是10~50。如 hibernate.jdbc.batch_size=20,并且保证每次向数据库发送的批量SQL语句数目与这个batch_size一致。
2)如果采用identity标示符生成器,则hibernate无法在jdbc层进行批量插入操作。
3)批量操作时,建议关闭hibernate二级缓存。
批量插入数据的部分代码如下:
for(int i=0;i<100000;i++){
Customer c = new Customer(...);
session.save(c);
if(i%20==0){
session.flush();
session.clear();
}
}
如前文所述,为保证上述代码顺利执行,需要遵循以下约束:hibernate.jdbc.batch_size=20;Customer对象标示符生成器不是“identity”;关闭二级缓存。
批量更新数据的部分代码如下:
ScrollableResults customers = session.createQuery("from Customer").scroll(ScrollMode.FORWARD_ONLY);
int count=0;
while(customers.next()){
Customer c = (Customer) customers.get(0);
c.setAge(c.getAge() + 1);
if(++count % 20 == 0){
session.flush();
session.clear();
}
}
上述代码中,Query对象的scroll()方法返回的ScrollableResults对象实际不包含任何Customer对象,它仅包含用于在线定位数据库中的CUSTOMERS记录的游标。只有当程序遍历访问ScrollableResults对象中的特定元素时,它才会到数据库中加载相应的Customer对象。
为保证程序顺利运行,需要遵守以下约束:
Hibernate配置文件中,hibernate.jdbc.batch_size=20;
关闭二级缓存。如果程序中已经开启了二级缓存,也可以通过以下方式在程序中忽略二级缓存
ScrollableResults customers = session.createQuery("from Customer").setCacheMode(CacheMode.IGNORE).scroll(ScrollMode.FORWARD_ONLY);
② 通过StatelessSession进行批量操作
主要代码如下:
StatelessSession session = SessionFactory.openStatelessSession();
Transaction tx = session.beginTransaction();
ScrollableResults customers = session.createQuery("from Customer").scroll(ScrollMode.FORWARD_ONLY);
while(customers.next()){
Customer c = (Customer) customers.get(0);
c.setAge(c.getAge() + 1);
session.update(c);
}
tx.commit();
session.close();
StatelessSession与session用法相似,二者主要区别如下:
1)前者没有缓存,通过StatelessSession加载、保存、更新的对象处于游离状态。
2)前者不会与二级缓存交互。
3)调用前者的方法时,sql语句立即执行,而后者需要等到清理缓存的时候才执行。
4)前者不会进行脏检查,所以上述程序中修改内存中Customer对象的属性之后,还要调用update方法更新数据库中相应数据。而后者得到的是持久化对象,修改对象属性之后,hibernate会自动在session关闭前清理缓存同步数据。
5)前者不会对任何关联对象进行级联操作。
6)前者所做操作可以被Interceptor拦截器捕捉到,但是会被hibernate事件处理系统忽略。
7)前者两次加载主键相同的对象时,获得的是两个内存地址不同的对象,用“==”比较获得的是false。
③通过HQL批量操作
通过Query.executeUpdate方法批量更新或删除数据,自定义HQL语句做方法参数。
④通过jdbc API批量操作
hibernate3中通过session.connection()获得数据库连接,之后可以通过jdbc API进行数据库操作。但是此方法已经被废弃,hibernate4中甚至已经删除了此方法,应该采用如下方法获取connection:
session.doWork(new Work() {
@Override
public void execute(Connection connection) throws SQLException {
// TODO Auto-generated method stub
}
});
⑤hibernate调用存储过程
hibernate没有专门的接口调用存储过程,在需要的时候先获得connection,然后通过jdbc API调用存储过程。
四、Hibernate检索策略
检索策略作用域 | 可选策略 | 默认值 | 受影响的检索方法 |
类级别 | 立即检索 延迟检索 | 延迟检索 | session的load()方法 |
关联级别 | 立即检索 延迟检索 迫切左外连接检索 | 延迟检索 | session的load()和get()方法,以及Query API和Criteria API。 例外情况是Query API会忽略映射文件中设定的迫切左外连接检索策略。 |
①类级别的检索策略
如果程序加载一个持久化对象的目的是为了访问他的属性,可以使用立即检索。如果加载一个持久化对象的目的仅仅是获得它的引用,可以使用延迟检索。如下是适用延迟检索的情况:
Customer c = (Customer)session.load(Customer.class,new Long(1));
Order o = new Order();
o.setCustomer(c);
session.save(o);
②迫切左外连接
List result = session.createQuery("from Customer c left jion fetch c.orders o where c.name like 'T%'").list();
使用迫切左外连接检索策略时,Query的list()方法返回的集合中存放Customer对象的引用,每个Customer对象的orders集合都被初始化,存放所有关联的Order对象。使用迫切左外连接检索策略时,查询结果中可能包含重复元素,可以通过HashSet进行过滤:
HashSet set = new HashSet(result);
说明:迫切连接会使延迟加载失效;左外连接、右外连接以及内连接的查询结果可能会包含重复对象;左右外连接情况下主表有记录,次表没有记录的时候次表返回的对象为null。内连接情况下,只要有一方的表中没有记录,相应的另一方也不会出现在查询结果中。也就是内连接查询出的两个关联的对象都不为null,如果有一方为null,这条记录就不会出现在查询结果中了。
五、Hql方式检索一条记录
如果Hql的查询结果只有一条记录,调用Query的uniqueResult()方法,该方法返回一个Object对象。如果查询结果包含多条记录,但是只需要抽取其中的一条使用,可以在调用uniqueResult()方法前先调用setMaxResult(1)方法限定只返回一条记录。
六、集合过滤
一对多关系中多的一方通常是保存在set中,不利于排序、筛选、分页等需求。可以使用hibernate的集合过滤功能解决此问题。
List result = session.createFilter(customer.getOrders(),"where this.price>100 order by this.price").list();
Session的createFilter()方法用来过滤集合,它具有以下特点:
①返回Query类型的实例。
②它的第一个参数指定一个持久化对象的集合。这个集合是否已经初始化没有关系,但是它所属的对象必须是持久化对象,也就是上述代码中customer对象必须是持久化对象。
③它的第二个参数指定过滤条件,它由合法的HQL查询语句组成。
④无论持久化对象的集合是否已经被初始化,Query的list()方法都会执行SQL查询语句,到数据库中检索Order对象。如果orders集合已经初始化了,Query的list()方法不会新建Order对象,仅返回已存在的Order对象的引用。如果orders集合没有初始化,Query的list()方法会创建相应的Order对象返回对它们的引用,但是不会初始化Customer对象的orders集合。
七、处理并发问题
悲观锁的实现方式:
1、在应用程序中显式指定采用数据库系统的独占锁来锁定数据资源。
select... for update
Hibernate中使用get()和load()方法加载对象可以采用如下方式声明使用悲观锁:
Account a = (Account) session.get(Account.class,new Long(1),LockMode.UPGRADE);
2、在数据库的ACCOUNTS(对应Account类)表中增加lock(锁)字段,可以是boolean类型,“true”表示锁定状态,“false”表示空闲状态。当一个事务先查询ACCOUNTS表中的ID为1的记录,然后再修改这条记录的时候,按以下步骤执行:
(1)根据lock字段判断这条记录是否处于空闲状态。
(2)如果处于锁定状态就一直等待,直到这条记录变成空闲状态。如果需要也可以撤销事务,抛出异常,通知客户系统正忙,稍后再执行该事务。
(3)如果记录处于空闲状态,就把lock字段改为“true”,锁定这条记录。
(4)更新这条记录,并把lock字段改为“false”,解除锁定。