Spring 中的 JDBC 事务
Spring 对 JDBC 的支持
JdbcTemplate 简介
•为了使 JDBC 更加易于使用, Spring 在 JDBC API 上定义了一个抽象层, 以此建立一个 JDBC 存取框架.
•作为 Spring JDBC 框架的核心, JDBC 模板的设计目的是为不同类型的 JDBC 操作提供模板方法. 每个模板方法都能控制整个过程, 并允许覆盖过程中的特定任务. 通过这种方式, 可以在尽可能保留灵活性的情况下, 将数据库存取的工作量降到最低.
使用 JdbcTemplate 更新数据库
1 <!-- 导入资源文件 --> 2 <context:property-placeholder location="classpath:db.properties"/> 3 4 <!-- 配置 C3P0 数据源 --> 5 <bean id="dataSource" 6 class="com.mchange.v2.c3p0.ComboPooledDataSource"> 7 <property name="user" value="${jdbc.user}"></property> 8 <property name="password" value="${jdbc.password}"></property> 9 <property name="jdbcUrl" value="${jdbc.jdbcUrl}"></property> 10 <property name="driverClass" value="${jdbc.driverClass}"></property> 11 12 <property name="initialPoolSize" value="${jdbc.initPoolSize}"></property> 13 <property name="maxPoolSize" value="${jdbc.maxPoolSize}"></property> 14 </bean> 15 16 <!-- 配置 Spirng 的 JdbcTemplate --> 17 <bean id="jdbcTemplate" 18 class="org.springframework.jdbc.core.JdbcTemplate"> 19 <property name="dataSource" ref="dataSource"></property> 20 </bean>
1 /** 2 * 获取单个列的值, 或做统计查询 3 * 使用 queryForObject(String sql, Class<Long> requiredType) 4 */ 5 @Test 6 public void testQueryForObject2(){ 7 String sql = "SELECT count(id) FROM employees"; 8 long count = jdbcTemplate.queryForObject(sql, Long.class); 9 10 System.out.println(count); 11 } 12 13 /** 14 * 查到实体类的集合 15 * 注意调用的不是 queryForList 方法 16 */ 17 @Test 18 public void testQueryForList(){ 19 String sql = "SELECT id, last_name lastName, email FROM employees WHERE id > ?"; 20 RowMapper<Employee> rowMapper = new BeanPropertyRowMapper<>(Employee.class); 21 List<Employee> employees = jdbcTemplate.query(sql, rowMapper,5); 22 23 System.out.println(employees); 24 } 25 26 /** 27 * 从数据库中获取一条记录, 实际得到对应的一个对象 28 * 注意不是调用 queryForObject(String sql, Class<Employee> requiredType, Object... args) 方法! 29 * 而需要调用 queryForObject(String sql, RowMapper<Employee> rowMapper, Object... args) 30 * 1. 其中的 RowMapper 指定如何去映射结果集的行, 常用的实现类为 BeanPropertyRowMapper 31 * 2. 使用 SQL 中列的别名完成列名和类的属性名的映射. 例如 last_name lastName 32 * 3. 不支持级联属性. JdbcTemplate 到底是一个 JDBC 的小工具, 而不是 ORM 框架 33 */ 34 @Test 35 public void testQueryForObject(){ 36 String sql = "SELECT id, last_name lastName, email, dept_id as \"department.id\" FROM employees WHERE id = ?"; 37 RowMapper<Employee> rowMapper = new BeanPropertyRowMapper<>(Employee.class); 38 Employee employee = jdbcTemplate.queryForObject(sql, rowMapper, 1); 39 40 System.out.println(employee); 41 } 42 43 /** 44 * 执行批量更新: 批量的 INSERT, UPDATE, DELETE 45 * 最后一个参数是 Object[] 的 List 类型: 因为修改一条记录需要一个 Object 的数组, 那么多条不就需要多个 Object 的数组吗 46 */ 47 @Test 48 public void testBatchUpdate(){ 49 String sql = "INSERT INTO employees(last_name, email, dept_id) VALUES(?,?,?)"; 50 51 List<Object[]> batchArgs = new ArrayList<>(); 52 53 batchArgs.add(new Object[]{"AA", "aa@atguigu.com", 1}); 54 batchArgs.add(new Object[]{"BB", "bb@atguigu.com", 2}); 55 batchArgs.add(new Object[]{"CC", "cc@atguigu.com", 3}); 56 batchArgs.add(new Object[]{"DD", "dd@atguigu.com", 3}); 57 batchArgs.add(new Object[]{"EE", "ee@atguigu.com", 2}); 58 59 jdbcTemplate.batchUpdate(sql, batchArgs); 60 } 61 62 /** 63 * 执行 INSERT, UPDATE, DELETE 64 */ 65 @Test 66 public void testUpdate(){ 67 String sql = "UPDATE employees SET last_name = ? WHERE id = ?"; 68 jdbcTemplate.update(sql, "Jack", 5); 69 }
简化 JDBC 模板查询
•每次使用都创建一个 JdbcTemplate 的新实例, 这种做法效率很低下.
•JdbcTemplate 类被设计成为线程安全的, 所以可以再 IOC 容器中声明它的单个实例, 并将这个实例注入到所有的 DAO 实例中.
1 @Repository 2 public class EmployeeDao { 3 4 @Autowired 5 private JdbcTemplate jdbcTemplate; 6 7 public Employee get(Integer id){ 8 String sql = "SELECT id, last_name lastName, email FROM employees WHERE id = ?"; 9 RowMapper<Employee> rowMapper = new BeanPropertyRowMapper<>(Employee.class); 10 Employee employee = jdbcTemplate.queryForObject(sql, rowMapper, id); 11 12 return employee; 13 } 14 }
•Spring JDBC 框架还提供了一个 JdbcDaoSupport 类来简化 DAO 实现. 该类声明了 jdbcTemplate 属性, 它可以从 IOC 容器中注入, 或者自动从数据源中创建.
1 @Repository 2 public class DepartmentDao extends JdbcDaoSupport{ 3 4 @Autowired 5 public void setDataSource2(DataSource dataSource){ 6 setDataSource(dataSource); 7 } 8 9 public Department get(Integer id){ 10 String sql = "SELECT id, dept_name name FROM departments WHERE id = ?"; 11 RowMapper<Department> rowMapper = new BeanPropertyRowMapper<>(Department.class); 12 return getJdbcTemplate().queryForObject(sql, rowMapper, id); 13 } 14 15 }
在 JDBC 模板中使用具名参数
•在 Spring JDBC 框架中, 绑定 SQL 参数的另一种选择是使用具名参数(named parameter).
•具名参数: SQL 按名称(以冒号开头)而不是按位置进行指定. 具名参数更易于维护, 也提升了可读性. 具名参数由框架类在运行时用占位符取代
•具名参数只在 NamedParameterJdbcTemplate 中得到支持
<!-- 配置 NamedParameterJdbcTemplate, 该对象可以使用具名参数, 其没有无参数的构造器, 所以必须为其构造器指定参数 --> <bean id="namedParameterJdbcTemplate" class="org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate"> <constructor-arg ref="dataSource"></constructor-arg> </bean>
1 /** 2 * 使用具名参数时, 可以使用 update(String sql, SqlParameterSource paramSource) 方法进行更新操作 3 * 1. SQL 语句中的参数名和类的属性一致! 4 * 2. 使用 SqlParameterSource 的 BeanPropertySqlParameterSource 实现类作为参数. 5 */ 6 @Test 7 public void testNamedParameterJdbcTemplate2(){ 8 String sql = "INSERT INTO employees(last_name, email, dept_id) " 9 + "VALUES(:lastName,:email,:dpetId)"; 10 11 Employee employee = new Employee(); 12 employee.setLastName("XYZ"); 13 employee.setEmail("xyz@sina.com"); 14 employee.setDpetId(3); 15 16 SqlParameterSource paramSource = new BeanPropertySqlParameterSource(employee); 17 namedParameterJdbcTemplate.update(sql, paramSource); 18 } 19 20 /** 21 * 可以为参数起名字. 22 * 1. 好处: 若有多个参数, 则不用再去对应位置, 直接对应参数名, 便于维护 23 * 2. 缺点: 较为麻烦. 24 */ 25 @Test 26 public void testNamedParameterJdbcTemplate(){ 27 String sql = "INSERT INTO employees(last_name, email, dept_id) VALUES(:ln,:email,:deptid)"; 28 29 Map<String, Object> paramMap = new HashMap<>(); 30 paramMap.put("ln", "FF"); 31 paramMap.put("email", "ff@atguigu.com"); 32 paramMap.put("deptid", 2); 33 34 namedParameterJdbcTemplate.update(sql, paramMap); 35 }
Spring 中的事务管理
•作为企业级应用程序框架, Spring 在不同的事务管理 API 之上定义了一个抽象层. 而应用程序开发人员不必了解底层的事务管理 API, 就可以使用 Spring 的事务管理机制.
•Spring 既支持编程式事务管理, 也支持声明式的事务管理.
•编程式事务管理: 将事务管理代码嵌入到业务方法中来控制事务的提交和回滚. 在编程式管理事务时, 必须在每个事务操作中包含额外的事务管理代码.
•声明式事务管理: 大多数情况下比编程式事务管理更好用. 它将事务管理代码从业务方法中分离出来, 以声明的方式来实现事务管理. 事务管理作为一种横切关注点, 可以通过 AOP 方法模块化. Spring 通过 Spring AOP 框架支持声明式事务管理.
用事务通知声明式地管理事务
•事务管理是一种横切关注点
•为了在 Spring 2.x 中启用声明式事务管理, 可以通过 tx Schema 中定义的 <tx:advice> 元素声明事务通知, 为此必须事先将这个 Schema 定义添加到 <beans> 根元素中去.
•声明了事务通知后, 就需要将它与切入点关联起来. 由于事务通知是在 <aop:config> 元素外部声明的, 所以它无法直接与切入点产生关联. 所以必须在 <aop:config> 元素中声明一个增强器通知与切入点关联起来.
1 <!-- 配置 bean --> 2 <bean id="bookShopDao" class="com.atguigu.spring.tx.xml.BookShopDaoImpl"> 3 <property name="jdbcTemplate" ref="jdbcTemplate"></property> 4 </bean> 5 6 <bean id="bookShopService" class="com.atguigu.spring.tx.xml.service.impl.BookShopServiceImpl"> 7 <property name="bookShopDao" ref="bookShopDao"></property> 8 </bean> 9 10 <bean id="cashier" class="com.atguigu.spring.tx.xml.service.impl.CashierImpl"> 11 <property name="bookShopService" ref="bookShopService"></property> 12 </bean> 13 14 <!-- 1. 配置事务管理器 --> 15 <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> 16 <property name="dataSource" ref="dataSource"></property> 17 </bean> 18 19 <!-- 2. 配置事务属性 --> 20 <tx:advice id="txAdvice" transaction-manager="transactionManager"> 21 <tx:attributes> 22 <!-- 根据方法名指定事务的属性 --> 23 <tx:method name="purchase" propagation="REQUIRES_NEW"/> 24 <tx:method name="get*" read-only="true"/> 25 <tx:method name="find*" read-only="true"/> 26 <tx:method name="*"/> 27 </tx:attributes> 28 </tx:advice> 29 30 <!-- 3. 配置事务切入点, 以及把事务切入点和事务属性关联起来 --> 31 <aop:config> 32 <aop:pointcut expression="execution(* com.atguigu.spring.tx.xml.service.*.*(..))" 33 id="txPointCut"/> 34 <aop:advisor advice-ref="txAdvice" pointcut-ref="txPointCut"/> 35 </aop:config>
用 @Transactional 注解声明式地管理事务
•为了将方法定义为支持事务处理的, 可以为方法添加 @Transactional 注解. 根据 Spring AOP 基于代理机制, 只能标注公有方法.
•可以在方法或者类级别上添加 @Transactional 注解. 当把这个注解应用到类上时, 这个类中的所有公共方法都会被定义成支持事务处理的.
•在 Bean 配置文件中只需要启用 <tx:annotation-driven> 元素, 并为之指定事务管理器就可以了.
•如果事务处理器的名称是 transactionManager, 就可以在<tx:annotation-driven> 元素中省略 transaction-manager 属性. 这个元素会自动检测该名称的事务处理器.
<!-- 配置事务管理器 --> <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource"></property> </bean> <!-- 启用事务注解 --> <tx:annotation-driven transaction-manager="transactionManager"/>
//添加事务注解 //1.使用 propagation 指定事务的传播行为, 即当前的事务方法被另外一个事务方法调用时 //如何使用事务, 默认取值为 REQUIRED, 即使用调用方法的事务 //REQUIRES_NEW: 事务自己的事务, 调用的事务方法的事务被挂起. //2.使用 isolation 指定事务的隔离级别, 最常用的取值为 READ_COMMITTED //3.默认情况下 Spring 的声明式事务对所有的运行时异常进行回滚. 也可以通过对应的 //属性进行设置. 通常情况下去默认值即可. //4.使用 readOnly 指定事务是否为只读. 表示这个事务只读取数据但不更新数据, //这样可以帮助数据库引擎优化事务. 若真的事一个只读取数据库值的方法, 应设置 readOnly=true //5.使用 timeout 指定强制回滚之前事务可以占用的时间. @Transactional(propagation=Propagation.REQUIRES_NEW, isolation=Isolation.READ_COMMITTED, readOnly=false, timeout=3) @Override public void purchase(String username, String isbn) { //1. 获取书的单价 int price = bookShopDao.findBookPriceByIsbn(isbn); //2. 更新数的库存 bookShopDao.updateBookStock(isbn); //3. 更新用户余额 bookShopDao.updateUserAccount(username, price); } }
事务传播属性
•当事务方法被另一个事务方法调用时, 必须指定事务应该如何传播. 例如: 方法可能继续在现有事务中运行, 也可能开启一个新事务, 并在自己的事务中运行.
•事务的传播行为可以由传播属性指定. Spring 定义了 7 种类传播行为.第一和第二种最常用.