废话不多说,我们先搭建一个简单的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("[email protected]", "苏胜路长乐街南山巴黎印象", "苏州大学"), 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 {}; }
我们可以试试这几个属性,我们先试试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