20191105 《Spring5高级编程》笔记-第6章

第6章 Spring JDBC支持

Spring官方:

位于Spring Framework Project下。
文档:
https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/data-access.html#jdbc

MySQL通常更广泛地用于Web应用程序开发,特别是Linux平台上。PostgreSQL对Oracle开发人员更友好,因为它的过程语言PLpgSQL非常接近Oracle的PL/SQL语言。

6.1 介绍Lambda表达式

大多数使用了模板或回调的Spring API都可以使用lambda表达式,不限于JDBC。

6.3 研究JDBC基础结构

JDBC为Java应用程序访问存储在数据库中的数据提供了一种标准方式。JDBC基础结构的核心是针对每个数据库的驱动程序,即允许Java代码访问数据库的驱动程序。
一旦加载驱动程序,就会注册java.sql.DriverManager类。该类管理驱动程序列表并提供建立与数据库连接的静态方法。DriverManager.getConnection()方法返回驱动程序实现的java.sql.Connection接口。该接口允许针对数据库运行SQL语句。

连接(Connection)是一种稀缺资源,建立起来非常昂贵。

演示怎么用JDBC写DAO代码。

6.4 Spring JDBC基础结构

org.springframework:spring-jdbc 提供对JDBC的支持,分为5个部分:
image

6.5 数据库连接和数据源

javax.sql.DataSource 用来帮助管理数据库连接。DataSourceConnection之间的区别在于DataSource可以提供并管理Connection。

org.springframework.jdbc.datasource.DriverManagerDataSourceDataSource的最简单实现,通过调用DriverManager来获得连接,不支持数据库连接池。

6.6 嵌入数据库支持

Spring提供了嵌入式数据库支持,该支持会自动启动嵌入式数据库并将其作为应用程序的DataSource公开。

Spring支持HSQL(默认)、H2DERBY

以H2为例:

<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
</dependency>
@Configuration
public class EmbeddedJdbcConfig {
    @Bean
    public DataSource dataSource() {
        EmbeddedDatabaseBuilder dbBuilder = new EmbeddedDatabaseBuilder();
        return dbBuilder.setType(EmbeddedDatabaseType.H2).addScripts("classpath:db/h2/schema.sql", "classpath:db/h2/test_data.sql").build();
    }
}

这里要注意脚本的顺序,DDL文件应该第一个显示,之后是DML文件。

# schema.sql
CREATE TABLE SINGER
(
    ID         INT         NOT NULL AUTO_INCREMENT,
    FIRST_NAME VARCHAR(60) NOT NULL,
    LAST_NAME  VARCHAR(40) NOT NULL,
    BIRTH_DATE DATE,
    UNIQUE UQ_SINGER_1 (FIRST_NAME, LAST_NAME),
    PRIMARY KEY (ID)
);

CREATE TABLE ALBUM
(
    ID           INT          NOT NULL AUTO_INCREMENT,
    SINGER_ID    INT          NOT NULL,
    TITLE        VARCHAR(100) NOT NULL,
    RELEASE_DATE DATE,
    UNIQUE UQ_SINGER_ALBUM_1 (SINGER_ID, TITLE),
    PRIMARY KEY (ID),
    CONSTRAINT FK_ALBUM FOREIGN KEY (SINGER_ID) REFERENCES SINGER (ID)
);
-- test_data.sql
INSERT INTO `singer`(`id`, `first_name`, `last_name`, `birth_date`) VALUES (1, 'John', 'Mayer', '1997-10-16');
INSERT INTO `singer`(`id`, `first_name`, `last_name`, `birth_date`) VALUES (2, 'Eric', 'Clapton', '1945-03-30');
INSERT INTO `singer`(`id`, `first_name`, `last_name`, `birth_date`) VALUES (3, 'John', 'Butler', '1975-04-01');

INSERT INTO `album`(`id`, `singer_id`, `title`, `release_date`) VALUES (1, 1, 'The Search For Everything', '2017-01-20');
INSERT INTO `album`(`id`, `singer_id`, `title`, `release_date`) VALUES (2, 1, 'Battle Studies', '2009-11-17');
INSERT INTO `album`(`id`, `singer_id`, `title`, `release_date`) VALUES (3, 2, 'From the Cradle', '1994-09-13');
public class DbConfigTest {
    @Test
    public void test3() {
        AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
        ctx.register(EmbeddedJdbcConfig.class);
        ctx.refresh();

        DataSource dataSource = ctx.getBean("dataSource", DataSource.class);
        testDataSource(dataSource);
        ctx.close();
    }

    private void testDataSource(DataSource dataSource) {
        Connection connection = null;
        try {
            connection = dataSource.getConnection();
            PreparedStatement statement = connection.prepareStatement("select 1");
            ResultSet resultSet = statement.executeQuery();
            while (resultSet.next()) {
                int mockVal = resultSet.getInt("1");
                System.out.println("mockVal = " + mockVal);
            }
            statement.close();
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            if (connection != null) {
                try {
                    connection.close();
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

对于本地开发或单元测试来说,嵌入式数据库支持是非常有用的。

6.7 在DAO类中使用DataSource

数据访问对象(DAO)模式用于将低级数据访问API或操作与高级业务服务相分离。数据访问对象模式需要以下组件:

  • DAO接口:该接口定义了在模型对象(或多个对象)上执行的标准操作;
  • DAO实现:该类提供了DAO接口的具体实现。通常使用JDBC连接或数据源来处理模型对象;
  • 模型对象也称为数据对象或实体:这是映射到数据表记录的简单POJO;

6.8 异常处理

Spring提倡使用运行时异常(非检查型异常)而不是检查型异常,Spring的SQL异常更精细。

org.springframework.jdbc.support.SQLExceptionTranslator 接口负责将通用SQL错误代码转换为Spring JDBC异常。需要配合org.springframework.jdbc.core.JdbcTemplate使用。

6.9 JdbcTemplate类

该类代表Spring JDBC支持的核心。它可以执行所有类型的SQL语句,包括DDL和DML。

JdbcTemplate类允许向数据库发出任何类型的SQL语句并返回任何类型的结果。

6.9.1 在DAO类中初始化JdbcTemplate

JdbcTemplate是线程安全的。这意味着可以选择在Spring的配置中初始化一个JdbcTemplate实例,并将其注入到所有的DAO bean中。

// 定义jdbcTemplate和DAO
@Bean
public JdbcTemplate jdbcTemplate() {
    JdbcTemplate jdbcTemplate = new JdbcTemplate();
    jdbcTemplate.setDataSource(dataSource());
    return jdbcTemplate;
}

@Bean
public SingerDao singerDao() {
    JdbcSingerDao dao = new JdbcSingerDao();
    dao.setJdbcTemplate(jdbcTemplate());
    return dao;
}

// 使用jdbcTemplate
@Override
public String findNameById(Long id) {
    String name = jdbcTemplate.queryForObject("select first_name || ' ' || last_name from singer where id = ?", new Object[]{id}, String.class);
    return name;
}

6.9.2 通过NamedParameterJdbcTemplate使用命名参数

org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate 提供了对命名参数的关系。与 org.springframework.jdbc.core.JdbcTemplate 没有继承关系,内部包含一个JdbcTemplate

// 定义namedParameterJdbcTemplate和DAO
@Bean
public NamedParameterJdbcTemplate namedParameterJdbcTemplate() {
    NamedParameterJdbcTemplate namedParameterJdbcTemplate = new NamedParameterJdbcTemplate(dataSource());
    return namedParameterJdbcTemplate;
}

@Bean
public NamedJdbcSingerDao namedJdbcSingerDao(){
    NamedJdbcSingerDao namedJdbcSingerDao = new NamedJdbcSingerDao();
    namedJdbcSingerDao.setNamedParameterJdbcTemplate(namedParameterJdbcTemplate());
    return namedJdbcSingerDao;
}

// 使用namedParameterJdbcTemplate
@Override
public String findNameById(Long id) {
    String sql = "select first_name || ' ' || last_name from singer where id = :singerId";

    Map<String, Object> namedParams = new HashMap<>();
    namedParams.put("singerId", id);

    return namedParameterJdbcTemplate.queryForObject(sql, namedParams, String.class);
}

6.9.3 使用RowMapper检索域对象

Spring的 org.springframework.jdbc.core.RowMapper<T> 提供了一种简单的方法来完成从JDBC结果集到POJO的映射。

// 手动创建RowMapper
@Override
public List<Singer> findAll() {
    String sql = "select id, first_name, last_name, birth_date from singer";
    return namedParameterJdbcTemplate.query(sql, new SingerMapper());
}

private class SingerMapper implements RowMapper<Singer> {
    @Override
    public Singer mapRow(ResultSet rs, int rowNum) throws SQLException {
        return new Singer().setId(rs.getLong("id")).setFirstName(rs.getString("first_name")).setLastName(rs.getString("last_name")).setBirthDate(rs.getDate("birth_date"));
    }
}

// 使用Lambda表达式创建匿名RowMapper
@Override
public List<Singer> findAll() {
    String sql = "select id, first_name, last_name, birth_date from singer";
    return namedParameterJdbcTemplate.query(sql, (rs, rowNum) ->
            new Singer().setId(rs.getLong("id")).setFirstName(rs.getString("first_name")).setLastName(rs.getString("last_name")).setBirthDate(rs.getDate("birth_date"))
    );
}

6.10 使用ResultSetExtractor检索嵌套域对象

org.springframework.jdbc.core.RowMapper<T> 仅适用于将行映射到单个域对象;对于更复杂的对象结构,则需要使用org.springframework.jdbc.core.ResultSetExtractor 接口。

public List<Singer> findAllWithAlbums() {
        String sql = "select s.id, s.first_name, s.last_name, s.birth_date" +
                ", a.id as album_id, a.title, a.release_date " +
                " from singer s left join album a on s.id = a.singer_id";
//        return namedParameterJdbcTemplate.query(sql, new SingerWithDetailExtractor());
        return namedParameterJdbcTemplate.query(sql, rs -> {
            Map<Long, Singer> map = new HashMap<>();
            Singer singer;
            while (rs.next()) {
                Long id = rs.getLong("id");
                singer = map.get(id);
                if (singer == null) {
                    singer = new Singer();
                    singer.setId(id);
                    singer.setFirstName(rs.getString("first_name"));
                    singer.setLastName(rs.getString("last_name"));
                    singer.setBirthDate(rs.getDate("birth_date"));
                    singer.setAlbums(new ArrayList<>());
                    map.put(id, singer);
                }

                Long albumId = rs.getLong("album_id");
                if (albumId > 0) {
                    Album album = new Album();
                    album.setId(albumId);
                    album.setSingerId(id);
                    album.setTitle(rs.getString("title"));
                    album.setReleaseDate(rs.getDate("release_date"));
                    singer.addAlbum(album);
                }
            }
            return new ArrayList<>(map.values());
        });
    }

6.11 建模JDBC操作的Spring类

Spring提供了许多有用的类来模拟JDBC数据库,从而让开发人员以更面向对象的方式将ResultSet中的查询和转换逻辑维护到域对象。

  • org.springframework.jdbc.object.MappingSqlQuery<T>
    允许将查询字符串和mapRow()方法一起封装到要给类中
  • org.springframework.jdbc.object.SqlUpdate
    能够封装任何SQL更新语句,绑定SQL参数,在插入新的记录后检索RDBMS生成的键等。
  • org.springframework.jdbc.object.BatchSqlUpdate
    允许执行批量更新操作。可以随时设置批量大小并刷新操作
  • org.springframework.jdbc.object.SqlFunction
    允许使用参数和返回类型调用数据库中的存储函数。此外,还可以使用另一个类StoredProcedure来帮助调用存储过程。

6.12 使用MappingSqlQuery查询数据

Spring提供了MappingSqlQuery<T>类对查询操作进行建模。

示例:

  1. 查询不带参数:
//----定义MappingSqlQuery-------------------------------------//
public class SelectAllSingers extends MappingSqlQuery<Singer> {
    private static String SQL_SELECT_ALL_SINGER = "select id, first_name, last_name, birth_date from singer";

    public SelectAllSingers(DataSource ds) {
        super(ds, SQL_SELECT_ALL_SINGER);
    }
        
    @Override
    protected Singer mapRow(ResultSet rs, int rowNum) throws SQLException {
        Singer singer = new Singer();
        singer.setId(rs.getLong("id"));
        singer.setFirstName(rs.getString("first_name"));
        singer.setLastName(rs.getString("last_name"));
        singer.setBirthDate(rs.getDate("birth_date"));
        return singer;
    }
}

//----调用--------------------------------------------------//
@Resource(name = "dataSource")
public void setDataSource(DataSource dataSource) {
    this.dataSource = dataSource;
    this.selectAllSingers = new SelectAllSingers(dataSource);
}
@Override
public List<Singer> findAll() {
    return selectAllSingers.execute();
}
  1. 查询带参数:
//----定义MappingSqlQuery-------------------------------------//
public class SelectAllSingers extends MappingSqlQuery<Singer> {

    private static String SQL_FIND_BY_FIRST_NAME = "select id, first_name, last_name, birth_date from singer " +
            " where first_name = :first_name";

    public SelectAllSingers(DataSource ds) {
        super(ds, SQL_FIND_BY_FIRST_NAME);
        super.declareParameter(new SqlParameter("first_name", Types.VARCHAR));
    }

    @Override
    protected Singer mapRow(ResultSet rs, int rowNum) throws SQLException {
        Singer singer = new Singer();
        singer.setId(rs.getLong("id"));
        singer.setFirstName(rs.getString("first_name"));
        singer.setLastName(rs.getString("last_name"));
        singer.setBirthDate(rs.getDate("birth_date"));
        return singer;
    }
}

//----调用--------------------------------------------------//
public List<Singer> findByFirstName(String firstName) {
    Map<String, Object> paramMap = new HashMap<>();
    paramMap.put("first_name", firstName);
    return selectAllSingers.executeByNamedParam(paramMap);
}

MappingSqlQuery仅适用于将单个行映射到域对象。对于嵌套对象,则需要将JdbcTemplateResultSetExtractor一起使用。

使用SqlUpdate更新数据

//-------定义SqlUpdate-------------------//
public class UpdateSinger extends SqlUpdate {
    private static String SQL_UPDATE_SINGER = "update singer set first_name=:first_name, last_name=:last_name, birth_date=:birth_date where id=:id";

    public UpdateSinger(DataSource ds) {
        super(ds, SQL_UPDATE_SINGER);
        super.declareParameter(new SqlParameter("first_name", Types.VARCHAR));
        super.declareParameter(new SqlParameter("last_name", Types.VARCHAR));
        super.declareParameter(new SqlParameter("birth_date", Types.DATE));
        super.declareParameter(new SqlParameter("id", Types.INTEGER));
    }
}

//-------调用-------------------//
@Resource(name = "dataSource")
public void setDataSource(DataSource dataSource) {
    this.dataSource = dataSource;
    this.updateSinger = new UpdateSinger(dataSource);
}

public void update(Singer singer) {
    Map<String, Object> paramMap = new HashMap<>();
    paramMap.put("first_name", singer.getFirstName());
    paramMap.put("last_name", singer.getLastName());
    paramMap.put("birth_date", singer.getBirthDate());
    paramMap.put("id", singer.getId());
    updateSinger.updateByNamedParam(paramMap);
}

6.13 插入数据并检索生成的键

从JDBC 3.0开始,允许以统一的方式检索RDBMS生成的键。

//-----------定义SqlUpdate ------------------------//
public class InsertSinger extends SqlUpdate {
    private static final String SQL_INSERT_SINGER = "insert into singer (first_name, last_name, birth_date) values " +
            " (:first_name, :last_name, :birth_date)";

    public InsertSinger(DataSource ds) {
        super(ds, SQL_INSERT_SINGER);
        super.declareParameter(new SqlParameter("first_name", Types.VARCHAR));
        super.declareParameter(new SqlParameter("last_name", Types.VARCHAR));
        super.declareParameter(new SqlParameter("birth_date", Types.DATE));

        super.setGeneratedKeysColumnNames(new String[]{"id"});
        super.setReturnGeneratedKeys(true);
    }
}

//-----------调用 ------------------------//
public void insert(Singer singer) {
    System.out.println(singer);

    Map<String, Object> paramMap = Maps.newHashMap();
    paramMap.put("first_name", singer.getFirstName());
    paramMap.put("last_name", singer.getLastName());
    paramMap.put("birth_date", singer.getBirthDate());

    GeneratedKeyHolder keyHolder = new GeneratedKeyHolder();
    insertSinger.updateByNamedParam(paramMap, keyHolder);
    singer.setId(keyHolder.getKey().longValue());

    System.out.println(singer);
}

6.14 使用BatchSqlUpdate进行批处理操作

//-----------定义BatchSqlUpdate ------------------------//
public class InsertSingerAlbum extends BatchSqlUpdate {
    private static final String SQL_INSERT_SINGER_ALBUM = "insert into album (singer_id, title, release_date) values " +
            " (?, ?, ?)";

    private static final int BATCH_SIZE = 1;

    public InsertSingerAlbum(DataSource ds) {
        super(ds, SQL_INSERT_SINGER_ALBUM);

        declareParameter(new SqlParameter("singer_id", Types.VARCHAR));
        declareParameter(new SqlParameter("title", Types.VARCHAR));
        declareParameter(new SqlParameter("release_date", Types.DATE));

        setBatchSize(BATCH_SIZE);
    }
}

//-----------调用 ------------------------//
@Override
    public void insertWithDetail(Singer singer) {
        Map<String, Object> paramMap = new HashMap<>();
        paramMap.put("first_name", singer.getFirstName());
        paramMap.put("last_name", singer.getLastName());
        paramMap.put("birth_date", singer.getBirthDate());

        GeneratedKeyHolder keyHolder = new GeneratedKeyHolder();
        insertSinger.updateByNamedParam(paramMap, keyHolder);

        singer.setId(keyHolder.getKey().longValue());

        List<Album> albums = singer.getAlbums();
        if (albums != null) {
            for (Album album : albums) {
                insertSingerAlbum.update(new Object[]{singer.getId(), album.getTitle(), album.getReleaseDate()});
            }
        }
        insertSingerAlbum.flush();
    }

6.15 使用SqlFunction调用存储函数

//-----------定义SqlFunction------------------------//
public class StoredFunctionFirstNameById extends SqlFunction<String> {
    private static final String SQL = "select getfirstnamebyid(?)";

    public StoredFunctionFirstNameById(DataSource dataSource){
        super(dataSource,SQL);

        declareParameter(new SqlParameter(Types.INTEGER));
        compile();
    }
}

//-----------调用------------------------//
public String findFirstNameById(Long id) {
    List<String> result = storedFunctionFirstNameById.execute(id);
    return result.get(0);
}

Spring还提供了StoredProcedure来调用复杂的存储过程。

6.16 Spring Data项目:JDBC Extensions

Spring创建了Spring Data项目,主要目标是在Spring的核心数据访问功能之上提供有用的扩展,以便与传统RDBMS之外的数据库进行交互。

Spring Data的扩展之一 JDBC Extensions 提供了一些高级功能:

  • QueryDSL支持;
  • 对Oracle数据库的高级支持;

6.17 使用JDBC的注意事项

在JDBC基础上有很多开源库,帮助缩小关系数据结构与Java的OO模型之间的差距。

在使用Spring时,可以混合搭配不同的数据访问技术。例如,可以将Hibernate用作主ORM,然后将JDBC用作一些复杂查询逻辑或批处理操作的补充;可以在单个事务操作中将它们混合搭配,并封装在同一个事务中。

6.18 Spring Boot JDBC

Spring Boot JDBC的启动器:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>

启动器使用 HikariCPcom.zaxxer.hikari.HikariDataSource 来配置DataSource bean。老版本可能使用其他不同的DataSource。

Spring Boot还会自动注册以下bean:

  • JdbcTemplate
  • NamedParameterJdbcTemplate
  • PlatformTransactionManagerDataSourceTransactionManager

Spring Boot对JDBC的默认配置:

  1. 启动器默认使用嵌入式数据库。默认schema.sql包含DDL语句,data.sql包含DML,可配置:
// 默认位于classpath下
spring:
  datasource:
    schema: classpath:db/h2/schema.sql
    data: classpath:db/h2/test_data.sqlspring.data
  1. 默认会在启动时初始化数据库,可通过spring.datasource.initialize = false来进行更改;
posted @ 2019-11-05 11:34  流星<。)#)))≦  阅读(373)  评论(0编辑  收藏  举报