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个部分:
6.5 数据库连接和数据源
javax.sql.DataSource
用来帮助管理数据库连接。DataSource
和Connection
之间的区别在于DataSource可以提供并管理Connection。
org.springframework.jdbc.datasource.DriverManagerDataSource
是DataSource
的最简单实现,通过调用DriverManager
来获得连接,不支持数据库连接池。
6.6 嵌入数据库支持
Spring提供了嵌入式数据库支持,该支持会自动启动嵌入式数据库并将其作为应用程序的DataSource
公开。
Spring支持HSQL
(默认)、H2
和DERBY
。
以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>
类对查询操作进行建模。
示例:
- 查询不带参数:
//----定义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();
}
- 查询带参数:
//----定义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
仅适用于将单个行映射到域对象。对于嵌套对象,则需要将JdbcTemplate
与ResultSetExtractor
一起使用。
使用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>
启动器使用 HikariCP
的 com.zaxxer.hikari.HikariDataSource
来配置DataSource bean。老版本可能使用其他不同的DataSource。
Spring Boot还会自动注册以下bean:
JdbcTemplate
NamedParameterJdbcTemplate
PlatformTransactionManager
(DataSourceTransactionManager
)
Spring Boot对JDBC的默认配置:
- 启动器默认使用嵌入式数据库。默认schema.sql包含DDL语句,data.sql包含DML,可配置:
// 默认位于classpath下
spring:
datasource:
schema: classpath:db/h2/schema.sql
data: classpath:db/h2/test_data.sqlspring.data
- 默认会在启动时初始化数据库,可通过
spring.datasource.initialize = false
来进行更改;