百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术教程 > 正文

深入理解SpringJDBC的解决方案

csdh11 2024-11-30 14:14 24 浏览

数据访问

本部分关注高效访问关系型数据的相关实践。我们将系统讨论基于JDBC以及ORM框架实现数据访问的常见开发陷阱及其解决方法,同时,将进一步基于缓存机制分析如何使用它来优化数据访问性能。

通过这一部分的学习,读者将掌握如何系统性地分析和解决关系型数据访问过程中的开发问题,并加深对Spring JDBC、Spring Data JPA等框架的理解。

Spring JDBC解决方案

从本章开始,我们将进入Spring Boot中另一个核心技术体系的讨论,这个技术体系就是数据访问。无论互联网应用还是传统软件,对于任何一个系统而言,基于关系型数据库的存储和访问都是不可缺少的。

然而,数据库交互的过程通常也是应用程序性能的最大瓶颈。针对关系型数据库,Java世界中应用最广泛的就是JDBC规范,本章首先对这个经典规范展开讨论。

然后,将介绍基于Spring JDBC的数据库交互过程。在Spring JDBC中,为开发人员提供了JdbcTemplate这一非常实用的模板工具类,我们会对基于该工具类实现数据查询和插入的过程进行详细介绍,并深入剖析JdbcTemplate背后的实现原理。最后,将研究如何优化Spring JDBC的各项参数和使用方式

JDBC规范

JDBC是Java DataBase Connectivity的简称,它的设计初衷是提供一套能够应用于各种数据库的统一标准。不同的数据库厂家共同遵守这套标准,并提供各自的实现方案供应用程序调用。作为统一标准,JDBC规范具有完整的架构,如图8-1所示。

从图8-1中可以看到,Java应用程序通过JDBC所提供的API进行数据访问,而这些API中包含了开发人员所需要掌握的各个核心编程对象,包括DriverManger、DataSource、Connection、Statement以及ResultSet。使用这些JDBC API进行数据访问的示例代码如代码清单8-1所示。

代码清单8-1 使用JDBC API进行数据访问的示例代码

String query = "SELECT COUNT(*) FROM USER";

try{

Connection conn = dataSource.getConnection();

Statement statement = conn.createStatement(); ResultSet resultSet = statement.executeQuery(query)

if (resultSet.next()) {

int count = resultSet.getInt(1);

System.out.println("count : " + count);

}

}catch(SQLException e){

...

}

在上述代码中,我们看到JDBC API的异常检查机制迫使开发人员处理错误,这增加了应用程序中使用JDBC的代码复杂性。

同时,必须手工关闭数据库连接,如果开发人员忘记关闭连接,就会导致资源泄漏。事实上,上述代码中只有处理ResultSet部分的内容需要开发人员根据具体的业务对象进行定制化处理,而打开连接、执行SQL、关闭连接和执行异常处理部分的代码对于每次SQL操作而言都是重复的。Spring Boot针对使用JDBC过程中的复杂性和重复性的问题提供了Spring JDBC这套解决方案。

Spring JDBC解决方案

在Spring Boot中,JdbcTemplate模板工具类是我们基于JDBC规范实现数据访问的强大工具,它对常见的CRUD操作做了封装并提供了一大批简化的API。本节我们将分别针对查询和插入这两大类数据操作给出基于JdbcTemplate的实现方案。特别是针对插入场景,我们还将引入SimpleJdbcInsert工具类来简化这一操作,而SimpleJdbcInsert也是构建在JdbcTemplate基础之上的。

Spring JDBC工具类概览

针对8.1节给出的基于JDBC规范进行数据操作的示例代码,如果使用Spring JDBC,那么两行代码就可以完成同样的工作,如代码清单8-2所示。

代码清单8-2 使用Spring JDBC进行数据访问的示例代码

JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);

int count = jdbcTemplate.queryForObject("SELECT COUNT(*) FROM USER",

Integer.class);

可以看到,这里引入了一个Spring JDBC提供的JdbcTemplate模板工具类,而Jdbc-Template只是Spring JDBC众多工具类中的一个。

正如我们在前面的示例中看到的,Spring通过使用JDBC工具类简化了处理数据库访问的过程。JDBC工具类在内部使用JDBC API,但能够自动释放数据库连接以清理系统资源,并将JDBC的SQLException转换为RuntimeException,从而提供更好的错误检测机制。Spring JDBC工具类提供了多种直接编写SQL查询的方法,帮助我们移除了重复性代码,让开发人员只需要关注SQL查询动作本身。图8-2展示了Spring JDBC工具类在整个数据访问过程中所处的位置。

在Spring JDBC中,除了前面介绍的JdbcTemplate之外,还存在一大批实用的工具类,包括NamedParameterJdbcTemplate、SimpleJdbcTemplate、SimpleJdbcInsert和SimpleJdbcCall等。

JdbcTemplate应用

JdbcTemplate是Spring JDBC中最核心的一个工具类,本小节我们结合一个常见的应用场景来演示JdbcTemplate的具体使用方法。

设计一个系统的用户模块时,通常会涉及对用户权限的管理需求。通常,一个用户拥有一个账户(Account),而一个账户包含了多个权限(Authority)。反过来,一个权限可以属于多个不同的账户。这里的Account和Authority实体类定义如代码清单8-3所示。

代码清单8-3 Account和Authority类定义代码

public class Account {

private Long id;

private String accountNumber;

private String accountName;

private List<Authority> authorities;

}

public class Authority {

private Long id;

private String authorityCode;

private String authorityName;

private String description;

}

在设计关系型数据库表时,一般做法是构建一个中间表来保存Account和Authority之间的一对多关系。所以,在创建数据库时,我们会有三张表——account、authority以及中间表account_authority,其中account_authority表保存着account表和authority表中对应主键的映射关系。这三张表的SQL脚本如代码清单8-4所示。

代码清单8-4 Account和Authority相关表定义脚本

DROP TABLE IF EXISTS `account`;

DROP TABLE IF EXISTS `authority`;

DROP TABLE IF EXISTS `account_authority`;

create table `account` (

`id` bigint(20) NOT NULL AUTO_INCREMENT,

`account_number` varchar(50) not null,

`account_name` varchar(100) not null,

`create_time` timestamp not null DEFAULT CURRENT_TIMESTAMP,

PRIMARY KEY (`id`)

);

create table `authority` ( `id` bigint(20) NOT NULL AUTO_INCREMENT,

`authority_code` varchar(50) not null,

`authority_name` varchar(50) not null,

`description` varchar(100) not null,

`create_time` timestamp not null DEFAULT CURRENT_TIMESTAMP,

PRIMARY KEY (`id`)

);

create table `account_authority` (

`account_id` bigint(20) not null,

`authority_id` bigint(20) not null,

foreign key(`account_id`) references `account`(`id`),

foreign key(`authority_id`) references `authority`(`id`)

);

1. 原生JDBC实现方案

在讨论JdbcTemplate之前,为了做对比,我们首先给出基于原生JDBC的实现方案。因为原生JDBC的使用方式不是本书的重点,所以我们只提供getAccountById()方法的实现过程,如代码清单8-5所示。

代码清单8-5 getAccountById()方法的原生JDBC实现代码

public class AccountRawJdbcRepository {

@Autowired

private DataSource dataSource;

@Override

public Account getAccountById(Long accountId) {

Connection connection = null;

PreparedStatement statement = null;

ResultSet resultSet = null;

try {

connection = dataSource.getConnection();

statement = connection.prepareStatement("select id,

account_number, account_name from `account` where id=?");

statement.setLong(1, accountId);

resultSet = statement.executeQuery(); Account account = null;

if (resultSet.next()) {

account = new Account(resultSet.getLong("id"),

resultSet.getString("account_number"),

resultSet.getString("account_name"));

}

return account;

} catch (SQLException e) {

System.out.print(e);

} finally {

if (resultSet != null) {

try {

resultSet.close();

} catch (SQLException e) {

}

}

if (statement != null) {

try {

statement.close();

} catch (SQLException e) {

}

}

if (connection != null) {

try {

connection.close();

} catch (SQLException e) {

}

}

}

return null;

}

}

可以看到,上述代码使用JDBC原生DataSource、Connection、PreparedStatement、ResultSet等核心编程对象完成了针对account表的一次查询。代码功能比较简单,但流程显然比较烦琐。

2. 基于JdbcTemplate实现查询

回顾完原生JDBC的使用方法,接下来就引出本节的重点——JdbcTemplate模板工具类。我们创建一个AccountJdbcRepository类,并实现如代码清单8-6所示的getAccountById()方法。

代码清单8-6 getAccountById()方法的JdbcTemplate实现代码

public Account getAccountById(Long accountId) {

Account account = jdbcTemplate.queryForObject("select id,

account_number, account_name from `account` where id=?",

this::mapRowToAccount, accountId);

return account;

}

可以看到,这里使用了JdbcTemplate的queryForObject()方法来执行查询操作,该方法传入目标SQL、参数以及一个RowMapper对象。其中RowMapper的作用就是将来自数据库中的数据映射成领域对象。在这个示例中,mapRowToAccount()方法的实现过程如代码清单8-7所示。

代码清单8-7 RowMapper使用示例代码

private Account mapRowToAccount(ResultSet rs, int rowNum) throws

SQLException {

return new Account(rs.getLong("id"),

rs.getString("account_number"), rs.getString("account_name"));

}

讲到这里,你可能注意到getAccountById()方法实际上只是获取了Account对象中的账户部分信息,并不包含用户权限数据。接下来,我们再来设计一个getAuthorities-ByAccount()方法,用于根据账户编号获取用户账户以及账户对应的权限信息,如代码清单8-8所示。

代码清单8-8 getAuthoritiesByAccount()方法实现代码

public Account getAuthoritiesByAccount(String accountNumber) {

//获取Account基础信息

Account account = jdbcTemplate.queryForObject("select id,

account_number, account_name from `account` where account_number=?",

this::mapRowToAccount, accountNumber);

if (account == null) {

return account;

}

//获取Account与Authority之间的关联关系,找到Account中的所有AuthorityId

Long accountId = account.getId();

List<Long> authorityIds = jdbcTemplate.query("select account_id,

authority_id from account_authority where account_id=?",

newResultSetExtractor<List<Long>>() {

public List<Long> extractData(ResultSet rs) throws

SQLException, DataAccessException {

List<Long> list = new ArrayList<Long>();

while (rs.next()) {

list.add(rs.getLong("authority_id"));

}

return list;

}

}, accountId);

//根据AuthorityId分别获取Authority信息并填充到Account对象中

for (Long authorityId : authorityIds) {

Authority authority = getAuthorityById(authorityId);

account.addAuthority(authority);

}

return account;

}

上述代码有点复杂,可以分成几个部分来讲解。首先,我们获取Account基础信息,并通过Account中的ID编号从中间表中获取所有Authority的ID列表。然后遍历这个ID列表,再分别获取Authority信息。最后将Authority信息填充到Account中,从而构建一个完整的Account对象。这里通过ID获取Authority数据的实现方法也与getAccountById()方法的实现过程一样,如代码清单8-9所示。

代码清单8-9 getAccountById()方法实现代码

private Authority getAuthorityById(Long authorityId) {

return jdbcTemplate.queryForObject("select id, authority_code,

authority_name, description from authority where id=?",

this::mapRowToAuthority, authorityId);

}

private Authority mapRowToAuthority(ResultSet rs, int rowNum) throws

SQLException {

return new Authority(rs.getLong("id"),

rs.getString("authority_code"), rs.getString("authority_name"),

rs.getString("description"));

}

3. 基于JdbcTemplate实现插入

在JdbcTemplate中,可以通过update()方法来实现数据的插入和更新。

针对Account和Authority中的关联关系,插入一个Account对象需要同时完成两张表的更新,即account表和account_authority表,所以插入Account的实现过程也是分成两个阶段,如代码清单8-10所示的addAccount()方法展示了这一过程。

代码清单8-10 addAccount()方法实现代码

private Account addAccount(Account account) {

//插入Account基础信息

Long accountId = saveAccount(account);

account.setId(accountId); //插入Account与Authority的关联关系

List<Authority> authorityList = account.getAuthorities();

for (Authority authority : authorityList) {

saveAuthoritiesToAccount(authority, accountId);

}

return account;

}

可以看到,这里同样先是插入Account的基础信息,然后再遍历Account中的Authority列表并逐条进行插入。其中的saveAccount()方法如代码清单8-11所示。

代码清单8-11 saveAccount()方法实现代码

private Long saveAccount(Account account) {

PreparedStatementCreator psc = new PreparedStatementCreator() {

@Override

public PreparedStatement createPreparedStatement(Connection

con) throws SQLException {

PreparedStatement ps = con.prepareStatement("insert into

`account` (account_number, account_name) values (?, ?)",

Statement.RETURN_GENERATED_KEYS);

ps.setString(1, account.getAccountNumber());

ps.setString(2, account.getAccountName());

return ps;

}

};

KeyHolder keyHolder = new GeneratedKeyHolder();

jdbcTemplate.update(psc, keyHolder);

return keyHolder.getKey().longValue();

}

上述saveAccount()方法比想象中的要复杂,主要原因在于需要在插入account表的同时返回数据库中生成的自增主键值。因此,这里使用了PreparedStatementCreator这个工具类来封装PreparedStatement对象的构建过程,并在PreparedStatement的创建过程中设置了Statement.RETURN_GENERATED_KEYS属性用于返回自增主键。然后,我们同样构建了一个GeneratedKeyHolder对象用于保存所返回的自增主键。这是使用JdbcTemplate实现带有自增主键数据插入的一种标准做法,你可以参考这一做法并应用到日常开发过程中。

至于用于插入Account与Authority关联关系的saveAuthorityToAccount()方法就比较简单了,直接调用JdbcTemplate的update()方法向account_authority表中插入数据即可,如代码清单8-12所示。

代码清单8-12 saveAuthorityToAccount()方法实现代码

private void saveAuthorityToAccount(Authority authority, long

accountId) {

jdbcTemplate.update("insert into account_authority (account_id,

authority_id) " + "values (?, ?)", accountId, authority.getId());

}

SimpleJdbcInsert应用

通过JdbcTemplate的update()方法可以完成数据的正确插入,但我们发现这个实现过程还是比较复杂的,尤其是涉及自增主键处理的部分,代码显得有点臃肿。那么,有没有更加简单的实现方法呢?

答案是肯定的,Spring JDBC针对数据插入场景专门提供了一个SimpleJdbcInsert工具类。SimpleJdbcInsert本质上是在JdbcTemplate的基础上添加了一层封装,提供了一组execute()、executeAndReturnKey()以及executeBatch()重载方法来简化数据插入操作。通常,可以基于JdbcTemplate对SimpleJdbcInsert进行初始化,如代码清单8-13所示。

代码清单8-13 初始化SimpleJdbcInsert代码

SimpleJdbcInsert accountInserter = new

SimpleJdbcInsert(jdbcTemplate).withTableName("account").usingGenerated

KeyColumns("id");

SimpleJdbcInsert accountAuthorityInserter = new

SimpleJdbcInsert(jdbcTemplate).withTableName("account_authority");

可以看到,这里基于JdbcTemplate并针对account表和account_authority表分别初始化了两个SimpleJdbcInsert对象accountInserter和accountAuthorityInserter。其中accountInserter中还使用了usingGeneratedKeyColumns()方法来设置自增主键列。

基于SimpleJdbcInsert,完成Account对象的插入就显得非常简单了,实现方式如代码清单8-14所示。

代码清单8-14 基于SimpleJdbcInsert的saveAccount()代码

private Long saveAccountWithSimpleJdbcInsert(Account account) {

Map<String, Object> values = new HashMap<String, Object>();

values.put("account_number", account.getAccountNumber());

values.put("account_name", account.getAccountName());

Long accountId =

accountInserter.executeAndReturnKey(values).longValue();

return accountId;

}

我们构建一个Map对象,然后把需要添加的字段设置成一组键值对。通过Simple-JdbcInsert的executeAndReturnKey()方法就可以在插入数据的同时直接返回自增主键。同样,完成account_authority表的操作也只需要几行代码,如代码清单8-15所示。

代码清单8-15 基于SimpleJdbcInsert的saveAuthorityToAccount()代码

private void saveAuthorityToAccountWithSimpleJdbcInsert(Authority

authority, long accountId) {

Map<String, Object> values = new HashMap<>();

values.put("account_id", accountId);

values.put("authority_id", authority.getId());

accountAuthorityInserter.execute(values);

}

这里用到了SimpleJdbcInsert提供的execute()方法。我们可以把这些方法组合起来对原有基于JdbcTemplate的addAccount()方法进行重构,从而得到如代码清单8-16所示的新的addAccount()方法。

代码清单8-16 基于SimpleJdbcInsert的addAccount ()方法代码

private Account addAccount(Account account) {

//插入Account基础信息

Long accountId = saveAccountWithSimpleJdbcInsert(account);

account.setId(accountId);

//插入Account与Authority的关联关系

List<Authority> authorityList = account.getAuthorities();

for (Authority authority : authorityList) {

saveAuthorityToAccountWithSimpleJdbcInsert(authority,

accountId);

} return account;

}

可以看到,整个代码执行流程并没有任何改变,但具体的执行过程已经采用了更加高效的实现方式。

Spring JDBC案例分析

事实上,前面几小节展示的内容已经构成了一个完整的案例。我们可以通过Jdbc-Template完成对数据的查询和插入,以及使用SimpleJdbcInsert来简化数据插入过程。完整的案例源码可以参考:

https://github.com/tianminzheng/spring-boot

examples/tree/main/SpringJdbcExample。

请注意,要想在应用程序中使用JdbcTemplate,首先需要引入对它的依赖,如代码清单8-17所示。

代码清单8-17 spring-boot-starter-jdbc Maven依赖

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-jdbc</artifactId>

</dependency>

同时,不要忘了在Spring Boot应用程序的配置文件中添加数据源配置,如代码清单8-18所示。

代码清单8-18 数据源配置

spring:

datasource:

driver-class-name: com.mysql.cj.jdbc.Driver

url: jdbc:mysql://localhost:3306/account

username: root

password: root

在后面讨论Spring Data JPA时,我们还将基于这个案例对目前的实现方案进行重构。

本文给大家讲解的内容是springboot数据访问:Spring JDBC解决方案

  • 下文给大家讲解的是springboot数据访问: JdbcTemplate实现原理

相关推荐

探索Java项目中日志系统最佳实践:从入门到精通

探索Java项目中日志系统最佳实践:从入门到精通在现代软件开发中,日志系统如同一位默默无闻却至关重要的管家,它记录了程序运行中的各种事件,为我们排查问题、监控性能和优化系统提供了宝贵的依据。在Java...

用了这么多年的java日志框架,你真的弄懂了吗?

在项目开发过程中,有一个必不可少的环节就是记录日志,相信只要是个程序员都用过,可是咱们自问下,用了这么多年的日志框架,你确定自己真弄懂了日志框架的来龙去脉嘛?下面笔者就详细聊聊java中常用日志框架的...

物理老师教你学Java语言(中篇)(物理专业学编程)

第四章物质的基本结构——类与对象...

一文搞定!Spring Boot3 定时任务操作全攻略

各位互联网大厂的后端开发小伙伴们,在使用SpringBoot3开发项目时,你是否遇到过定时任务实现的难题呢?比如任务调度时间不准确,代码报错却找不到方向,是不是特别头疼?如今,随着互联网业务规模...

你还不懂java的日志系统吗 ?(java的日志类)

一、背景在java的开发中,使用最多也绕不过去的一个话题就是日志,在程序中除了业务代码外,使用最多的就是打印日志。经常听到的这样一句话就是“打个日志调试下”,没错在日常的开发、调试过程中打印日志是常干...

谈谈枚举的新用法--java(java枚举的作用与好处)

问题的由来前段时间改游戏buff功能,干了一件愚蠢的事情,那就是把枚举和运算集合在一起,然后运行一段时间后buff就出现各种问题,我当时懵逼了!事情是这样的,做过游戏的都知道,buff,需要分类型,且...

你还不懂java的日志系统吗(javaw 日志)

一、背景在java的开发中,使用最多也绕不过去的一个话题就是日志,在程序中除了业务代码外,使用最多的就是打印日志。经常听到的这样一句话就是“打个日志调试下”,没错在日常的开发、调试过程中打印日志是常干...

Java 8之后的那些新特性(三):Java System Logger

去年12月份log4j日志框架的一个漏洞,给Java整个行业造成了非常大的影响。这个事情也顺带把log4j这个日志框架推到了争议的最前线。在Java领域,log4j可能相对比较流行。而在log4j之外...

Java开发中的日志管理:让程序“开口说话”

Java开发中的日志管理:让程序“开口说话”日志是程序员的朋友,也是程序的“嘴巴”。它能让程序在运行过程中“开口说话”,告诉我们它的状态、行为以及遇到的问题。在Java开发中,良好的日志管理不仅能帮助...

吊打面试官(十二)--Java语言中ArrayList类一文全掌握

导读...

OS X 效率启动器 Alfred 详解与使用技巧

问:为什么要在Mac上使用效率启动器类应用?答:在非特殊专业用户的环境下,(每天)用户一般可以在系统中进行上百次操作,可以是点击,也可以是拖拽,但这些只是过程,而我们的真正目的是想获得结果,也就是...

Java中 高级的异常处理(java中异常处理的两种方式)

介绍异常处理是软件开发的一个关键方面,尤其是在Java中,这种语言以其稳健性和平台独立性而闻名。正确的异常处理不仅可以防止应用程序崩溃,还有助于调试并向用户提供有意义的反馈。...

【性能调优】全方位教你定位慢SQL,方法介绍下!

1.使用数据库自带工具...

全面了解mysql锁机制(InnoDB)与问题排查

MySQL/InnoDB的加锁,一直是一个常见的话题。例如,数据库如果有高并发请求,如何保证数据完整性?产生死锁问题如何排查并解决?下面是不同锁等级的区别表级锁:开销小,加锁快;不会出现死锁;锁定粒度...

看懂这篇文章,你就懂了数据库死锁产生的场景和解决方法

一、什么是死锁加锁(Locking)是数据库在并发访问时保证数据一致性和完整性的主要机制。任何事务都需要获得相应对象上的锁才能访问数据,读取数据的事务通常只需要获得读锁(共享锁),修改数据的事务需要获...