Java for Web学习笔记(九七):持久化初探(2)ORM、JPA和Hibernate
对象关系映射:Object-Relational Mapper(O/RM)
我们希望直接在关系型数据库中读写对象,而无需在代码中操作SQL,这需要Object-Relational Mapper。
我们可能有几十个表格,每个表格都有很多列,这样整个增删改查的代码就很繁琐,使用O/RM,我们的代码可以得到很大的简化。仍如使用上一学习中的学生例子:
public Product getStudent(long id){
return this.getCurrentTransaction().get(Student.class, id);
}
public void addStudent(Student student){
this.getCurrentTransaction().persist(student);
}
public void updateStudent(Student student){
this.getCurrentTransaction().update(student);
}
public void deleteStudent(Student student){
this.getCurrentTransaction().delete(student);
}
我们需告知O/RM映射的规则,不同的O/RM有不同映射方式,存在迁移的成本。高度的抽象会隐藏实现的细节,有时合理的statement(sql)对性能很关键,我们可以通过数据库服务的statement log来分析。很多O/RM可以根据映射指令自动生成表格的schema(自动生成表格),但我们不应该这样做,尤其在生产环境或者产品,要记住我们应该自己设计表格。如果表格设计差,是你的错,不是O/RM。
Java Persistence API
O/RM的产品有很多,例如Top Link及其后来的Eclipse Link,iBATIS以及后来的MyBatis,Hibernate ORM以及用于.NET的NHibernate。为解决不同O/RM迁移的成本问题,出现了Java Persistence API,并作为JSR 220加入了在Java EE 5。在2009年,JPA 2.0作为JSR 317加入了Java EE 6,在2013年,JPA2.1作为JSR 338加入Jaav EE 7。
JPA使用JDBC来执行SQL statement,但封装得很好,我们不会直接使用JDBC API,也避免直接使用SQL语句。
Hibernate ORM
Hibernate是JPA的实现。在maven中,scope设置为runtime,即不直接使用其API,而是使用JPA。但是Hibernate提供了JPA外的一些能力,如果需要使用,则应将scope设置为compile。下面重点介绍这些额外的功能,这些功能仅作初步的了解,在后面的学习中将不再涉及。
使用Hibernate Mapping文件
早期的Hibernate版本使用XML文件来描述映射,在3.5版本支持annotation,包括JPA的标记。现在也可以使用XML来映射,但应只在仅使用Hibernate而非JPA的场景。
一般来讲,XML映射文件放在resource,与对应的entity同名,且路径相同,例如一个com.example.entities.Product的类对应为Product.hbm.xml,位于resource下的com/example/entities/Product.hbm.xml。例子如下:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN" "http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd">
<hibernate-mapping>
<class name="com.example.entities.Product" table="Product" schema="dbo">
<!-- id用于表示单列单field的index,如果多列或者组合,使用composite-id -->
<id name="productId" column="ProductId" type="long" unsaved-value="0">
<generator class="identity" />
</id>
<!-- property将类的field映射到表的列,此外还有map,list,set,或者one-to-many和many-to-one等进行entities的关联。具体的可以看Hibernate ORM的文档,我们在后面并不适用xml的方式来映射,仅作为介绍 -->
<property name="name" column="Name" type="string" length="60" />
<property name="description" column="Description" type="string" length="255" />
<property name="datePosted" column="DatePosted" type="java.time.Instant" />
<property name="purchases" column="Purchases" type="long" />
<property name="price" column="Price" type="double" />
<property name="bulkPrice" column="BulkPrice" type="double" />
<property name="minimumUnits" column="MinimumUnits" type="int" />
<property name="sku" column="Sku" type="string" length="12" />
<property name="editorsReview" column="EditorsReview" type="string" length="2000" />
</class>
</hibernate-mapping>
session API
Hibernate Session是执行transaction的单位,和JPA的EntityManager很是相似。
事务是一种机制、是一种操作序列,它包含了一组数据库操作命令,这组命令要么全部执行,要么全部不执行。因此事务是一个不可分割的工作逻辑单元。在数据库系统上执行并发操作时事务是作为最小的控制单元来使用的。这特别适用于多用户同时操作的数据通信系统。事务4大属性:
1 原子性(Atomicity):事务是一个完整的操作。
2 一致性(Consistency):当事务完成时,数据必须处于一致状态。
3 隔离性(Isolation):对数据进行修改的所有并发事务是彼此隔离的。
4 持久性(Durability):事务完成后,它对于系统的影响是永久性的。[1]
下面是有关的API:
//【Read】通过surrogate key获取entity
return (Product)session.get(Product.class, id);
//【Insert】增加entity,并返回生成的ID。如果已经存在ID,会更新这个ID。
session.save(product);
/*【Insert】同样是增加entity,并且更为安全。当事务关闭时,persist()不会执行INSERT(SQL)操作,而save()会开启一个额外的transaction来进行。save()可以马上获得ID,而persist()不保证一定执行,要在后面加上flush()才保证马上执行,因此它不会返回自动生成的ID。我们可以在后面加入flush()来获取。flush()将使当前的挂起的statement马上执行,但并不会关闭session。flush()还和Hibernate的statement队列有关,一个事务里面有多个动作,但这些动作的自行不一定是我们以为的顺序。在flush时候,自上个flush()后的执行顺序为:
* 1、按顺序的entity insert操作
* 2、按顺序的entity update操作
* 3、按顺序的collection的delete操作
* 4、按顺序的collection element的insert,update和delete操作
* 5、按顺序的collection的insert操作
* 6、按涮新的entity delete操作
* 通过flush()我们可以确保我们期待的顺序,需要注意save()是马上执行的,和flush()无关。我们使用flush()要非常小心,可能会引发混乱*/
session.persist(product);
session.flush();
//【Update】udpate()的用法有些特别,不允许session中,之前有过对entity有操作,即之前不对进行get,save,persist,否则或抛出异常
session.update(product);
//【Update】merge没有session中之前是否对entity有操作的限制,因此是更为常用的方式
session.merge(product);
//【Delete】
session.delete(product);
//【Evict】驱逐:从session中将entity驱逐或者分离,但对数据库没有实际影响。如果此时entity有尚未执行的statement,将被取消,不会再执行。如果我们需要驱逐所有的entity,使用clear()
session.envict(product);
session.clear();
使用surrogate key来查询当然不是唯一的方位,表格可以有其他的key,不是都要通过primary key的。可以通过org.hibernate.Criteria API或者 org.hibernate.Query API,下面是等价的return (Product)session.createCriteria(Product.class)
.add(Restrictions.eq("sku", sku))
.uniqueResult();
//里面的语句是HQL,类似于SQL,但用的是对象的概念,而不是表格,列。
return (Product)session.createQuery("FROM Product p WHERE p.sku = :sku")
.setString("sku", sku)
.uniqueResult();
下面是返回名字前缀是java,时间一年前的例子:return (List<Product>)session.createCriteria(Product.class)
.add(Restrictions.gt("datePosted",Instant.now().minus(365L, ChronoUnit.DAYS)))
.add(Restrictions.ilike("name", "java", MatchMode.START))
.addOrder()
.list();
return (List<Product>)session.createQuery("FROM Product p WHERE datePosted > :oneYearAgo AND name ILIKE :nameLike ORDER BY name")
.setParameter("oneYearAgo",Instant.now().minus(365L, ChronoUnit.DAYS))
.setString("nameLike", "java%")
.list();
sessionfactory
Session需要附着在JDBC的connection上面,SessionFactory提供了session获取的相关操作://开启新的session,采用缺省配置(DataSource,interceptors等)
Session session = sessionFactory.openSession();
//开启新的session,采用指定设置
Session session = sessionFactory.withOptions()
.connection(connection).openSession();
//开启新的session,拦截所有的SQL语句并进行修改,例如设置schema="@[email protected]"
Session session = sessionFactory.withOptions()
.interceptor(new EmptyInterceptor() {
@Override
public String onPrepareStatement(String sql){
return sql.replace("@[email protected]", schema);
}
}).openSession();
Hibernate ORM可以有stateless session,特别适合于大量数据的操作,我的理解,就是非事务性。开启无状态的session,可以通过.openStatelessSession()开启缺省的额无状态session或者通过.withStatelessOptions()开启定制的无状态会话。无状态session是不支持获得current
session的。//SessionFactory是Thread-safe,将返回当前线程中,之前开启的session。
Session session = sessionFactory.getCurrentSession();
在Spring Framework中创建SessionFactory Bean
Spring framework提供SessionFactory可以很好地管理Session的创建和关闭,避免资源访问出现内存泄漏。
对于XML方式,定义org.springframework.orm.hibernate5.LocalSessionFactoryBean,将返回SessionFactory.LocalSessionFactoryBean,同时实现org.springframework.dao.support.PersistenceExceptionTranslator接口,即将Hibernate的ORM异常转换为Spring framework通用的持久化异常。
XML配置如下[2]:
<bean id="sessionFactory" class="org.springframework.orm.hibernate5.LocalSessionFactoryBean">
<property name="dataSource" ref="dataSource" />
<property name="annotatedClasses" ref="hibernateClasses" />
<property name="hibernateProperties">
<props>
<prop key="hibernate.dialect">
${hibernate.dialect}
</prop>
<prop key="hibernate.show_sql">
${hibernate.show_sql}
</prop>
<prop key="hibernate.format_sql">true</prop>
<prop key="hibernate.generate_statistics">
${hibernate.generate_statistics}
</prop>
<prop key="hibernate.hbm2ddl.auto">
${hibernate.hbm2ddl.auto}
</prop>
</props>
</property>
</bean>
<bean id="transactionManager"
class="org.springframework.orm.hibernate5.HibernateTransactionManager">
<property name="sessionFactory" ref="sessionFactory" />
</bean>
Java代码配置如下,在root上下文中配置@EnableTransactionManagement
public class RootContextConfiguration implements AsyncConfigurer, SchedulingConfigurer{
...
@Bean
public PersistenceExceptionTranslator persistenceExceptionTranslator(){
return new HibernateExceptionTranslator();
}
@Bean
public HibernateTransactionManager transactionManager(){
HibernateTransactionManager manager = new HibernateTransactionManager();
manager.setSessionFactory(this.sessionFactory());
return manager;
}
@Bean
public SessionFactory sessionFactory(){
LocalSessionFactoryBuilder builder = new LocalSessionFactoryBuilder(this.dataSource());
builder.scanPackages("com.wrox.entities");
builder.setProperty("hibernate.default_schema", "dbo");
builder.setProperty("hibernate.dialect",MySQL5InnoDBDialect.class.getCanonicalName());
return builder.buildSessionFactory();
}
}
相关链接:
我的Professional Java for Web Applications相关文章