章节索引

  • 前提
  • 数据库事务
  • 数据库事务的四个特性
  • 数据库并发的经典问题和事务隔离
  • 事务传播行为
  • 基于配置文件配置事务管理
  • Hibernate5 初体验
  • 集成Hibernate5的配置
  • 后记



前提

这篇博文是这套Spring学习笔记的第八篇——数据访问篇,主要内容包括Spring数据访问、事务管理,以及一个例子对Hibernate做一个简单的介绍。


数据库事务

关于数据库事务,以下是百度百科的定义:

数据库事务(Database Transaction) ,是指作为单个逻辑工作单元执行的一系列操作,要么完全地执行,要么完全地不执行。 事务处理可以确保除非事务性单元内的所有操作都成功完成,否则不会永久更新面向数据的资源。通过将一组相关操作组合为一个要么全部成功要么全部失败的单元,可以简化错误恢复并使应用程序更加可靠。

举一个例子,A和B两个人使用同一个银行系统,A和B的账户中各有200元,A向B转账100元。在数据库中包含以下两个操作:

① A = A - 100 = 100;
② B = B + 100 = 300;

如果当①执行完后,服务器因其他因素(如遭受了DoS攻击)崩溃了,那么当系统恢复服务后,A会损失100元,变为100元,而B并没有收到那100元,仍是200元。

因此,当我们将①和②组合为一个事务单元,当②执行失败后,系统会对①操作进行回滚。这样,虽然没有完成既定的操作,但是挽回了损失。

数据库事务的四个特性

一个数据库事务必须同时满足以下四个特性:
①原子性
一个事物是由多个数据库操作组成的单元。只有所有的操作执行成功,整个事务才提交,事务中任何一个数据库操作失败,已经执行的任何操作都必须撤销,让数据库返回到初始状态。
②一致性
一致性是指事务必须使数据库从一个一致性状态变换到另一个一致性状态。
如上述A和B,他们两个人一共有400元。无论他们之间怎么转账,转多少次帐,他们的账户总额应该还是400元。
③隔离性
隔离性是在并发数据操作时,不同的事务拥有各自的数据空间,各自的操作不会干扰对方,多个并发事务之间要相互隔离。数据库规定了多种事务隔离,隔离级别越高,数据一致性越好,但并发性会越差。
④持久性
当事务提交后,事务中的所有数据操作就必须被持久化到数据库中。

数据库并发的经典问题和事务隔离

数据库中有多个用户进程并发执行数据操作时,将产生一些并发问题
数据库的事务隔离就是为了避免这些问题,在进程之间进行一定程序上的隔离。隔离程度越高,并发程度越低。下面是现行SQL标准中的四种事务隔离:

隔离级别

脏读

不可重复读

幻象读

第一类丢失更新

第二类丢失更新

READ UNCOMMITED

允许

允许

允许

不允许

允许

READ COMMITED

不允许

允许

允许

不允许

允许

REPEATABLE READ

不允许

不允许

允许

不允许

不允许

SERIALIZABLE

不允许

不允许

不允许

不允许

不允许


事务传播行为

在Spring中,事务会随着Service类(即被冠以@Service注解的类)的嵌套调用而传播。Spring重定义了以下的几种事务传播类型:

事务传播行为

说明

PROPAGATION_REQUIRED

如果没有当前事务,就创建一个新的事务;否则加入当前事务。

PROPAGATION_SUPPORTS

支持当前事务;如果没有当前事务,就以非事务方式执行。

PROPAGATION_MANDATORY

使用当前事务;如果不存在当前事务,则抛出异常。

PROPAGATION_REQUIRES_NEW

新建事务;如果存在当前事务,挂起当前事务。

PROPAGATION_NOT_SUPPORTED

以非事务方式执行;如果存在当前事务,挂起当前事务。

PROPAGATION_NEVER

以非事务方式执行;如果存在当前事务,则抛出异常。

PROPAGATION_NESTED

如果存在当前事务,则在嵌套事务内执行;否则,执行与PROPAGATION_REQUIRED类似的操作。


基于配置文件配置事务管理

之前,由于我们没有弄懂AOP和事务的知识,因此在《配置篇》中仅仅贴了示例代码,现在是时候融会贯通一下了。为了便于讲解,再次列出代码:

<!-- 配置事务管理器 -->
    <bean id="transactionManager"
          class="org.springframework.jdbc.datasource.DataSourceTransactionManager"
          p:dataSource-ref="dataSource" />

    <!-- 通过tx命名空间定义事务增强 -->
    <tx:advice id="txAdvice" transaction-manager="transactionManager">
        <tx:attributes>
            <tx:method name="*" />
        </tx:attributes>
    </tx:advice>

    <!-- 通过AOP配置提供事务增强,让service包下所有Bean的所有方法拥有事务 -->
    <aop:config proxy-target-class="true">
        <aop:pointcut id="serviceMethod"
                      expression=" execution(* com.implementist.MyFirstWebApp.service..*(..))" />
        <aop:advisor pointcut-ref="serviceMethod" advice-ref="txAdvice" />
    </aop:config>

解释:
①先创建一个事务管理器transactionManager,类指定为Spring提供的数据源事务管理器。dataSource引用之前定义的dataSource(这部分代码在博文中多次出现过,不徒增篇幅了)。
②通过tx命名空间定义事务增强,其中transaction-manager引用了上面定义的transactionManager。<tx:method>有以下属性:

属性是否必须默认值说明name是无与事务关联的方法名,可使用通配符“*”,如“handle*”propagation否REQUIRED事务传播行为,可选的值有:REQUIRED、SUPPORTS、MANDATORY、REQUIRES_NEW、NOT_SUPPORTED、NEVER和NESTEDisolation否DEFAULT事务隔离等级,可选的值有:DEFAULT、READ_UNCOMMITTED、READ_COMMITTED、REPEATABLE_READ和SERIALIZABLEtimeout否-1事务超时时间,设置为-1时将交由底层事务系统决定read-only否false事务是否只读rollback-for否所有运行期异常触发事务回滚的异常,指定多个时以逗号隔开,如"NullPointerException,ClassNotFoundException"no-rollback-for否所有检查型异常不触发回滚的异常,指定多个时以逗号隔开,如"IORangeException,FileNotFoundException"

③配置AOP事务增强,包含切点和切面,其中,切面又引用了上述切点和事务增强。


Hibernate5 初体验

《配置篇》中,我们使用过JDBCTemplate来简化DAO类的代码。为了便于讲解,再次列出代码。

UserDAO

@Repository
public class UserDAO {

    @Autowired  
    private JdbcTemplate jdbcTemplate;

    User user = null;

    public User queryUserByUserName(String username) {
        String sqlStatement = "SELECT * FROM user WHERE UserName=?";
        jdbcTemplate.query(sqlStatement, new Object[]{username}, (rs) -> {
            if (rs.isFirst()) {
                user = new User();
                user.setId(rs.getInt("Id"));
                user.setUsername(username);
                user.setPassword(rs.getString("Password"));
            }
        });
        return user;
    }
}

可以看到最后面有四行是将从数据库中查询出来的用户信息封装到一个User类的对象中去。这个工程中,User类和对应的user表中只有Id、UserName和Password三个字段,所以我们有三行set,如果我们今后扩展功能,字段变到20个,就必定需要20行set。 我当时就设想能不能有一个函数可以帮我们完成封装的操作呢?型如:

User user = rs.toDomain();

术业有专攻,在持久化层(即数据层)有比Spring做的更好的框架,如Hibernate、MyBaits等。我先给大家展示一下Hibernate5中对这个需求的实现,让大家尝尝鲜,Hibernate的详细知识,就是下一套博文的事了。

@Repository
public class UserDAO {

    @Autowired
    private SessionFactory sessionFactory;  //注释①

    public User queryUserByUserName(String username) {
        String sqlStatement = "SELECT * FROM User user WHERE user.username=:username";  //注释②
        NativeQuery<User> query = sessionFactory.getCurrentSession().createNativeQuery(sqlStatement);  //注释③
        query.addEntity(User.class);  //注释④
        query.setParameter("username", username);  //注释⑤
        return query.uniqueResult();  //注释⑥
    }
}

这是集成了Hibernate5之后,实现同样功能的代码,这样的代码就不需要随着字段的增减而改动了。

注释:
①会话工厂,其中存储了数据源等信息,可通过它获取当前会话;
②注意,这行SQL表达式和JDBC中的不一样,这里的字段没有用数据库字段UserName等,而用的是User类中相对应的字段username等,这是Hibernate的重要贡献之一,它实现了让开发人员只需要关注业务,而无需关注数据。
③通过会话工厂获取当前会话,并通过SQL表达式创建本地查询。注意query的类型NativeQuery,这里使用了泛型技术,指定User类与sqlStatement里的User类对应;
④想查询中添加实体,这也就是指定要将查询结果封装成哪个类型,这里指定了User类;
⑤向SQL表达式中的username传入值,需要注意的是,传统的 ‘?’ 方式已经过时,Hibernate5提示使用新的 ‘:参数名’ 方式;
⑥查询单一结果,如果存在,会返回封装好的对象,否则返回null。


集成Hibernate5的配置

①下载Hibernate源码;
首先去Hibernate官方资源站选择一个版本的Hibernate源码压缩包下载下来。
②解压缩,将lib->required包下的所有Jar包全部添加到工程的库中去;
③需要额外添加jta的Jar包添加到库中去;
jta:链接 密码:v185
④修改User类,为其添加与user表的映射:

@Entity
@Table(name = "user")  
public class User implements Serializable {

    @Id
    @Column(name = "Id")  
    private int id;
    
    @Column(name = "UserName")
    private String username;
    
    @Column(name = "Password")
    private String password;
    
    //getter\setter省略
}

解释:
(1)@Entity注解标识User类是一个实体类;
(2)@Table注解标识User对应数据库中的user表;
(3)@Id注解标识id字段是主键;
(4)@Column注解标识各属性对应user数据库中的哪个字段

上述DAO中“一键”封装的功能就是依靠这些注解完成的映射。由此可以看出,字段的增减并不会影响各DAO函数。这又是Hibernate的大功一件。

⑤修改applicationContext.xml中的部分配置:
(1)配置Hibernate会话工厂SessionFactory

<bean id="sessionFactory" class="org.springframework.orm.hibernate5.LocalSessionFactoryBean"
          p:dataSource-ref="dataSource"> <!-- 注释① -->
        <property name="packagesToScan" value="com.implementist.jiyizahuodian.domain"/>  <!-- 注释② -->
        <property name="hibernateProperties">
            <props>
                <prop key="hibernate.dialect">org.hibernate.dialect.MySQLInnoDBDialect</prop>
                <prop key="hibernate.show_sql">true</prop>  <!-- 注释③ -->
            </props>
        </property>
    </bean>

注释:
①引用dataSource,这个dataSource就是之前定义的,不需要改变;
②制定需要扫描的包,指向存放实体类的domain包;
③这里设置为true后,每次请求时,在服务器的控制台上可以看到请求表达式。

(2)修改事务管理器transactionManager,这里把class指向了HibernateTransactionManager,为它添加了sessionFactory的引用。

<bean id="transactionManager" class="org.springframework.orm.hibernate5.HibernateTransactionManager"
          p:sessionFactory-ref="sessionFactory"/>

因为各Bean的命名都没有改变,所以AOP事务增强那里什么都不用改,Spring和Hibernate5的及成就完成了。


后记

1. Spring的 JDBCTemplate 和 Hibernate这两部分持久化层的知识真的是让我涨见识了。学会之后,就有一种原始人学会用火的感觉,怎一个“爽”字了得!

2. 关于数据库并发的问题,我略有些不解,如果取到数据库中A = 10。客户端用完之后告诉数据库 A = A - 1就行了,这个时候根本不用管A现在被其他的进程操作之后变成了几,为什么非要 A = 9呢?
根据目前的理解,我觉得按上述操作虽然不能解决不可重复读和幻象读的问题,但至少可以解决脏读和两类丢失更新问题,因为我认为这三个问题都发生在写操作的方法上。