Spring中的数据库异常体系
使用JDBC(不使用Spring)的时候,我们需要强制捕获SQLException,否则无法使用JDBC处理任何事情。SQLException表示尝试访问数据库的时候出现问题,但是这个异常却没有告诉我们哪里出错以及如何处理。可能出现SQLException的情况有:
- 应用程序无法连接数据库
- 查询的SQL有问题
- 查询中的表,列不存在
- 尝试插入或更新的数据违法数据库的约束
还有其他的数据库有关的异常,但是真正抛出SQLException的时候,我们没有办法准确的定位,只有靠经验与猜测去处理。而且抛出SQLException意味着出现致命性的错误,程序也没有办法继续处理。
一些持久层框架都有相对丰富的异常体系,例如Hibernate有二十几左右的异常,分别对应特定的数据访问问题。这样就可以针对特定的异常编写catch代码块。但是,这种Hibernate的异常是与对应的框架相对应的,我们如果希望特定的持久框架独立于数据访问层。如果我们将这些Hibernate的异常抛出去,那么我们对Hibernate的使用将渗透到其他层。如果不这样做,就需要将这些特定的异常捕获然后将其转换为与平台无关的异常再次处理。
一方面,JDBC的异常过于简单,而Hibernate的异常体系又是与其平台有关的。Spring 针对这个问题提供了多个数据访问异常,Spring的异常体系并没有与特定持久方法相关联,这样我们就不用关心所选择的持久化方案。这有助于我们将所选择的具体持久化技术与数据访问层隔离开。而且Spring的数据访问异常都继承于DataAccessException,这是一个非检查异常,也就是说没有必要捕获Spriing这些数据访问异常(如果想捕获也是完全可以的)。
第一步 配置数据源
Spring的模板类处理数据访问层的国定部分——事务控制,管理控制以及处理异常。同时,应用程序相关的数据访问——语句,绑定参数以及真理数据集需要我们自己处理。针对不同的持久化平台,Spring提供了可选的模板,如果使用JDBC就选择JdbcTemplate,如果使用对象关系映射框架,那么可以使用HibernateTemplcate或JpaTemplcate。
但首先要说明的是Spring所支持的持久功能都需要依赖数据源。所以不要忘记配置数据源,并将其作为第一步(建议使用数据库连接池或者JNDI的方式,如果有可能使用Spring的profile也是极好的)。
@Bean public DataSource dataSource() { BasicDataSource dataSource = new BasicDataSource(); dataSource.setUrl("jdbc:mysql:///crud"); dataSource.setUsername("root"); dataSource.setPassword("root"); dataSource.setDriverClassName("com.mysql.jdbc.Driver"); return dataSource; }
第二步 在Spring中使用JDBC
JDBC是建立在SQL之上的,而SQL本身就是数据库访问语言,此外,与其他的技术相比,直接使用JDBC可以更好对数据库进行调优,JDBC允许使用数据库的所有特性,而这是其他框架不鼓励甚至是禁止的。相对于其他的持久层框架,JDBC能够让我们在更底层上处理数据,我们完全可以控制应用程序如何读取和管理数据,这是一种细粒度的数据访问方式。
使用JDBC模板
Spring的JDBC模板承担了资源管理和异常处理的工作,从而简化了JDBC代码,将数据访问的样板代码抽象到模板类中,Spring为JDBC提供了三个JDBC模版类:
- JdbcTemplate:最基本的Spring JDBC模板类,支持基于索引参数(?)的查询。
- NamedParameterJdbcTemplate:使用该模板类执行查询的时候使用命名参数的方式绑定到SQL,而不是用索引参数。
- SimpleJdbcTemplate:Spring3.1之后已弃用
只有我们需要使用命名参数的时候,才使用NamedParameterJdbcTemplate,对于的大多数JDBC任务使用JdbcTemplate就是最好的方案。为了JdbcTemplate可以正常工作,只需要为其配置DataSource:
@Bean public JdbcTemplate jdbcTemplate(DataSource dataSource) { return new JdbcTemplate(dataSource); }
然后,我们就可以将JdbcTemplate装配到Repository中了。
@Repository public class JdbcRepository { @Autowired private JdbcOperations jdbcOperations;
这里使用的是 JdbcOperations ,JdbcOperations接口定义了JdbcTemplate模板类所实现的方法,使用接口而不是具体的某一个JDBC模板类,保证松耦合。这里使用Spring的@AutoWired,其实使用@Inject作也可以,只不过后者是Java的注入规范中的注解。
使用模板类之后所有的样板代码隐藏到JDBC模板类中,并且也不用对SQLException进行处理,在内部,JdbcTemplate将会捕获说有可能抛出的SQLException,并将其转换为Spring中通用的数据访问异常,然后将其重新抛出。
使用JdbcTemplate读取数据
JdbcTemplate中有众多query...()的方法用于读取数据,但是对于需要将返回结果封装为对象的操作,需要有一些特殊处理。这个对象需要实现RowMapper接口,对于查询所返回的每一行数据,JdbcTemplate将会调用RowMapper的mapRow方法,并传入一个ResultSet和包含行号的整数。
private static final class SpitterRowMapper implements RowMapper<Spitter> { public Spitter mapRow(ResultSet rs, int rowNum) throws SQLException { //这段代码摘自书本 long id = rs.getLong("id"); String username = rs.getString("username"); String password = rs.getString("password"); String fullName = rs.getString("fullname"); String email = rs.getString("email"); boolean updateByEmail = rs.getBoolean("updateByEmail"); return new Spitter(id, username, password, fullName, email, updateByEmail); } }
因为RowMapper接口中只有一个mapRow方法,它完全符合函数式接口的标准。这意味着使用Java8来开发应用的化可以使用Lambda表达式,而不必使用具体的实现类了。或者使用Java8的方法引用,在单独的方法中定义映射规则:
public Dept findOne(Integer deptId) { return jdbcOperations.queryForObject(SELECT_SQL, this::mapDept,deptId); } private Dept mapDept(ResultSet rs,int row)throws SQLException { return new Dept(rs.getInt("dept_id"),rs.getString("dept_name")); }
但是,不论使用Lambda表达式还是方法引用,都需要保证接受与mapRow相同的参数。
使用命名参数
使用索引参数的时候,需要注意参数的顺序,在将值传递给繁华的是否需要保证正确的顺序。如果在修改SQL时改变了参数的顺序,那我们还需要修改参数值的顺序。而我们使用命名参数,命名参数可以给SQL中的每一个参数一个明确的名字,在绑定值得时候通过名字来引用参数。
private static final String SQL_INSERT_SPITTER="insert into spitter(username,password,fullname)"+"values(:username,:password,:fullname)";
使用命名参数查询,绑定值得顺序就不重要了,如果SQL改变导致参数顺序与之前不一致,我们不需要修改绑定的代码。
NamedParameterJdbcTemplate是一个特殊得JDBC模板类,它使用命名参数。NamedParameterJdbcTemplate得用法与JdbcTemplate一致:
@Bean public JdbcTemplate jdbcTemplate(DataSource dataSource) { return new NamedParameterJdbcTemplate(dataSource); }
然后,就开始在Repository中使用吧:
public void addSpitter(Spitter spitter){ Map<String,Object> map=new HashMap<String,Object>(); map.put("username",spitter.getUsername()); map.put("password",spitter.getPassword()); map.put("username",spitter.getFullName()); jdbcOperations.update(SQL_INSERT_SPITTER, map); }
因为命名参数需要通过java.util.Map进行绑定,所以需要将参数放进Map中。
使用事务
因为采用JavaConfig得方式配置Spring,所以对于事务得处理也需要通过Java代码的方式来声明,至于使用,还是与XML得方式一样来使用即可,先来回顾一下XML中配置事务的方式(使用事务注解,而不是AOP配置事务):
- 与平台相关的事务管理器
- 开启事务注解驱动:<tx:annotation-driven transaction-manager="transactionManage"/>
- 在需要事务的方法上使用@Transactional
Java代码的方式也是按照这个步骤去进行的,首先,我们配置事务管理器,JDBC和MyBatis都是使用数据源的事务管理(DataSourceTransactionManager),Hibernate和JPA使用分别各自的事务管理器(HibernateTransactionManager, JpaTransactionManager)。 因为这里使用JDBC就用DataSourceTransactionManager了
//注册事务管理器在容器中 @Bean public PlatformTransactionManager transactionManager() throws Exception{ return new DataSourceTransactionManager(dataSource()); }
需要将数据源交给事务管理器,这样管理器可以控制commit或rollback
接下来在配置类上启用 @EnableTransactionManagement 表示开启事务注解驱动
@EnableTransactionManagement @ComponentScan("cn.lynu") @Configuration public class TxConfig {
最后在需要事务的方法上使用@Transactional注解即可,使用方法与XML的配置没有什么不同。