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

深入理解SpringJDBC的解决方案

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

数据访问

本部分关注高效访问关系型数据的相关实践。我们将系统讨论基于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实现原理

相关推荐

Github霸榜的SpringBoot全套学习教程,从入门到实战,内容超详细

前言...

SpringBoot+LayUI后台管理系统开发脚手架

源码获取方式:关注,转发之后私信回复【源码】即可免费获取到!项目简介本项目本着避免重复造轮子的原则,建立一套快速开发JavaWEB项目(springboot-mini),能满足大部分后台管理系统基础开...

Spring Boot+Vue全栈开发实战,中文版高清PDF资源

SpringBoot+Vue全栈开发实战,中文高清PDF资源,需要的可以私我:)SpringBoot致力于简化开发配置并为企业级开发提供一系列非业务性功能,而Vue则采用数据驱动视图的方式将程序...

2021年超详细的java学习路线总结—纯干货分享

本文整理了java开发的学习路线和相关的学习资源,非常适合零基础入门java的同学,希望大家在学习的时候,能够节省时间。纯干货,良心推荐!第一阶段:Java基础...

探秘Spring Cache:让Java应用飞起来的秘密武器

探秘SpringCache:让Java应用飞起来的秘密武器在当今快节奏的软件开发环境中,性能优化显得尤为重要。SpringCache作为Spring框架的一部分,为我们提供了强大的缓存管理能力,让...

3,从零开始搭建SSHM开发框架(集成Spring MVC)

目录本专题博客已共享在(这个可能会更新的稍微一些)https://code.csdn.net/yangwei19680827/maven_sshm_blog...

Spring Boot中如何使用缓存?超简单

SpringBoot中的缓存可以减少从数据库重复获取数据或执行昂贵计算的需要,从而显著提高应用程序的性能。SpringBoot提供了与各种缓存提供程序的集成,您可以在应用程序中轻松配置和使用缓...

我敢保证,全网没有再比这更详细的Java知识点总结了,送你啊

接下来你看到的将是全网最详细的Java知识点总结,全文分为三大部分:Java基础、Java框架、Java+云数据小编将为大家仔细讲解每大部分里面的详细知识点,别眨眼,从小白到大佬、零基础到精通,你绝...

1,从零开始搭建SSHM开发框架(环境准备)

目录本专题博客已共享在https://code.csdn.net/yangwei19680827/maven_sshm_blog1,从零开始搭建SSHM开发框架(环境准备)...

做一个适合二次开发的低代码平台,把程序员从curd中解脱出来-1

干程序员也有好长时间了,大多数时间都是在做curd。现在想做一个通用的curd平台直接将我们解放出来;把核心放在业务处理中。用过代码生成器,在数据表设计好之后使用它就可以生成需要的controller...

设计一个高性能Java Web框架(java做网站的框架)

设计一个高性能JavaWeb框架在当今互联网高速发展的时代,构建高性能的JavaWeb框架对于提升用户体验至关重要。本文将从多个角度探讨如何设计这样一个框架,让我们一起进入这段充满挑战和乐趣的旅程...

【推荐】强&amp;牛!一款开源免费的功能强大的代码生成器系统!

今天,给大家推荐一个代码生成器系统项目,这个项目目前收获了5.3KStar,个人觉得不错,值得拿出来和大家分享下。这是我目前见过最好的代码生成器系统项目。功能完整,代码结构清晰。...

Java面试题及答案总结(2025版持续更新)

大家好,我是Java面试分享最近很多小伙伴在忙着找工作,给大家整理了一份非常全面的Java面试场景题及答案。...

Java开发网站架构演变过程-从单体应用到微服务架构详解

Java开发网站架构演变过程,到目前为止,大致分为5个阶段,分别为单体架构、集群架构、分布式架构、SOA架构和微服务架构。下面玄武老师来给大家详细介绍下这5种架构模式的发展背景、各自优缺点以及涉及到的...

本地缓存GuavaCache(一)(guava本地缓存原理)

在并发量、吞吐量越来越大的情况下往往是离不开缓存的,使用缓存能减轻数据库的压力,临时存储数据。根据不同的场景选择不同的缓存,分布式缓存有Redis,Memcached、Tair、EVCache、Aer...