Logo

郎哥编程

使用Spring框架实现数据库事务处理

2018-11-01 800

企业级应用系统在更新数据库数据时,一般都采用数据库事务处理,以确保数据库数据的一致性。本课主要讨论在Spring框架中如何使用数据库事务处理更新数据库数据。通过本课的学习,可以达到如下目标。

●  了解JDBC对数据库事务处理的支持

●  掌握在Spring框架中使用事务处理的技术

事务处理对数据库来说,是对数据库的一系列SQL语句操作,这些操作被组织为一个事务。事务具有原子性,即事物中的操作要么全部执行,要么全部不执行。若事务中的SQL语句在执行过程中发生错误,事务需要对已经执行的SQL语句进行回滚操作,撤销先前对数据库的操作,防止数据库出现错误状态。

例如,在课程案例mooc数据库中,一个业务是学生购买课程,购买课程业务步骤包括更新teacher表,记录老师的收入,同时student_course表增加一条购买记录,更新student表的余额字段。上述业务步骤需要全部执行完毕,才能反映出学生购买课程的正确状态。如果因意外情况,上述操作仅成功执行了部分SQL语句,其它语句没有执行或执行失败,就会造成学生购买课程这个业务记录不完整,数据库处于数据错乱状态。若利用数据库事务技术执行上述操作,当发生上述情况时,数据库系统会将先前执行的SQL语句撤销,将数据库回滚到事务执行前状态。


1、JDBC对数据库事务处理的支持


JDBC本身就提供了对数据库事务处理的支持,使用java.sql.Connection对象完成事务的提交。使用Connection提交数据库事务处理代码如下。

Connection conn = DriverManager.getConnection();
    try { 
        conn.setAutoCommit(false);  //将自动提交设置为false                        
        需执行的SQL语句
        conn.commit();      //当两个操作成功后手动提交 
    } catch (Exception e) { 
        conn.rollback();    //一旦其中一个操作出错都将回滚,所有操作都不成功
        e.printStackTrace(); 
    } finally {
        conn.close();
}

Connection类的setAutoCommit方法用于设置JDBC提交SQL语句的方式。设置为ture时,JDBC自动提交SQL语句,JDBC提交SQL语句的方式默认为true。设置为false时,SQL语句的提交由应用程序负责,应用程序必须调用commit方法,同时要在执行SQL语句异常处理块中调用rollback方法,对异常发生前进行的数据库进行回滚操作。

在企业级应用中,事务一般是并发执行的,当事务并发执行时,就会发生数据库数据同步的问题,具体问题可分为下面四种类型。

(1)脏读:一个数据库事务在更新数据的过程中,数据是保存在内存中的,只有调用commit方法,更新的数据才最终写到数据库中。如果一个事务使用了另一个事务更新但没保存的数据,这个数据就称为脏数据,事务读取这个数据就称为脏读。

(2)不可重复读:在同一事务中,两次读取同一记录,读取的记录内容却不相同。例如,事务A第一次读取了一条记录,同时事务B更新并提交了该条记录,事务A第二次读取该条记录时,当前读取的记录内容和第一次读取的记录内容不相同。

(3)幻读:当事务A对表中的所有记录进行了修改,同时事务B又在表中插入了一条新的记录。A事务在后续对该表操作时,就会发现表中还存在没有修改的记录,就好象发生了幻觉一样。

(4)丢失更新:当事务A和事务B对同一数据进行修改时,就会发生丢失更新的问题。例如,B事务对m记录进行了修改,A事务在修改m记录时发生异常并回滚,就会将B事务对m记录的修改覆盖掉。

为了解决上面提到的事务并发问题,JDBC定义了五种事务隔离级别来解决来解决这些并发导致的问题。

表1 JDBC提供的事务隔离级别说明

image.png

表中隔离级别从上到下依次增高,最高级别是TRANSACTION_SERIALIZABLE,它通过强制事务串行执行(不是并行),避免了事务并发执行导致发生的数据同步问题。出于应用程序访问数据库性能的考虑,一般设置隔离级别为TRANSACTION_READ_COMMITTED。


2、在Spring框架中调用事务处理


下面给出具体执行事务处理的案例程序。案例程序的数据源采用mooc数据库,mooc数据库的结构以及本案例中没有列出的代码详见《Spring使用JDBC访问MySQL数据库》一文。

让Spring框架开始执行一个数据库事务时,需要分成三步走。第一步配置数据源DataSource;第二步声明事务管理TransactionManager类;第三步定义可以执行事务的DAO类。

第一步:配置数据源DataSource

需要让Spring框架知道数据库的位置及其连接方式,这个工作由DataSource完成。Spring框架通过DataSource连接数据库,DataSource即可以在Spring框架的配置文件中配置,也可以放在Bean类中配置。下面是在Spring配置文件中配置数据源的代码。

<!-- 配置数据源 -->
   <bean id="dataSource"
      class="org.springframework.jdbc.datasource.DriverManagerDataSource">
      <property name="driverClassName" value="com.mysql.jdbc.Driver"/>
      <property name="url" value="jdbc:mysql://localhost:3306/mooc?characterEncoding=utf8"/>
      <property name="username" value="root"/>
      <property name="password" value="~123456q"/>
   </bean>

配置语句定义了MySQL数据库的URL地址、访问账户及访问密码。

第二步:声明事务管理TransactionManager

Spring 框架提供了PlatformTransactionManager作为事务管理类的顶层接口,声明了初始化事务、提交事务、回滚事务等接口。接口实现由具体的数据库驱动类实现。具体包括DataSourceTransactionManager、HibernateTransactionManager等实现类。本课主要用DataSourceTransactionManager来实现事务的管理。在Spring框架配置文件中配置DataSourceTransactionManager类。

<!-- Initialization for TransactionManager -->
   <bean id="transactionManager"
      class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
      <property name="dataSource"  ref="dataSource" />   
   </bean>

声明DataSourceTransactionManager类,需要传入之前已声明的DataSource数据源。

第三步:定义可以执行事务的DAO类

DAO提供了应用程序访问数据源必要的接口和方法,接口和方法的具体实现细节,程序并不需要了解。DAO的具体细节详见《Spring使用JDBC访问MySQL数据库》一文。

public class CourseTransactio implements CourseDao {
private DataSource dataSource;
private JdbcTemplate jdbcTemplateObject;
private PlatformTransactionManager transactionManager;
@Override
public void setDataSource(DataSource ds) {
    // TODO Auto-generated method stub
    this.dataSource = ds;
    this.jdbcTemplateObject = new JdbcTemplate(dataSource);
}
 
@Override
public void insertCourse(Course inCourse) {
    // TODO Auto-generated method stub
    return;
}
 
public void setTransactionManager(PlatformTransactionManager transactionManager) {
    this.transactionManager = transactionManager;
}
 
@Override
public Course getCourse(String number) {
    // TODO Auto-generated method stub
    String SQL = "select * from course where number = ?";
    Course course = jdbcTemplateObject.queryForObject(SQL, new Object[] { number }, new CourseMapper());
    return course;
}
 
@Override
public List<Course> listCourse() {
    // TODO Auto-generated method stub
    String SQL = "select * from course";
    List<Course> students = jdbcTemplateObject.query(SQL, new CourseMapper());
    return students;
}
 
@Override
public void delete(String number) {
    // TODO Auto-generated method stub
    return;
 
}
 
@Override
public void update(Course inCourse) {
    // TODO Auto-generated method stub
    return;
 
}
 
@Override
public String getCoureName(String number) {
    // TODO Auto-generated method stub
    return "";
}
 
@Override
public void buyCourse(String courseNumber, String studenNember) {
    // TODO Auto-generated method stub
    TransactionDefinition def = new DefaultTransactionDefinition();
    TransactionStatus status = transactionManager.getTransaction(def);
    try {
        //获取课程信息
        Course  course =  getCourse(courseNumber);
        //获取课程老师信息
        String teacherSQL = "select * from teacher where number = ?";
        MoocTeacher teacher = (MoocTeacher)jdbcTemplateObject.queryForObject(teacherSQL, new Object[] { course.getTeacher_number() }, new TeacherMapper());
        //更新老师收入
        float allincome = teacher.getIncome() + course.getPrice();
            String updateIncomeSQL = "update teacher SET income = ?";
            jdbcTemplateObject.update(updateIncomeSQL,allincome);
            //新增课程购买记录
         String buSQL = "insert into "
                + "student_course (number,course_number,teacher_number,buy_time)"
                + " values (?,?,?,?)";    
         jdbcTemplateObject.update(buSQL,"0001",
             course.getNumber(),
             teacher.getNumber(),
             new Date());
         System.out.println("插入购买课程记录 = " + course.getName());
         //更新购买课程学生的余额
         String studentSQL = "select * from student where number = ?";
        MoocStudent student = (MoocStudent)jdbcTemplateObject.queryForObject(studentSQL, new Object[] { studenNember }, new StudentMapper());
        float balance = student.getBalance() - course.getPrice();
            String updatBalanceSQL = "update student SET balance = ?";
            jdbcTemplateObject.update(updatBalanceSQL,balance);
            transactionManager.commit(status);
    } catch (DataAccessException e) {
        System.out.println("Error in creating record, rolling back");
        transactionManager.rollback(status);
        throw e;
    }
 
}
}

CourseTransactio类实现了CourseDao接口,CourseDao是访问mooc数据库的顶层接口,接口提供了mooc数据库表的增删改查操作。CourseTransactio主要实现了CourseDao类的buyCourse,实现学生购买课程事务。购买课程事务涉及到更新teacher表,在student_course表增加一条购买记录,更新student表的余额字段数据库操作,这些操作需要进行连续处理,中间不能中断出错,如果出错则需要做回滚处理。 因此,buyCourse方法采用数据库事务进行处理。

事务处理从TransactionDefinition开始,前面我们谈到了事务的隔离级别,TransactionDefinition就是用来定义事务隔离级别的。DefaultTransactionDefinition表示使用数据库的默认隔离级别,对大部分数据库而言,默认隔离级别是TRANSACTION_READ_COMMITTED。

当TransactionDefinition 创建后,可以通过调用 getTransaction方法开始事务,该方法会返回 TransactionStatus 的一个实例。 TransactionStatus 用于追踪当前的事务状态,如果后面的SQL语句都运行成功,可以使用 TransactionManager 的 commit() 方法来提交这个事务,否则使用 rollback() 方法来回滚整个操作。

完整的配置文件代码。

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
    http://www.springframework.org/schema/beans/spring-beans-3.0.xsd ">
   <!-- 配置数据源 -->
   <bean id="dataSource"
      class="org.springframework.jdbc.datasource.DriverManagerDataSource">
      <property name="driverClassName" value="com.mysql.jdbc.Driver"/>
      <property name="url" value="jdbc:mysql://localhost:3306/mooc?characterEncoding=utf8"/>
      <property name="username" value="root"/>
      <property name="password" value="~123456q"/>
   </bean>
   <!-- Initialization for TransactionManager -->
   <bean id="transactionManager"
      class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
      <property name="dataSource"  ref="dataSource" />   
   </bean>
   <!-- Definition for courseJDBCTemplate bean -->
   <bean id="transactionJDBCTemplate"
      class="com.milihua.springprogram.jdbc.CourseTransactio">
      <property name="dataSource"  ref="dataSource" />
      <property name="transactionManager"  ref="transactionManager" />     
   </bean>
</beans>

学生实体类代码。

package com.milihua.springprogram.entity;
public class MoocStudent {
/*
 * 学生编号
 */
private String number;
/*
 * 学生余额
 */
private float balance;
/*
 * 学生名称
 */
private String name;
/*
 * 学生年龄
 */
private int age;
public String getNumber() {
    return number;
}
public void setNumber(String number) {
    this.number = number;
}
public float getBalance() {
    return balance;
}
public void setBalance(float balance) {
    this.balance = balance;
}
public String getName() {
    return name;
}
public void setName(String name) {
    this.name = name;
}
public int getAge() {
    return age;
}
public void setAge(int age) {
    this.age = age;
}
}

老师实体类代码。

package com.milihua.springprogram.entity;
public class MoocTeacher {
/*
 * 老师编号
 */
private String number;
/*
 * 老师收入
 */
private float income;
/*
 * 老师简介
 */
private String brief;
/*
 * 老师名称
 */
private String name;
public String getNumber() {
    return number;
}
public void setNumber(String number) {
    this.number = number;
}
public float getIncome() {
    return income;
}
public void setIncome(float income) {
    this.income = income;
}
public String getBrief() {
    return brief;
}
public void setBrief(String brief) {
    this.brief = brief;
}
public String getName() {
    return name;
}
public void setName(String name) {
    this.name = name;
}
 
}

student表映射到学生实体类。

public class StudentMapper implements RowMapper<MoocStudent>{
@Override
public MoocStudent mapRow(ResultSet rs, int arg1) throws SQLException {
    // TODO Auto-generated method stub
    MoocStudent  student = new MoocStudent();
    student.setName(rs.getString("name"));
    student.setAge(rs.getInt("age"));
    student.setBalance(rs.getFloat("balance"));
    student.setNumber(rs.getString("number"));
    return student;
}
}

teacher表映射到老师实体类。

public class TeacherMapper implements RowMapper<MoocTeacher>{
@Override
public MoocTeacher mapRow(ResultSet rs, int arg1) throws SQLException {
    // TODO Auto-generated method stub
   
    MoocTeacher  teacher = new MoocTeacher();
    teacher.setName(rs.getString("name"));
    teacher.setBrief(rs.getString("brief"));
    teacher.setIncome(rs.getFloat("income"));
    teacher.setNumber(rs.getString("number"));
    return teacher;
}
 
}

测试类代码。

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import com.milihua.springprogram.jdbc.CourseTransactio;
public class TransactionTest {
public static void main(String[] args) {
    // TODO Auto-generated method stub
    ApplicationContext context = new ClassPathXmlApplicationContext("config/transactio.xml");
    CourseTransactio courseTransactio =
            (CourseTransactio) context.getBean("transactionJDBCTemplate");
    courseTransactio.buyCourse("0001","0001");
}
}

课程小结

(1)事务处理对数据库来说,是对数据库的一系列SQL语句操作,这些操作被组织为一个事务。事务具有原子性,即事物中的操作要么全部执行,要么全部不执行。若事务中的SQL语句在执行过程中发生错误时,事务需要对已经执行的SQL语句进行回滚操作,撤销先前对数据库的操作,防止数据库出现错误状态。

(2)让Spring框架开始执行一个数据库事务时,需要分成三步走。第一步配置数据源DataSource;第二步声明事务管理TransactionManager类;第三步定义可以执行事务的DAO类。


代码在线纠错(通义千问 qwen-max)

支持粘贴多个代码文件,提交后由阿里云通义千问自动分析代码漏洞、语法错误、逻辑问题并给出修改建议。
您已解锁 AI 代码纠错功能,可正常使用!

评论区

登录 后发表评论
暂无评论