企业级应用系统在更新数据库数据时,一般都采用数据库事务处理,以确保数据库数据的一致性。本课主要讨论在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提供的事务隔离级别说明

表中隔离级别从上到下依次增高,最高级别是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类。