Spring详解8.Spring DAO

一年又一年,字节跳动 Lark(飞书) 研发团队又双叒叕开始招新生啦!
【内推码】:GTPUVBA
【内推链接】: https://job.toutiao.com/s/JRupWVj
【招生对象】:20年9月后~21年8月前 毕业的同学
【报名时间】:6.16-7.16(提前批简历投递只有一个月抓住机会哦!)
【画重点】:提前批和正式秋招不矛盾!面试成功,提前锁定Offer;若有失利,额外获得一次面试机会,正式秋招开启后还可再次投递。

点击进入我的博客

Spring详解1.概述
Spring详解2.理解IoC容器
Spring详解3.Bean的装配
Spring详解4.容器内幕
Spring详解5.AOP
Spring详解6.基于AspectJ的AOP
Spring详解7.Spring MVC
Spring详解8.Spring DAO

1 Spring DAO的概念

什么是DAO

DAO(Data Access Object)是用于访问数据的对象,虽然在大多数情况下将数存在数据库中,但这并不是唯一的选择,也可以将数据存储到文件中或LDAP中。DAO不但屏蔽了数据存储的最终介质的不同,也屏蔽了具体的实现技术的不同。提供DAO层的抽象可以带来一些好处:可以很容易地构造模拟对象,方便单元测试的开展;在使用切面时会有更多的选择,既可以使用JDK动态代理,又可以使用 CGLib动态代理。

Spring DAO的内容
  • Spring对多个持久化技术提供了集成支持,包括 Hibernate、 MyBatis、JPA、JDO;
  • Spring提供一个简化JDBC API操作的Spring JDBC框架。
  • Spring面向DAO制定了一个通用的异常体系,屏蔽具体持久化技术的异常,使业务层和具体的持久化技术实现解耦。
  • Spring提供了模板类简化各种持久化技术的使用。
  • 2 Spring统一的异常体系

    数据访问异常DataAccessException

    Spring在org.springframework.dao包中提供了一套完备优雅的DAO异常体系,这些异常都继承于 DataAccessException,而DataAccessException本身又继承于NestedRuntimeException,NestedRuntime Exception异常以嵌套的方式封装了源异常。因此,虽然不同持久化技术的特定异常被转换到 Spring的DAO异常体系中,但原始的异常信息并不会丢失;只要用户愿意,就可以方便地通过getCause()方法获取原始的异常信息。

    InvalidDataAccessApiUsageException 不正确地调用某一持久化技术时抛出的异常,如在 Spring JDBC中查询对象,在调用前必须进行编译操作,如果忘记这项操作则会产生该异常。这种异常不是由底层数据资源产生的,而是由不正确地使用持久化技术产生的 InvalidDataAccessResourceUsageException 在访问数据源时使用了不正确的方法所抛出的异常,如SQL语句错误将抛出该异常 PermissionDeniedDataAccessException 数据访问时由于权限不足引发的异常,如仅拥有只读权限的用户试图进行数据更改操作时将抛出该异常 UncategorizedAccessException 其他未分类的异常都归到该异常中
    JDBC异常转换器
  • 传统的JDBC API在发生几乎所有的数据操作问题时都会抛出相同的SQLException,它将异常的细节性信息封装在异常属性中。SQLException拥有两个代表异常具体原因的属性:错误码和SQL状态码。
  • Spring根据错误码和SQL状态码信息将SQLException译成 Spring DAO的异常体系所对应的异常。 org.springframework.jdbc.support.SQLExceptionTranslator 接口的两个实现类SQLErrorCodeTranslator和SQLStateSQLExceptionTranslator分别负责处理SQLException中错误码和SQL状态码的翻译工作。
  • 其他持久化技术的异常转换器

    由于各种框架级的持久化技术都拥有一个语义明确的异常体系,所以将这些异常转换为Spring DAO的体系相对轻松一些。在 org.springframework.orm 包中,分别为Spring所支持的ORM持久化技术定义了一个子包,在这些子包中提供相应ORM技术的整合类。

    ORM持久化技术 异常转换器

    3 统一数据访问模板

    访问数据库的流程

    以JDBC为例,访问数据库的操作大致按照以下流程进行:准备资源、启动事务、 在事务中执行具体的数据访问操作 、提交/回滚事务、关闭资源及处理异常。而其中除了 在事务中执行具体的数据访问操作 是业务相关的,其他代码都是几乎固定不变的。

    Spring DAO的模板

    Spring将这个相同的数据访问流程固化到模板类中,并将数据访问中固定和变化的部分分开,同时保证模板类是线程安全的,以便多个数据访问线程共享同一个模板实例。固定的部分在模板类中已经准备好,而变化的部分通过回调接口开放出来,用于定义具体数据访问和结果返回的操作。这样,只要编写好回调接口,并调用模板类进行数据访问,就可以得到预想的结果。

    4 数据源

    不管采用何种持久化技术,都必须拥有数据连接。在 Spring中,数据连接是通过数据源获得的。在Spring中,不但可以通过JNDI获取应用服务器的数据源,也可以直接在Spring容器中配置数据源,还可以通过代码的方式创建一个数据源,以便进行无容器依赖的单元测试。

    配置DBCP数据源
    @Bean(destroyMethod = "close")
        public BasicDataSource basicDataSource() {
            BasicDataSource basicDataSource = new BasicDataSource();
            basicDataSource.setDriverClassName("com.mysql.jdbc.Driver");
            basicDataSource.setUrl("jdbc:mysql://localhost:3306/test");
            basicDataSource.setUsername("root");
            basicDataSource.setPassword("password");
            return basicDataSource;
    
  • BasicDataSource提供了close方法关闭数据源,所以必须设定destroyMethod为close,以便 Spring容器关闭时,数据源能够正常关闭。
  • 假设数据库是 MySQL,如果数据源配置不当,则将可能发生经典的8小时问题——原因是MySQL在默认情况下如果发现一个连接的空闲时间超过8小时,则将会在数据库端自动关闭这个连接。而数据源并不知道这个连接已经被数据库关闭了,当它将这个无用的连接返回给某个DAO时,DAO就会报无法获取 Connection的异常。
  • 配置C3P0数据源

    C3P0是一个开放源码的JDBC数据源实现项目,实现了JDBC3和JDBC2扩展规范说明的Connection和Statement池。ComboPooledDataSource也提供了一个用于关闭数据源的close方法,这样就可以保证 Spring容器关闭时数据源能够被成功释放。C3P0拥有比DBCP更丰富的配置属性,通过这些属性,可以对数据源进行各种有效的控制。

    配置Druid数据源

    Druid首先是阿里巴巴开源的一个数据库连接池,但它不仅仅是一个数据库连接池,它还包含一个ProxyDriver,一系列内置的JDBC组件库,一个SQL Parser。Druid是目前最好的数据库连接池,在功能、性能、扩展性方面,都超过其他数据库连接池,包括DBCP、C3P0、BoneCP、Proxool、JBoss DataSource。

        @Bean
        public DruidDataSource druidDataSource() {
            DruidDataSource druidDataSource = new DruidDataSource();
            druidDataSource.setDriverClassName(driverClassName);
            druidDataSource.setUrl(url);
            druidDataSource.setUsername(username);
            druidDataSource.setPassword(password);
            return druidDataSource;
    
    使用配置文件放置属性
    @PropertySource(value = "classpath:database.properties")
    public class DatabaseConfig {
        @Value("${jdbc.driverClassName}")
        private String driverClassName;
        @Value("${jdbc.url}")
        private String url;
        @Value("${jdbc.username}")
        private String username;
        @Value("${jdbc.password}")
        private String password;
    
    配置JNDI

    如果应用配置在高性能的应用服务器上,则可能更希望使用应用服务器本身提供的数据源。应用服务器的数据源使用JNDI开放调用者使用,Spring为此专门提供了引用JDI数据源的JndiobjectFactoryBean类,通过jndiName指定引用的JNDI数据源名称。

    Spring数据源

    Spring本身也提供了一个简单的数据源实现类DriverManagerDataSource,它位于org. springframework.jdbc.datasource包中。这个类实现了Javax.sql.DataSource接口,但它并没有提供池化连接的机制;每次调用 getConnectionO方法获取新连接时,只是简单地创建一个新的连接。因此,这个数据源类比较适合在单元测试或简单的独立应用中使用,因为它不需要额外的依赖类。

    5 Spring JDBC

    Spring JDBC是Spring所提供的持久层技术。它的主要目的是降低使用 JDBC API的门槛,以一种更直接、更简洁的方式使用 JDBC API。在 Spring JDBC里,仅需做那些与业务相关的DML操作的事,而将资源获取、Statement创建、资源释放及异常处理等繁杂而乏味的工作交给Spring JDBC。

    5.1 使用Spring JDBC

    简单的例子
        private void func() {
            JdbcTemplate jdbcTemplate = new JdbcTemplate(); // (1)
            jdbcTemplate.setDataSource(dataSource);
            String sql = "INSERT INTO tb_name (name, value) VALUES ('Lucas', '26')";
            jdbcTemplate.execute(sql);
    

    上述示例中,特意将数据源创建和模板实例创建的代码都列在这个例子中,但在实际应用中,用户一般不会在DAO中做这些事情。由于JdbcTemplate是线程安全的,因而所有的DAO都可以共享同一个 JdbcTemplate实例,这样(1)的代码就可以从DAO中移除了,转而在 Spring配置文件中统一定义即可。

    在DAO中使用JdbcTemplate
    @Configuration
    public class DatabaseConfig {
        @Bean
        public JdbcTemplate jdbcTemplate(@Autowired DataSource dataSource) {
            JdbcTemplate jdbcTemplate = new JdbcTemplate();
            jdbcTemplate.setDataSource(dataSource);
            return jdbcTemplate;
    @Repository
    public class MyTestDao {
        @Autowired
        private JdbcTemplate jdbcTemplate;
        public void func() {
            jdbcTemplate.execute("INSERT INTO tb_name (name, value) VALUES ('Lucas', '26')");
    
    JdbcTemplate的属性

    JdbcTemplate拥有几个可用于控制底层JDBC API的属性

  • queryTimeout:设置 JdbcTemplate所创建的Statement查询数据时的最大超时时间。默认为0,表示使用底层JDBC驱动程序的默认设置。
  • fetchSize:设置底层的ResultSet每次从数据库返回的行数。该属性对程序的性能影响很大,如果设置过大,因为一次性载入的数据都放到内存中,所以内存的消耗很大:反之,如果设置过小,从数据库读取的次数将增大,也会影响性能。默认值为0,表示使用底层JDBC驱动程序的默认设置。
  • maxRows:设置底层的Resultset从数据库返回的最大行数。默认值为0,表示使用底层JDBC驱动程序的默认设置。
  • ignoreWarnings:是否忽略SQL的警告信息。默认为tue,即所有的警告信息都被记录到日志中,如果设置为fale,则JdbcTemplate将抛出SQLWarningException。
  • 5.2 数据操作

    更改数据update

    JdbcTemplate在内部是通过PreparedStatement执行SQL语句的,可以使用绑定了参数的SQL语句。

        private void func() {
            String value = "ZZX";
            // (1) 直接执行SQL
            jdbcTemplate.update("UPDATE tb_name SET name = 'ZZX' WHERE id = '1'");
            // (2) 绑定参数的SQL
            jdbcTemplate.update("UPDATE tb_name SET name = ? WHERE id = ?", new Object[]{value, 2});
            // (3) 指定参数的参数类型
            jdbcTemplate.update("UPDATE tb_name SET name = ? WHERE id = ?", new Object[]{value, 3}, new int[]{Types.VARCHAR, Types.INTEGER});
    
    带回调的update

    需要指出的是,在实际使用中,应优先考虑使用不带回调接口的 JdbcTemplate方法。首先,回调使代码显得臃肿;其次,回调并不能带来额外的好处。当使用简洁版的方法时,JdbcTemplate会在内部自动创建这些回调实例。以下是带回调带update方法:

    int update(String sql, PreparedStatementSetter pss):PreparedStatementSetter是一个回调接口,它定义了一个void set values( PreparedStatement ps)接口方法。 JabcTemplate使用SQL语句创建出 PreparedStatement实例后,将调用该回调接口执行绑定参数的操作。PreparedStatement绑定参数时,参数索引从1开始而非从0开始。 int update(PreparedStatementCreator psc)PreparedStatementCreator也是一个回调接口,它负责创建一个PreparedStatement实例。该回调接口定义了一个PreparedStatement create PreparedStatement( Connection con)方法。 protected int update(PreparedStatementCreator psc, PreparedStatementSetter pss):联合使用PreparedStatementCreator和 PreparedStatementSetter回调。
    获取自增主键

    在JDBC3.0规范中,当新增记录时,允许将数据库自动产生的主键值绑定到Statement或PreparedStatement中。在使用 Statement时,可以通过以下方法绑定主键值:int executeUpdate(String sql, int autoGeneratedKeys);也可以通过Connection创建绑定自增主键值的PreparedStatement,如下:Preparedstatement prepareStatement(String sql, int autoGeneratedKeys)。Spring利用这一技术,提供了一个可以返回新增记录对应主键值的方法:int update(Preparedstatementcreator psc, KeyHolder generatedKey Holder)

    private void func() {
            String sql = "INSERT INTO tb_name (name) VALUES (?)";
            // 创建一个主键持有对象
            KeyHolder keyHolder = new GeneratedKeyHolder();
            jdbcTemplate.update((Connection con) ->  {
                PreparedStatement statement = con.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS);
                statement.setString(1, "ZZX");
                return statement;
                }, keyHolder);
            // 获取主键
            System.out.println(keyHolder.getKey().intValue());
    
    批量修改数据

    如果需要一次性插入或更新多条记录,最好的选择是使用JdbcTemplate批量数据更改的方
    法。JdbcTemplate的两个批量数据操作方法:

    int[] batchUpdate(String[] sql):多条SQL语句组成一个数组(这些SQL语句不带参数),该方法以批量方式执行这些SQL语句。 Spring在内部使用JDBC提供的批量更新API完成操作。 int[] batchUpdate(String sql, BatchPreparedStatementSetter pss):使用该方法对于同
    一结构的带参SQL语句多次进行数据更新操作。BatchPreparedStatementSetter是一次性批量提交数据的,getSize()是整批的大小。
    private void func() {
            String sql = "INSERT INTO tb_name (name) VALUES (?)";
            String[] values = {"A", "B", "C"};
            jdbcTemplate.batchUpdate(sql, new BatchPreparedStatementSetter() {
                @Override
                public void setValues(PreparedStatement ps, int i) throws SQLException {
                    ps.setString(1, values[i]);
                @Override
                public int getBatchSize() {
                    return values.length;
    
    查询数据——使用RowCallbackHandler

    当结果集中没有数据时,此时并不会抛出异常,而是此时RowCallbackHandler回调接口中定义的处理逻辑没有得到调用。

        private void func() {
            String sql = "SELECT * FROM tb_name WHERE id = 1";
            User user = new User();
            jdbcTemplate.query(sql, (ResultSet rs) -> {
                user.setId(rs.getInt("id"));
                user.setName(rs.getString("name"));
                user.setValue(rs.getLong("value"));
            System.out.println(user);
    
    批量查询数据——使用RowCallbackHandler
    private void func() {
            String sql = "SELECT * FROM tb_name";
            List<User> users = new ArrayList<>();
            jdbcTemplate.query(sql, (ResultSet rs) -> {
                User user = new User();
                user.setId(rs.getInt("id"));
                user.setName(rs.getString("name"));
                user.setValue(rs.getLong("value"));
                users.add(user);
            System.out.println(users);
    
    查询数据——使用RowMapper<T>
    private void func() {
            String sql = "SELECT * FROM tb_name";
            List<User> users = jdbcTemplate.query(sql, (ResultSet rs, int rowNum) -> {
                User user = new User();
                user.setId(rs.getInt("id"));
                user.setName(rs.getString("name"));
                user.setValue(rs.getLong("value"));
                return user;
            System.out.println(users);
    
  • 从功能上来说,RowCallbackHandler和RowMapper<T并没有太大的区别,它们都用于定义结果集行的读取逻辑,将 Resultset中的数据映射到对象或List中。
  • 通过JDBC查询返回一个Resultset结果集时,JDBC并不会一次性将所有匹配的数据都加载到JM中,而是只返回一批次的数据(由JDBC驱动程序决定),当通过ResultSet#next()游标滚动结果集超过数据范围时,JDBC再获取一批数据。这样以一种“批量化+串行化”的处理方式避免大结果集处理时JVM内存的过大开销。
  • 当处理大结果集时,如果使用RowMapper,那么采用的方式是将结果集中的所有数据都放到一个Lis<T>对象中,这样将会占用大量的JVM内存,甚至可能直接引发OutOfMemoryException异常。这时,可使用RowCallbackHandler接口,在processRow()接口方法内部一边获取数据一边完成处理,这样数据就不会在内存中堆积,可大大减少对JVM内存的占用。
  • 采用RowMapper的操作方式是先获取数据,然后再处理数据;而 RowCallbackHandler的操作方式是一边获取数据一边处理,处理完就丢弃之。因此,可以将 Row Mapper看作采用批量化数据处理策略,而 RowHandler则采用流化处理策略。
  • 查询单值数据

    如果查询的结果集仅有一个值,如SELECT COUNT(*) FROM tb_name,这时可以使用更简单的方式获取结果集的值。 JdbcTemplate为获取结果集中的单值数据提供了3组方法,分别用于获取int、long的单值,其他类型的单值则以 Object类型返回。

    调用存储过程

    JdbcTemplate提供了两个调用存储过程的接口方法,分别介绍如下。

    <T> T execute(String callString, CallableStatementCallback<T> action):用户通过callString参数指定调用存储过程的SQL语句;第二个参数CallableStatementCallback<T>是一个回调接口,可以在接口的方法中进行输入参数绑定、输出参数注册及返回数据处理等操作。 <T> T execute(CallableStatementCreator csc, CallableStatementCallback<T> action):该接口方法使用 CallableStatementCreator创建CallableStatement。CallableStatementCreator负责创建 CallableStatement实例、绑定参数、注册输出参数等工作,CallableStatementCallback<T>负责处理存储过程的返回结果。 Spring提供了创建CallableStatementcreator的工厂类 CallableStatementCreatorFactory,通过该工厂类可以简化CallableStatementCreator的实例创建工作。

    6 整合其他ORM框架

    6.1 Spring ORM框架好处

  • 方便基础设施的搭建:对于不同的ORM框架,始终可以采用相同的方式配置数据源;Spring还为不同的ORM框架提供了相应的FactoryBean,用以初始化ORM框架的基础设施,可以将它们当成普通的Bean对待。
  • 异常封装:Spring能够转换各种ORM框架所抛出的异常,将ORM框架专有的或检査型异常
    转换为Spring DAO异常体系中的标准异常。
  • 统一的事务管理:通过使用基于 Spring DAO模板的编程风格,甚至使用ORM框架原生的API,只要遵循 Spring所提出的很少的编程要求,就可以使用 Spring提供的事务管理功能。此外,JDBC代码能够在事务级别上与用户使用的ORM框架一起使用。这一功能对于诸如批量处理、LOB操作等并不适合单独采用ORM完成的地方尤其有用。
  • 允许混合使用多个ORM框架:Spring在DAO异常、事务、资源等高级层次建立了抽象,因而可以让业务层对DAO具体实现的技术不敏感,可以在底层选用适合的实现方式,甚至可以混合使用多种ORM
  • 方便单元测试:Spring容器使得替换不同的实现和配置变得非常简单,这些内容包括 HibernateSession Factory的位置、 JDBC DataSource、事务管理器及映射对象的实现等,这样就很容易隔离并测试不同的DAO类。