在没有学习Spring框架之前,通过学习JDBC编程,已经了解到JDBC编程中使用的工具:
数据库连接池有 dbcp:Database connection pool数据库连接池 和 c3p0,两种数据库连接池使用优势各有千秋。
DAO工具有dbUtils,利用dbUtils工具可以简化对数据库的一系列操作。
那么在Spring对JDBC的支持中,使用数据库连接池工具 和 Spring的JdbcTemplate类
spring开发小组推荐使用的数据库连接池工具是dbcp,本文这里是使用的c3p0作为例子。
jdbc.user=root jdbc.password=root jdbc.driverClass=com.mysql.jdbc.Driver jdbc.jdbcUrl=jdbc:mysql://localhost:3306/test jdbc.initialPoolSize=20 jdbc.maxPoolSize=40
<!-- 引入外部配置文件 --> <context:property-placeholder location="classpath:db.properties"/> <!-- 配置c3p0数据源 --> <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource"> <property name="user" value="${jdbc.user}"></property> <property name="password" value="${jdbc.password}"></property> <property name="jdbcUrl" value="${jdbc.jdbcUrl}"></property> <property name="driverClass" value="${jdbc.driverClass}"></property> <property name="initialPoolSize" value="${jdbc.initialPoolSize}"></property> <property name="maxPoolSize" value="${jdbc.maxPoolSize}"></property> </bean>这个时候可以用测试用例,获取IOC中的dataSource实例,看是否已经连接到数据库
@Test public void testSpringJdbc(){ DataSource dataSource = (DataSource) applicationContext.getBean(DataSource.class); Connection conn = null; try { conn = dataSource.getConnection(); System.out.println(conn); } catch (SQLException e) { e.printStackTrace(); } finally{ if(conn != null){ try { conn.close(); } catch (SQLException e) { e.printStackTrace(); } } } }
JdbcTemplate类是Spring框架中的,其目的是对JDBC的操作进行一层封装,就是DAO思想
使用JdbcTemplate时,需在spring配置文件中,配置该类的bean,并且要设置属性dataSource
<!-- 配置spring的JdbcTemplate --> <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate"> <property name="dataSource" ref="dataSource"></property> </bean>
这样才可以从IOC中获取到JdbcTemplate的实例,并使用。
可以实现对数据库的各种操作:
更新(增删改):使用JdbcTemplate对象的update(String sql, Object... args)方法
批量更新:使用JdbcTemplate对象的batchUpdate(String sql, List<Object[]> batchArgs)方法
查询一条记录并返回一个对象:使用JdbcTemplate对象的queryForObject(String sql, RowMapper<Employee> rowMapper, Object... args)
查询实体类的集合(多条记录返回实体对象的集合):query(String sql, RowMapper<Employee> rowMapper, Object... args) ,返回List<Employee> 对象
获取单个列的值或者统计查询:queryForObject(String sql, Class<Long> requiredType)
public class JDBCTest { private ApplicationContext applicationContext = null; private JdbcTemplate jdbcTemplate = null; { applicationContext = new ClassPathXmlApplicationContext("applicationContext-jdbc.xml"); jdbcTemplate = (JdbcTemplate) applicationContext.getBean("jdbcTemplate"); } /* * 获取单个列的值,或者统计查询 * 使用 JdbcTemplate.queryForObject(String sql, Class<T> requiredType)方法 */ @Test public void testQueryForObject2(){ String sql= "SELECT count(id) FROM employees"; Long count = jdbcTemplate.queryForObject(sql, Long.class); System.out.println(count); } /* * 查询实体类的集合 * 使用JdbcTemplate.query(String sql, RowMapper<Employee> rowMapper, Object... args) 方法 * 注意调用的不是queryForList()方法 */ @Test public void testQueryForList(){ String sql="SELECT id, name, email FROM employees WHERE id>?"; RowMapper<Employee> rowMapper = new BeanPropertyRowMapper<>(Employee.class); List<Employee> list = jdbcTemplate.query(sql,rowMapper,4); System.out.println(list); } /* * 从数据库读取一条数据,返回对应的对象 * 注意不是使用JdbcTemplate.queryForObject(String sql, Class<Employee> requiredType, Object... args)方法 * 需要调用JdbcTemplate.queryForObject(String sql, RowMapper<Employee> rowMapper, Object... args)方法 * 其中1.RowMapper指定如何去映射结果集的行,常用的实现类是BeanPropertyRowMapper * 2.使用sql的别名完成列名与类的属性名之间的匹配 * 3.不支持级联属性,JdbcTemplate到底是JDBC的一个小工具,而不是ORM框架 */ @Test public void testQueryForObject(){ String sql="SELECT id, name, email FROM employees WHERE id=?"; // Employee employee = jdbcTemplate.queryForObject(sql, Employee.class,5); RowMapper<Employee> rowMapper = new BeanPropertyRowMapper<>(Employee.class); Employee employee = jdbcTemplate.queryForObject(sql, rowMapper,3); System.out.println(employee); } //批量更新,使用JdbcTemplate类对象的batchUpdate(String sql, List<Object[]> batchArgs)方法 @Test public void testBatchUpdate(){ String sql = "INSERT INTO employees(name, email, dept_id) VALUES(?,?,?)"; List<Object[]> batchArgs= new ArrayList<>(); batchArgs.add(new Object[]{"jack2","[email protected]",5}); batchArgs.add(new Object[]{"jack3","[email protected]",5}); batchArgs.add(new Object[]{"jack4","[email protected]",5}); jdbcTemplate.batchUpdate(sql, batchArgs); } //INSERT , DELETE, UPDATE 使用JdbcTemplate类对象的update()方法 @Test public void testUpdate(){ String sql="INSERT employees VALUES(4,'yuchen','[email protected]',3)"; jdbcTemplate.update(sql); sql = "UPDATE employees SET name=? WHERE id=?"; jdbcTemplate.update(sql, "jack",1); sql="DELETE FROM employees WHERE id=?"; jdbcTemplate.update(sql, 1); } }
创建Employee的数据库访问对象EmployeeDAO,通过id获取Employee对象的信息
//使用注解时,记得配置context:component-scan标签 @Repository public class EmployeeDAO { @Autowired private JdbcTemplate jdbcTemplate; public Employee getEmployeeById(int id){ String sql = "SELECT id, name, email FROM employees WHERE id=?"; RowMapper<Employee> rowMapper = new BeanPropertyRowMapper<>(Employee.class); if(jdbcTemplate==null){ System.out.println("null"); } Employee employee = jdbcTemplate.queryForObject(sql, rowMapper,id); return employee; } @Test public void testGetEmployeeById(){ ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext-jdbc.xml"); //这里的EmployeeDAO对象必须是从IOC容器中获取的,使用new创建的对象,不能得到自动装配的jdbcTemplate //使用new创建的对象,jdbcTemplate为空指针 EmployeeDAO employeeDAO = (EmployeeDAO) applicationContext.getBean("employeeDAO"); Employee employee = employeeDAO.getEmployeeById(3); System.out.println(employee); } }
JdbcDaoSupport类同JdbcTemplate类一样,都是spring框架对Jdbc编程的支持,不过使用方法不同,JdbcDaoSupport其实是调用的JdbcTemplate类。
使用JdbcDaoSupport对数据库的操作比直接使用JdbcTemplate要繁琐,所以一般使用JdbcTemplate类对象作为属性进数据库操作。
使用JdbcDaoSupport的方法:
创建一个类,继承JdbcDaoSupport父类,(通过注解方式在IOC中生成bean)
新建一个方法,对DataSource属性进行配置,否则出现异常。(通过注解的方法将IOC中的dataSource自动装配到参数)
调用JdbcDaoSupport的getJdbcTemplate方法得到JdbcTemplate对象,调用方法完成操作。
//不推荐使用JdbcDaoSupport,而推荐使用JdbcTemplate作为类的成员变量 //继承JdbcDaoSupport父类,必须配置dataSource属性 @Repository public class DepartmentDAO extends JdbcDaoSupport{ //因为父类JdbcDaoSupport的setDataSource()方法是finally,不能覆盖,故曲线配置 //如果不配置,则会出现异常 @Autowired//参数自动装配 public void setDataSource2(DataSource dataSource){ setDataSource(dataSource); } public Department get(Integer id){ String sql= "SELECT id,dept_name name FROM department WHERE id = ?"; RowMapper<Department> rowMapper = new BeanPropertyRowMapper<>(Department.class); //可见JdbcDaoSupprot类也是通过JdbcTemplate实现对数据库的操作的 Department department = getJdbcTemplate().queryForObject(sql, rowMapper,id); return department; } @Test public void testGet(){ ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext-jdbc.xml"); DepartmentDAO departmentDAO = (DepartmentDAO) ctx.getBean("departmentDAO"); Department department = departmentDAO.get(1); System.out.println(department); } }
使用注解方式配置bean时,记得在spring配置文件中添加标签:context:component-scan base-package=""
<context:component-scan base-package="com.cqupt.spring.jdbc"></context:component-scan>
NamedParameterJdbcTemplate同JdbcTemplate一样,是一个对数据库操作的类,与JdbcTemplate稍有不同。
String sql="INSERT INTO employees VALUES(?,?,?)";当占位符,参数过多时,需要特别注意参数对应的位置,不方便,故引入具名参数的JdbcTemplate。
使用NamedParameterJdbcTemplate的好处是:不需要再对应参数的位置,直接对应参数名。
/* * 使用NamedParameterJdbcTemplate, * 好处:不用再去对应位置,直接对应参数名,便于维护 * 缺点:稍微麻烦 */ @Test public void testNamedParameterJdbcTemplate(){ // String sql="INSERT INTO employees VALUES(?,?,?)"; String sql="INSERT INTO employees(name,email,dept_id) VALUES(:name,:email,:deptId)"; NamedParameterJdbcTemplate njt = (NamedParameterJdbcTemplate) applicationContext.getBean("namedParameterJdbcTemplate"); //通过Map容器完成操作 Map<String, Object> paramMap = new HashMap<>(); paramMap.put("name", "zhangM"); paramMap.put("email", "[email protected]"); paramMap.put("deptId", 4); njt.update(sql, paramMap); //通过对象完成操作 //创建对象,属性赋值,创建SqlParameterSource对象 //update(String sql, SqlParameterSource paramSource) Employee employee = new Employee(); employee.setName("wangX"); employee.setEmail("[email protected]"); employee.setDeptId(5); SqlParameterSource paramSource = new BeanPropertySqlParameterSource(employee); njt.update(sql, paramSource); }使用对象完成操作的时候,具名参数的名字要和对象的属性名保持一致。
Spring框架对于事务处理也有应对机制,
spring既支持编程式事务管理,也支持声明式事务管理,声明式事务可以非常简单的完成对事务的操作。
编程式事务管理 ,就是将事务管理的代码嵌入到业务方法中,控制事务的提交和回滚。
声明式事务管理,就是将事务管理的代码从业务中分离出来,以声明的方式控制事务管理。
Connection conn = null; try{//前置通知:获取连接和禁止自动提交 conn = JDBCTools.getConnection(); //禁止自动提交,设置自动提交为false conn.setAutoCommit(false); String sql= "UPDATE users SET balance = balance+500 WHERE username = 'yuchen' "; update(conn, sql); int a =10/0; sql= "UPDATE users SET balance = balance-500 WHERE username = 'jack'"; update(conn,sql); //提交,相当于返回通知 conn.commit(); } catch (Exception e){ e.printStackTrace(); try { //回滚操作,相当于异常通知 conn.rollback(); } catch (SQLException e1) { e1.printStackTrace(); } } finally{ JDBCTools.release(null,null, conn); }
从事务处理的编程式中,可以看出,事务管理与AOP有很大的相似性,所以,利用spring的AOP机制,支持声明式事务管理。
事务处理不单是针对于JDBC来说的,对于JTA(Java Transaction API),Hibernate都有各自的事务处理。
Spring从不同的事务管理API中,抽象了一整套的事务机制,开发人员不必了解这些事务的底层API,就可以利用这些事务机制。有了这些事务机制,事务管理代码就能独立于特定的事务技术了。
Spring的核心事务管理抽象是:Interface PlatformTransactionManager
对于Jdbc,有DataSourceTransactionManager,在应用程序中,只需处理一个数据源,而且通过JDBC存取
对于JTA,有JtaTransactionManager,在JavaEE应用服务器上用JTA进行事务管理(JTA需详细了解)
对于Hibernate,有HibernateTransactionManager,用Hibernate框架存取数据库(Hibernate需详细了解)
事务管理器以普通Bean形式声明在IOC容器中。
通过一个例子完成对事务处理的详述。
一个书店,有图书销售,三个要素:客户的账户、图书的信息、图书的库存。
账户:账户名、余额;
图书:书号,书名,价格;
库存:书号,库存。
首先,建立BookShopDao接口,完成对数据库的各种操作。
public interface BookShopDao { //根据书号获取书的单价 public double findBookPriceByIsbn(String isbn); //更新书的库存,使书号对应的库存-1 public void updateBookStock(String isbn); //更新账户的余额,使username的 balance-price public void updateUserAccount(String isbn, double price); }实现类
@Repository("bookShopDao") public class BookShopDaoImpl implements BookShopDao { @Autowired private JdbcTemplate jdbcTemplate; @Override public double findBookPriceByIsbn(String isbn) { String sql = "SELECT price FROM book WHERE isbn=?"; double price = jdbcTemplate.queryForObject(sql, double.class,isbn); return price; } @Override public void updateBookStock(String isbn) { //检查书的库存是否足够,若不足,抛出异常(手动提供必要的检查,因为mysq不支持检查约束) String sqlCheck = "SELECT stock FROM book_stock WHERE isbn = ?"; int stock = jdbcTemplate.queryForObject(sqlCheck, int.class,isbn); if(stock == 0){ throw new BookStockException("库存不足!"); } String sql = "UPDATE book_stock SET stock = stock-1 WHERE isbn=?"; jdbcTemplate.update(sql, isbn); } @Override public void updateUserAccount(String username, double price) { //检查账户的余额是否足够,若不足,抛出异常 String sqlCheck = "SELECT balance FROM account WHERE username = ?"; double balance = jdbcTemplate.queryForObject(sqlCheck, double.class,username); if(balance -price < 0){ throw new BookStockException("余额不足!"); } String sql = "UPDATE account SET balance = balance-? WHERE username=?"; jdbcTemplate.update(sql,price,username); } }可以通过Junit测试单元,对BookShopDao的各种方法进行验证。
其次,建立对业务的抽象类BookShopService
public interface BookShopService { //客户购买图书,完成两个操作:库存减一、账户余额减书的价格 public void purchase(String username,String isbn); }实现类,通过调用BookShopDao的方法
@Repository("bookShopService") public class BookShopServiceImpl implements BookShopService{ @Autowired private BookShopDao bookShopDao; @Override public void purchase(String username, String isbn) { //获取书的单价 double price = bookShopDao.findBookPriceByIsbn(isbn); //更新库存 bookShopDao.updateBookStock(isbn); //更新账户余额 bookShopDao.updateUserAccount(username, price); } }
purchase方法有可能在余额不足时,更新库存,然后发现余额不足的异常,导致数据库中的数据不具一致性。
此时,如何使purchase()方法,成为一个事务,就需要利用spring提供的事务处理了。使用方法:
1.在spring配置文件中,配置事务管理器
spring中事务管理器是DataSourceTransactionManager类,配置时,必须通过指定构造参数,指明管理的是哪一个数据源
<!-- 配置事务管理器 ,指定管理的数据源--> <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <constructor-arg ref="dataSource"></constructor-arg> </bean>2.启用事务注解
引入命名空间tx,通过标签指明事务管理器
<!-- 启用事务注解 --> <tx:annotation-driven transaction-manager="transactionManager"/>3.通过注解@Transactional,将一套操作声明为事务(一个方法)
//添加事务注解 @Transactional @Override public void purchase(String username, String isbn) { //获取书的单价 double price = bookShopDao.findBookPriceByIsbn(isbn); //更新库存 bookShopDao.updateBookStock(isbn); //更新账户余额 bookShopDao.updateUserAccount(username, price); }这样就完成了spring的事务管理
什么是事务的传播行为?方法A是一个事务,方法B也是一个事务,方法A中调用方法B,那就形成了事务的传播行为。
那么此时,方法B是继续使用方法A的事务,还是方法B新开一个自己的事务,把方法A的事务暂时挂起?
这就是事务的传播行为的设置。
设置方法:
在方法B的事务注解中,注明事务的传播行为,有两种:
@Transactional(propagation=Propagation.REQUIRED)方法B在被其他事务方法调用时,无需创建自己的事务,只需使用调用方法的事务即可,这是默认的传播行为。
@Transactional(propagation=Propagation.REQUIRES_NEW)方法B在被其他事务方法调用时,创建自己新的事务,将调用方法的事务暂时挂起。
注解在接口的方法或者实现类的方法上都可以。
用例:
创建Cashier接口,一次购买多本图书,并作检查。实现类CashierImpl会调用BookShopService的方法
public interface Cashier { //一次性买多本书 public void checkout(String username, List<String> isbns); }实现类
@Repository("cashier") public class CashierImpl implements Cashier { @Autowired private BookShopService bookShopService; @Transactional @Override public void checkout(String username, List<String> isbns) { for(String isbn : isbns){ //purchase()也是一个声明式事务 bookShopService.purchase(username,isbn); } } }其中,checkout方法是一个事务,调用的purchase方法也是一个事务,那么在purchase方法的事务注解中,注明事务的传播行为
//添加事务注解 //使用propagation指定事务的传播行为,默认的取值为REQUIRED,即使用调用方法的事务 //另一种常见的传播行为是REQUIRES_NEW,表示注解的方法必须启动一个新事务,并在自己的事务内运行, //如果调用方法有事务运行,先挂起 @Transactional(propagation=Propagation.REQUIRES_NEW) @Override public void purchase(String username, String isbn) { //获取书的单价 double price = bookShopDao.findBookPriceByIsbn(isbn); //更新库存 bookShopDao.updateBookStock(isbn); //更新账户余额 bookShopDao.updateUserAccount(username, price); }此时,传播行为是REQUIRES_NEW,表示purchase方法会创建自己的事务,当purchase方法异常时,回滚操作在自己的事务内完成,不会影响到checkout的事务,状态回到当前方法purchase被调用之前。
如果,传播行为是REQUIRED,表示purchase方法使用checkout的事务,一单方法purchase出现异常,回滚操作在checkout的事务内完成,状态回到checkout被调用的状态之前。
事务的隔离级别在JDBC的学习中已经详述,可参见。最常用的是”读已提交“READ_COMMITTED
在spring的设置中,像传播属性一样,直接添加:isolation=Isolation.READ_COMMITTED
事务的回滚就是针对于,在事务中发生异常时,状态回滚到事务之前的状态。
指定不回滚的异常,就是当这些异常发生时,不执行回滚操作,其他未指定的异常发生时,一样需要回滚操作。
直接添加:noRollbackFor={xxxException.class}
添加只读属性,是为了更高效的操作数据库。同时读取与更新的事务,底层需要为数据加锁,以达到隔离级别。而声明为只读属性,就不需要这些操作,使得数据读取更加高效。
直接添加:readOnly=true/false
为了使该事务不能长期占据资源,当事务一旦超过某个时间限,就必须强制执行回滚操作。
timeout=3 单位是秒
@Transactional(propagation=Propagation.REQUIRES_NEW,//传播行为 isolation=Isolation.READ_COMMITTED,//隔离级别 noRollbackFor={AccountBalanceException.class},//指定不回滚的异常 readOnly=false,//只读属性 timeout=3//超时属性 ) @Override public void purchase(String username, String isbn) { //获取书的单价 double price = bookShopDao.findBookPriceByIsbn(isbn); //更新库存 bookShopDao.updateBookStock(isbn); //更新账户余额 bookShopDao.updateUserAccount(username, price); }
在配置DataSource和JdbcTemplate的bean 的前提下,配置自定义类的bean,然后进行配置事务:
1.配置事务管理器的bean
2.配置事务的属性,使用标签tx:advice,并指明属性--关联的事务管理器
3.配置事务切入点,以及把事务切入点aop:pointcut和事务的属性关联起来aop:advisor,并指明关联的事务属性bean
<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:tx="http://www.springframework.org/schema/tx" xmlns:aop="http://www.springframework.org/schema/aop" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.2.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.2.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.2.xsd"> <!-- 引入外部配置文件 --> <context:property-placeholder location="classpath:db.properties"/> <!-- 配置c3p0数据源 --> <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource"> <property name="user" value="${jdbc.user}"></property> <property name="password" value="${jdbc.password}"></property> <property name="jdbcUrl" value="${jdbc.jdbcUrl}"></property> <property name="driverClass" value="${jdbc.driverClass}"></property> <property name="initialPoolSize" value="${jdbc.initialPoolSize}"></property> <property name="maxPoolSize" value="${jdbc.maxPoolSize}"></property> </bean> <!-- 配置spring的JdbcTemplate --> <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate"> <property name="dataSource" ref="dataSource"></property> </bean> <!-- 配置自定义类的bean --> <bean id="bookShopDao" class="com.cqupt.spring.tx.xml.BookShopDaoImpl"> <property name="jdbcTemplate" ref="jdbcTemplate"></property> </bean> <bean id="bookShopService" class="com.cqupt.spring.tx.xml.service.impl.BookShopServiceImpl"> <property name="bookShopDao" ref="bookShopDao"></property> </bean> <bean id="cashier" class="com.cqupt.spring.tx.xml.service.impl.CashierImpl"> <property name="bookShopService" ref="bookShopService"></property> </bean> <!--1. 配置事务管理器 ,指定管理的数据源--> <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <constructor-arg ref="dataSource"></constructor-arg> </bean> <!-- 2.配置事务属性 --> <tx:advice id="txAdvice" transaction-manager="transactionManager"> <tx:attributes> <!-- 必须有tx:method子节点,通过指定方法名,为特定方法的事务配置属性,可以通过*通配符指定多个方法 --> <tx:method name="purchase" propagation="REQUIRES_NEW"/> <tx:method name="get*" read-only="true"/> <tx:method name="find*" read-only="true"/> <!-- 如果上边一一指定了需要指定为事务的方法及属性,那么可以不需要name="*", 这是为了其他需要指定为事务的方法,统一指定为事务,并设置为默认属性 --> <tx:method name="*"/> </tx:attributes> </tx:advice> <!-- 3.配置事务切入点,以及把事务切入点和事务属性关联起来 --> <aop:config> <aop:pointcut expression="execution(* com.cqupt.spring.tx.xml.service.impl.*.*(..))" id="txPintcut"/> <aop:advisor advice-ref="txAdvice" pointcut-ref="txPintcut"/> </aop:config> </beans>