废话不多说,我们先搭建一个简单的spring声明式事务的小demo
demo的背景:保存用户信息,分三部分保存,用户基本信息,用户详细信息,用户职业信息,当新增保存用户的时候,必须保存好用户的详细,否则就全部不保存,但如果保存用户的时候失败的时候,用户对应的职业可以保存,我们假设职业和用户不是强相关的
先定义对象模型:
User.java
package org.study.spring.transaction.entity;
import java.io.Serializable;
public class User implements Serializable{
/**
*
*/
private static final long serialVersionUID = 1L;
private Integer id;
private String name;
private Integer age;
/**
* 职业id
*/
private Integer professionId;
public User() {
}
public User(String name, Integer age) {
this.name = name;
this.age = age;
}
public User(String name, Integer age,Integer professionId) {
this.name = name;
this.age = age;
this.professionId = professionId;
}
//Getter/Setter省略
}
用户详细模型
UserDetail.java
package org.study.spring.transaction.entity;
import java.io.Serializable;
public class UserDetail implements Serializable{
/**
*
*/
private static final long serialVersionUID = 1L;
private Integer id;
private Integer userId;
private String mail;
private String address;
private String school;
public UserDetail() {
}
public UserDetail(Integer userId, String mail, String address, String school) {
this.userId = userId;
this.mail = mail;
this.address = address;
this.school = school;
}
public UserDetail(String mail, String address, String school) {
this.mail = mail;
this.address = address;
this.school = school;
}
//Getter/Setter省略
}
职业信息:
Profession.java
package org.study.spring.transaction.entity;
import java.io.Serializable;
public class Profession implements Serializable{
/**
*
*/
private static final long serialVersionUID = 1L;
private Integer id;
/**
* 职业名称
*/
private String name;
/**
* 所属行业
*/
private String category;
public Profession() {
}
public Profession(String name, String category) {
this.name = name;
this.category = category;
}
//Getter/Setter省略
}
我们使用最基本的Spring的JdbcTemplate完成对数据库的操作:
UserDao.java接口
package org.study.spring.transaction.dao;
import org.study.spring.transaction.entity.User;
public interface UserDao {
/**
* 插入并返回id
* @param u
* @return
*/
Integer insertReturnPK(User u);
}
具体实现类:
UserDaoImpl.java
package org.study.spring.transaction.dao.impl;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import javax.annotation.Resource;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.PreparedStatementCreator;
import org.springframework.jdbc.support.GeneratedKeyHolder;
import org.springframework.jdbc.support.KeyHolder;
import org.springframework.stereotype.Component;
import org.study.spring.transaction.dao.UserDao;
import org.study.spring.transaction.entity.User;
@Component
public class UserDaoImpl implements UserDao{
@Resource
private JdbcTemplate jdbcTemplate;
public Integer insertReturnPK(User u) {
KeyHolder keyHolder = new GeneratedKeyHolder();
final String sql = "insert into user (name,age,profession_id) values ('"+u.getName()+"',"+u.getAge()+","+u.getProfessionId()+")";
jdbcTemplate.update(new PreparedStatementCreator() {
public PreparedStatement createPreparedStatement(Connection con) throws SQLException {
PreparedStatement ps = con.prepareStatement(sql,PreparedStatement.RETURN_GENERATED_KEYS);
return ps;
}
},keyHolder);
return keyHolder.getKey().intValue();
}
}
UserDetailDao.java 接口
package org.study.spring.transaction.dao;
import org.study.spring.transaction.entity.UserDetail;
public interface UserDetailDao {
Integer insert(UserDetail ud);
}
具体实现类UserDetailImpl.java
package org.study.spring.transaction.dao.impl;
import javax.annotation.Resource;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Component;
import org.study.spring.transaction.dao.UserDetailDao;
import org.study.spring.transaction.entity.UserDetail;
@Component
public class UserDetailImpl implements UserDetailDao{
@Resource
private JdbcTemplate jdbcTemplate;
public Integer insert(UserDetail ud) {
return jdbcTemplate.update("insert into user_detail (user_id,mail,address,school) values(?,?,?,?)",ud.getUserId(),ud.getMail(),ud.getAddress(),ud.getSchool());
}
}
ProfessionDao.java接口
package org.study.spring.transaction.dao;
import org.study.spring.transaction.entity.Profession;
public interface ProfessionDao {
/**
* 插入并返回id
* @param pf
* @return
*/
Integer insertReturnPK(Profession pf);
}
ProfessionDaoImpl.java实现类:
package org.study.spring.transaction.dao.impl;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import javax.annotation.Resource;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.PreparedStatementCreator;
import org.springframework.jdbc.support.GeneratedKeyHolder;
import org.springframework.jdbc.support.KeyHolder;
import org.springframework.stereotype.Component;
import org.study.spring.transaction.dao.ProfessionDao;
import org.study.spring.transaction.entity.Profession;
@Component
public class ProfessionDaoImpl implements ProfessionDao{
@Resource
private JdbcTemplate jdbcTemplate;
public Integer insertReturnPK(Profession pf) {
KeyHolder keyHolder = new GeneratedKeyHolder();
final String sql = "insert into profession (name,category) values ('"+pf.getName()+"','"+pf.getCategory()+"')";
jdbcTemplate.update(new PreparedStatementCreator() {
public PreparedStatement createPreparedStatement(Connection con) throws SQLException {
PreparedStatement ps = con.prepareStatement(sql,PreparedStatement.RETURN_GENERATED_KEYS);
return ps;
}
},keyHolder);
return keyHolder.getKey().intValue();
}
}
最后一个就是配置文件
spring-transaction.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.1.xsd
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.2.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd">
<context:component-scan base-package="org.study.spring.transaction" />
<tx:annotation-driven transaction-manager="txManager" />
<bean class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
<property name="locations">
<list>
<value>classpath:jdbc.properties</value>
</list>
</property>
</bean>
<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="driverClassName" value="${jdbc.driver}"></property>
<property name="url" value="${jdbc.url}"></property>
<property name="username" value="${jdbc.username}"></property>
<property name="password" value="${jdbc.password}"></property>
</bean>
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="dataSource"></property>
</bean>
<bean id="txManager"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource" />
</bean>
</beans>
jdbc.properites
jdbc.driver= com.mysql.jdbc.Driver
jdbc.url= jdbc:mysql://localhost:3306/spring?useUnicode=true&characterEncoding=UTF-8
jdbc.username= root
jdbc.password=
数据库sql:
SET FOREIGN_KEY_CHECKS=0;
DROP TABLE IF EXISTS `profession`;
CREATE TABLE `profession` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(255) NOT NULL,
`category` varchar(255) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(50) NOT NULL,
`age` int(11) NOT NULL,
`profession_id` int(11) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
DROP TABLE IF EXISTS `user_detail`;
CREATE TABLE `user_detail` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`user_id` int(11) NOT NULL,
`mail` varchar(50) NOT NULL,
`address` varchar(50) NOT NULL,
`school` varchar(50) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
好了,写了这么一大堆东西做铺垫,也是累~
基本的serviceUserDetailService.java
package org.study.spring.transaction.service;
import org.study.spring.transaction.entity.UserDetail;
public interface UserDetailService {
void insert(UserDetail ud);
}
UserDetailServiceImpl.java
package org.study.spring.transaction.service.impl;
import javax.annotation.Resource;
import org.springframework.stereotype.Service;
import org.study.spring.transaction.dao.UserDetailDao;
import org.study.spring.transaction.entity.UserDetail;
import org.study.spring.transaction.service.UserDetailService;
@Service
public class UserDetailServiceImpl implements UserDetailService{
@Resource
private UserDetailDao userDetailDao;
public void insert(UserDetail ud) {
userDetailDao.insert(ud);
}
}
ProfessionService.java
package org.study.spring.transaction.service;
import org.study.spring.transaction.entity.Profession;
public interface ProfessionService {
Integer insert(Profession pf);
}
ProfessionServiceImpl.java
package org.study.spring.transaction.service.impl;
import javax.annotation.Resource;
import org.springframework.stereotype.Service;
import org.study.spring.transaction.dao.ProfessionDao;
import org.study.spring.transaction.entity.Profession;
import org.study.spring.transaction.service.ProfessionService;
@Service
public class ProfessionServiceImpl implements ProfessionService{
@Resource
private ProfessionDao professionDao;
public Integer insert(Profession pf) {
return professionDao.insertReturnPK(pf);
}
}
①我们先写一下这么一个业务场景:保存一个用户,同时保存他的详细信息和职业信息,要么全部成功,要么全部失败
我们先写一个
UserService
package org.study.spring.transaction.service;
import org.study.spring.transaction.entity.Profession;
import org.study.spring.transaction.entity.User;
import org.study.spring.transaction.entity.UserDetail;
public interface UserService {
void saveUserAllInfo(User u,UserDetail ud,Profession pf);
}
UserServiceImpl.java
package org.study.spring.transaction.service.impl;
import javax.annotation.Resource;
import org.springframework.stereotype.Service;
import org.study.spring.transaction.dao.UserDao;
import org.study.spring.transaction.entity.Profession;
import org.study.spring.transaction.entity.User;
import org.study.spring.transaction.entity.UserDetail;
import org.study.spring.transaction.service.ProfessionService;
import org.study.spring.transaction.service.UserDetailService;
import org.study.spring.transaction.service.UserService;
@Service(value="userServiceImpl")
public class UserServiceImpl implements UserService{
@Resource
private UserDao userDao;
@Resource
private ProfessionService professionService;
@Resource
private UserDetailService userDetailService;
public void saveUserAllInfo(User u, UserDetail ud, Profession pf) {
Integer pfId = professionService.insert(pf);
Integer id = userDao.insertReturnPK(new User(u.getName(), u.getAge(), pfId));
userDetailService.insert(new UserDetail(id, ud.getMail(), ud.getAddress(), ud.getSchool()));
}
}
测试类
package org.study.spring.test;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.study.spring.transaction.entity.Profession;
import org.study.spring.transaction.entity.User;
import org.study.spring.transaction.entity.UserDetail;
import org.study.spring.transaction.service.UserService;
public class SpringTransactionTest {
@Test
public void test1() throws IllegalAccessException{
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-transaction.xml");
UserService userBussinessServiceImpl = applicationContext.getBean("userServiceImpl",UserService.class);
userBussinessServiceImpl.saveUserAllInfo(new User("Lyncc",21), new UserDetail("lyncc@163.com", "苏胜路长乐街南山巴黎印象", "苏州大学"), new Profession("程序员","互联网"));
}
}
数据库结果:
好了,貌似成功了,全部插入成功了,貌似也达到了我们的目的
但是如果我们修改一下代码UserServiceImpl.java
再运行一下测试类,显示测试失败,我们再看下数据库
可以看到user_detail这张表没有保存成功,但其他两张表都成功了,这就违反了数据库事务的原则了,没有从一个一致性状态转化到另一个一致性状态
好了,spring的声明式事务很简单,在spring-transaction.xml中已经配置过了
现在只需要小小的修改一下,这也是我们平时用的最多的配置
再次运行测试类,发现数据库没有任何改变,说明事务生效了,做到了我们想要的要么全部插入要么全部失败的效果
好了,我们再看下这个Annotation------@Transactional
package org.springframework.transaction.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.transaction.TransactionDefinition;
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Transactional {
//事务名称的修饰符
String value() default "";
//事务的传播种类
Propagation propagation() default Propagation.REQUIRED;
//事务的隔离级别
Isolation isolation() default Isolation.DEFAULT;
//事务的超时
int timeout() default TransactionDefinition.TIMEOUT_DEFAULT;
//是否只是可读
boolean readOnly() default false;
//根据什么class可以回滚
Class<? extends Throwable>[] rollbackFor() default {};
//根据什么class名称可以回滚
String[] rollbackForClassName() default {};
//什么样的异常class可以不回滚
Class<? extends Throwable>[] noRollbackFor() default {};
//什么样的异常classname可以不会滚
String[] noRollbackForClassName() default {};
}
可以看得出来,这个annotation虽然很小巧,但是还是五脏俱全,基本上所有的功能都涵盖了,而且对于开发者来说,很好用,Spring帮助开发者屏蔽了事务的实现细节,更多的去关注业务
我们可以试试这几个属性,我们先试试rollbackfor,这个属性是告诉spring如果被管理的代码中,发生了哪些类型的异常就必须回滚,我们刚才故意产生的异常是应该数据库user_id字段不能为空,现在我们修改一下代码:
我们手动抛出一个IllegalAccessException 但我们只回滚数据越界异常,所以数据库并没有回滚
修改rollbackfor的异常与抛出的异常一致:
再执行测试类,发现产生了回滚,并没有插入
②测试@Transactional的readOnly属性
运行测试类,显示异常:
异常显示数据库链接只是可读的,你的sqlQuery会导致数据库数据修改,所以是不被允许的~
③测试@Transactional的timeout属性,修改代码:
timeout的单位是秒,我们设置程序睡四秒,超时,运行测试类,提示如下
提示事务超时
④测试@Transactional的noRollbackFor属性,我们修改一下测试类,因为数据库中的数据都一样,不好区分
测试结果,发现插入成功:
好了,测试了@Transactional这个annotation的大部分属性,关于rollbackForClassName和noRollbackForClassName就不测试了,与rollbackFor和noRollbackFor一样
关于测试事务的隔离级别,也没有必要再测,默认的隔离隔离级别就可以了,如果你想修改隔离级别也是要根据实际业务产品去获取,我没有用过其他的数据库,只用过Mysql,所以不敢瞎说,Mysql的Innodb存储引擎默认事务隔离级别就是可重复读,但oracle就不一定支持这四个隔离级别了
关于spring的事务传播行为,下次接着介绍~End