跳到主要内容Java JDBC 核心技术全解析 | 极客日志Javajava
Java JDBC 核心技术全解析
Java JDBC 核心技术,涵盖概念、核心 API(DriverManager、Connection、Statement、PreparedStatement)、基本使用步骤、SQL 注入问题及解决方案、事务管理、连接池(Druid)配置与使用,以及工具类封装方法。通过代码示例演示增删改查、批量插入、主键回显等实战场景。
ServerBase11K 浏览 一、JDBC 概述
1、概念
- jdbc 是 (Java Database Connectivity) 单词的缩写,翻译为 java 连接数据库
- jdbc 是 java 程序连接数据库的技术统称
- jdbc 由 java 语言的规范 (接口) 和各个数据库厂商的实现驱动 (jar) 组成
- jdbc 是一种典型的面向接口编程
- jdbc 优势
- 只需要学习 jdbc 规范接口的方法,即可操作所有的数据库软件
- 项目中期切换数据库软件,只需要更换对应的数据库驱动 jar 包,不需要更改代码
2、jdbc 核心 api 和使用路线
1、jdbc 技术组成
各个数据库厂商提供的驱动 jar 包
因为各个数据库厂商的 DBMS 软件各有不同,那么内部如何通过 sql 实现增、删、改、查等管理数据,只有这个数据库厂商自己更清楚,因此把接口规范的实现交给各个数据库厂商自己实现。
jar 包是什么?
java 程序打成的一种压缩包格式,你可以将这些 jar 包引入你的项目中,然后你可以使用这个 java 程序中类和方法以及属性了!
jdk 下 jdbc 规范接口,存储在 java.sql 和 javax.sql 包中的 api
为了项目代码的可移植性,可维护性,SUN 公司从最初就制定了 Java 程序连接各种数据库的统一接口规范。这样的话,不管是连接哪一种 DBMS 软件,Java 代码可以保持一致性。
2、涉及具体核心类和接口
- DriverManager
- 将第三方数据库厂商的实现驱动 jar 注册到程序中
- 可以根据数据库连接信息获取 connection
- Connection
- 和数据库建立的连接,在连接对象上,可以多次执行数据库 curd 动作
- 可以获取 statement 和 preparedstatement,callablestatement 对象
- Statement | PreparedStatement | CallableStatement
- 具体发送 SQL 语句到数据库管理软件的对象
- 不同发送方式稍有不同! preparedstatement 使用为重点!
- Result
- 面向对象思维的产物(抽象成数据库的查询结果表)
- 存储 DQL 查询数据库结果的对象
- 需要我们进行解析,获取具体的数据库数据
3、jdbc api 使用路线
- JDBC API 使用路线
- 静态 SQL 路线 (没有动态值语句)
- 预编译 SQL 路线 (有动态值语句)
- 执行标准存储过 SQL 路线
二、全新 JDBC 核心 API
1、引入 mysql-jdbc 驱动 jar
| mysql 版本 | 推荐驱动版本 | 备注 |
|---|
| mysql 5.5.x | 5.0.x | com.mysql.jdbc.Driver |
| mysql 5.7.x | 5.1.x | com.mysql.jdbc.Driver |
| mysql 8.x | 8.0.x | 建议:8.0.25+ 省略时区设置 com.mysql.cj.jdbc.Driver |
2、POM 依赖
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.18</version>
</dependency>
3、jdbc 基本使用步骤分析
- 注册驱动
- 获取连接
- 创建发送 sql 语句对象
- 发送 sql 语句,并获取返回结果
- 结果集解析
- 资源关闭
4、基于 statement 实现查询
CREATE DATABASE xx_jdbc;
USE xx_jdbc;
CREATE TABLE t_user(
id INT PRIMARY KEY AUTO_INCREMENT COMMENT '用户主键',
account VARCHAR(20) NOT NULL UNIQUE COMMENT '账号',
PASSWORD VARCHAR(64) NOT NULL COMMENT '密码',
nickname VARCHAR(20) NOT NULL COMMENT '昵称'
);
INSERT INTO t_user(account,PASSWORD,nickname) VALUES ('root','123456','技术总监'),('admin','666666','CTO');
package com.xx;
import java.sql.*;
public class JdbcBasePart {
public static void main(String[] args) throws SQLException {
DriverManager.registerDriver(new com.mysql.cj.jdbc.Driver());
Connection connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/xx_jdbc", "root", "mac_root");
Statement statement = connection.createStatement();
String sql = "select id,account,password,nickname from t_user ;";
ResultSet resultSet = statement.executeQuery(sql);
while (resultSet.next()) {
int id = resultSet.getInt("id");
String account = resultSet.getString("account");
String password = resultSet.getString("password");
String nickname = resultSet.getString("nickname");
System.out.println(id + "\t" + account + "\t" + password + "\t" + nickname);
}
resultSet.close();
statement.close();
connection.close();
}
}
5、基于 statement 方式问题
模拟登录,控制台输入账号和密码,判断是否登陆成功成功!
package com.xx;
import java.sql.*;
import java.util.Scanner;
public class JdbcStatementLoginPart {
public static void main(String[] args) throws ClassNotFoundException, SQLException {
Scanner scanner = new Scanner(System.in);
String account = scanner.nextLine();
String password = scanner.nextLine();
scanner.close();
Class.forName("com.mysql.cj.jdbc.Driver");
Connection connection = DriverManager.getConnection("jdbc:mysql:///xx_jdbc", "root", "mac_root");
Statement statement = connection.createStatement();
String sql = "select * from t_user where account = '" + account + "' and password = '" + password + "';";
ResultSet resultSet = statement.executeQuery(sql);
if (resultSet.next()) {
System.out.println("登录成功!");
} else {
System.out.println("登录失败!");
}
resultSet.close();
statement.close();
connection.close();
}
}
admin 666666 登录成功!
admin 123456 'or 1='1 登录成功!
1、存在问题
- SQL 语句需要字符串拼接,比较麻烦
- 只能拼接字符串类型,其他的数据库类型无法处理
动态值充当了 SQL 语句结构,影响了原有的查询结果!
6、基于 preparedStatement 方式优化
利用 preparedStatement 解决上述案例注入攻击和 SQL 语句拼接问题!
package com.xx;
import java.sql.*;
import java.util.Scanner;
public class JdbcPreparedStatementLoginPart {
public static void main(String[] args) throws ClassNotFoundException, SQLException {
Scanner scanner = new Scanner(System.in);
String account = scanner.nextLine();
String password = scanner.nextLine();
scanner.close();
Class.forName("com.mysql.cj.jdbc.Driver");
Connection connection = DriverManager.getConnection("jdbc:mysql:///xx_jdbc", "root", "mac_root");
String sql = "select * from t_user where account = ? and password = ? ;";
PreparedStatement preparedStatement = connection.prepareStatement(sql);
preparedStatement.setObject(2, password);
preparedStatement.setObject(1, account);
ResultSet resultSet = preparedStatement.executeQuery();
if (resultSet.next()) {
System.out.println("登录成功!");
} else {
System.out.println("登录失败!");
}
resultSet.close();
preparedStatement.close();
connection.close();
}
}
7、基于 preparedStatement 增删改查
1、新增
@Test
public void testInsert() throws Exception {
Class.forName("com.mysql.cj.jdbc.Driver");
Connection connection = DriverManager.getConnection("jdbc:mysql:///xx_jdbc", "root", "mac_root");
String sql = "insert into t_user(account,password,nickname) values (?,?,?);";
PreparedStatement preparedStatement = connection.prepareStatement(sql);
preparedStatement.setString(1, "test");
preparedStatement.setString(2, "test");
preparedStatement.setString(3, "测试");
int rows = preparedStatement.executeUpdate();
System.out.println(rows);
preparedStatement.close();
connection.close();
}
2、修改
@Test
public void testUpdate() throws Exception {
Class.forName("com.mysql.cj.jdbc.Driver");
Connection connection = DriverManager.getConnection("jdbc:mysql:///xx_jdbc", "root", "mac_root");
String sql = "update t_user set nickname = ? where account = ? ;";
PreparedStatement preparedStatement = connection.prepareStatement(sql);
preparedStatement.setString(1, "tomcat");
preparedStatement.setString(2, "test");
int rows = preparedStatement.executeUpdate();
System.out.println(rows);
preparedStatement.close();
connection.close();
}
3、删除
@Test
public void testDelete() throws Exception {
Class.forName("com.mysql.cj.jdbc.Driver");
Connection connection = DriverManager.getConnection("jdbc:mysql:///xx_jdbc", "root", "mac_root");
String sql = "delete from t_user where account = ? ;";
PreparedStatement preparedStatement = connection.prepareStatement(sql);
preparedStatement.setString(1, "test");
int rows = preparedStatement.executeUpdate();
System.out.println(rows);
preparedStatement.close();
connection.close();
}
4、查询
@Test
public void testQueryMap() throws Exception {
Class.forName("com.mysql.cj.jdbc.Driver");
Connection connection = DriverManager.getConnection("jdbc:mysql:///xx_jdbc", "root", "mac_root");
String sql = "select id,account,password,nickname from t_user ;";
PreparedStatement preparedStatement = connection.prepareStatement(sql);
ResultSet resultSet = preparedStatement.executeQuery();
List<Map> mapList = new ArrayList<>();
ResultSetMetaData metaData = resultSet.getMetaData();
int columnCount = metaData.getColumnCount();
while (resultSet.next()) {
Map map = new HashMap();
for (int i = 1; i <= columnCount; i++) {
map.put(metaData.getColumnLabel(i), resultSet.getObject(i));
}
mapList.add(map);
}
System.out.println(mapList);
preparedStatement.close();
connection.close();
resultSet.close();
}
三、全新 JDBC 扩展提升
1、自增长主键回显实现
java 程序获取插入数据时 mysql 维护自增长维护的主键id 值,这就是主键回显作用:在多表关联插入数据时,一般主表的主键都是自动生成的,所以在插入数据之前无法知道这条数据的主键,但是从表需要在插入数据之前就绑定主表的主键,这时可以使用主键回显技术:
@Test
public void returnPrimaryKey() throws Exception {
Class.forName("com.mysql.cj.jdbc.Driver");
Connection connection = DriverManager.getConnection("jdbc:mysql:///xx_jdbc?user=root&password=mac_root");
String sql = "insert into t_user (account,password,nickname) values (?,?,?);";
PreparedStatement statement = connection.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS);
statement.setObject(1, "tomcat");
statement.setObject(2, "123456");
statement.setObject(3, "汤姆猫");
int i = statement.executeUpdate();
System.out.println("i = " + i);
ResultSet resultSet = statement.getGeneratedKeys();
resultSet.next();
int anInt = resultSet.getInt(1);
System.out.println("pk = " + anInt);
statement.close();
connection.close();
}
2、批量数据插入性能提升
- 功能需求
- 批量数据插入优化
- 提升大量数据插入效率
- 功能实现
@Test
public void batchInsertYH() throws Exception {
Class.forName("com.mysql.cj.jdbc.Driver");
Connection connection = DriverManager.getConnection("jdbc:mysql:///xx_jdbc?rewriteBatchedStatements=true", "root", "mac_root");
String sql = "insert into t_user (account,password,nickname) values (?,?,?)";
long start = System.currentTimeMillis();
PreparedStatement statement = connection.prepareStatement(sql);
for (int i = 0; i < 10000; i++) {
statement.setObject(1, "ergouzi" + i);
statement.setObject(2, "lvdandan");
statement.setObject(3, "驴蛋蛋" + i);
statement.addBatch();
}
statement.executeBatch();
long end = System.currentTimeMillis();
System.out.println("消耗时间:" + (end - start));
connection.close();
}
3、jdbc 中数据库事务实现
1、事务概念
try{
connection.setAutoCommit(false);
connection.commit();
}catch(Exception e){
connection.rollback();
}
2、建表
CREATE TABLE t_bank(
id INT PRIMARY KEY AUTO_INCREMENT COMMENT '账号主键',
account VARCHAR(20) NOT NULL UNIQUE COMMENT '账号',
money INT UNSIGNED COMMENT '金额,不能为负值')
;
INSERT INTO t_bank(account,money) VALUES ('ergouzi',1000),('lvdandan',1000);
3、BankDao
4、BankService
package com.xx;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
public class BankService {
public void transfer(String addAccount, String subAccount, int money) throws ClassNotFoundException, SQLException {
System.out.println("addAccount = " + addAccount + ", subAccount = " + subAccount + ", money = " + money);
Class.forName("com.mysql.cj.jdbc.Driver");
Connection connection = DriverManager.getConnection("jdbc:mysql:///xx_jdbc", "root", "mac_root");
int flag = 0;
try {
connection.setAutoCommit(false);
BankDao bankDao = new BankDao();
bankDao.addMoney(addAccount, money, connection);
System.out.println("--------------");
bankDao.subMoney(subAccount, money, connection);
flag = 1;
connection.commit();
} catch (Exception e) {
connection.rollback();
throw e;
} finally {
connection.close();
}
if (flag == 1) {
System.out.println("转账成功!");
} else {
System.out.println("转账失败!");
}
}
}
5、BankTest
package com.xx;
import org.junit.Test;
public class BankTest {
@Test
public void testBank() throws Exception {
BankService bankService = new BankService();
bankService.transfer("ergouzi", "lvdandan", 500);
}
}
四、Druid 连接池使用
1、POM 依赖
2、硬编码方式 (了解,不推荐)
@Test
public void druidHard() throws SQLException {
DruidDataSource dataSource = new DruidDataSource();
dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
dataSource.setUsername("root");
dataSource.setPassword("root");
dataSource.setUrl("jdbc:mysql:///xx_jdbc");
Connection connection = dataSource.getConnection();
connection.close();
}
3、软编码方式
- 外部配置存放在src/druid.properties
driverClassName=com.mysql.cj.jdbc.Driver
username=root
password=root
url=jdbc:mysql:///xx_jdbc
@Test
public void druidSoft() throws Exception {
Properties properties = new Properties();
InputStream ips = DruidDemo.class.getClassLoader().getResourceAsStream("druid.properties");
properties.load(ips);
DataSource dataSource = DruidDataSourceFactory.createDataSource(properties);
}
4、druid 配置
| 配置 | 缺省 | 说明 |
|---|
| name | | 配置这个属性的意义在于,如果存在多个数据源,监控的时候可以通过名字来区分开来。如果没有配置,将会生成一个名字,格式是:'DataSource-' + System.identityHashCode(this) |
| jdbcUrl | | 连接数据库的 url,不同数据库不一样。例如:mysql : jdbc:mysql://10.20.153.104:3306/druid2 oracle : jdbc:oracle:thin:@10.20.149.85:1521:ocnauto |
| username | | 连接数据库的用户名 |
| password | | 连接数据库的密码。如果你不希望密码直接写在配置文件中,可以使用 ConfigFilter。详细看这里:https://github.com/alibaba/druid/wiki/%E4%BD%BF%E7%94%A8ConfigFilter |
| driverClassName | | 根据 url 自动识别 这一项可配可不配,如果不配置 druid 会根据 url 自动识别 dbType,然后选择相应的 driverClassName(建议配置下) |
| initialSize | 0 | 初始化时建立物理连接的个数。初始化发生在显示调用 init 方法,或者第一次 getConnection 时 |
| maxActive | 8 | 最大连接池数量 |
| maxIdle | 8 | 已经不再使用,配置了也没效果 |
| minIdle | | 最小连接池数量 |
| maxWait | | 获取连接时最大等待时间,单位毫秒。配置了 maxWait 之后,缺省启用公平锁,并发效率会有所下降,如果需要可以通过配置 useUnfairLock 属性为 true 使用非公平锁。 |
| poolPreparedStatements | false | 是否缓存 preparedStatement,也就是 PSCache。PSCache 对支持游标的数据库性能提升巨大,比如说 oracle。在 mysql 下建议关闭。 |
| maxOpenPreparedStatements | -1 | 要启用 PSCache,必须配置大于 0,当大于 0 时,poolPreparedStatements 自动触发修改为 true。在 Druid 中,不会存在 Oracle 下 PSCache 占用内存过多的问题,可以把这个数值配置大一些,比如说 100 |
| validationQuery | | 用来检测连接是否有效的 sql,要求是一个查询语句。如果 validationQuery 为 null,testOnBorrow、testOnReturn、testWhileIdle 都不会其作用。 |
| testOnBorrow | true | 申请连接时执行 validationQuery 检测连接是否有效,做了这个配置会降低性能。 |
| testOnReturn | false | 归还连接时执行 validationQuery 检测连接是否有效,做了这个配置会降低性能 |
| testWhileIdle | false | 建议配置为 true,不影响性能,并且保证安全性。申请连接的时候检测,如果空闲时间大于 timeBetweenEvictionRunsMillis,执行 validationQuery 检测连接是否有效。 |
| timeBetweenEvictionRunsMillis | | 有两个含义:1)Destroy 线程会检测连接的间隔时间 2)testWhileIdle 的判断依据,详细看 testWhileIdle 属性的说明 |
| numTestsPerEvictionRun | | 不再使用,一个 DruidDataSource 只支持一个 EvictionRun |
| minEvictableIdleTimeMillis | | |
| connectionInitSqls | | 物理连接初始化的时候执行的 sql |
| exceptionSorter | | 根据 dbType 自动识别 当数据库抛出一些不可恢复的异常时,抛弃连接 |
| filters | | 属性类型是字符串,通过别名的方式配置扩展插件,常用的插件有:监控统计用的 filter:stat 日志用的 filter:log4j 防御 sql 注入的 filter:wall |
| proxyFilters | | 类型是 List,如果同时配置了 filters 和 proxyFilters,是组合关系,并非替换关系 |
五、JDBC 使用优化以及工具类封装
1、jdbc 工具类封装一
driverClassName=com.mysql.cj.jdbc.Driver
username=root
password=root
url=jdbc:mysql:///xx_jdbc
package com.xx;
import com.alibaba.druid.pool.DruidDataSourceFactory;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.Properties;
public class JDBCToolsVersion1 {
private static DataSource ds;
static {
try {
Properties pro = new Properties();
pro.load(ClassLoader.getSystemResourceAsStream("druid.properties"));
ds = DruidDataSourceFactory.createDataSource(pro);
} catch (Exception e) {
e.printStackTrace();
}
}
public static Connection getConnection() throws SQLException {
return ds.getConnection();
}
public static void free(Connection conn) throws SQLException {
conn.setAutoCommit(true);
conn.close();
}
}
2、jdbc 工具类封装二
ThreadLocal 的介绍:
JDK 1.2 的版本中就提供 java.lang.ThreadLocal,为解决多线程程序的并发问题提供了一种新的思路。使用这个工具类可以很简洁地编写出优美的多线程程序。通常用来在在多线程中管理共享数据库连接、Session 等
ThreadLocal 用于保存某个线程共享变量,原因是在 Java 中,每一个线程对象中都有一个 ThreadLocalMap<ThreadLocal, Object>,其 key 就是一个 ThreadLocal,而 Object 即为该线程的共享变量。而这个 map 是通过 ThreadLocal 的 set 和 get 方法操作的。对于同一个 static ThreadLocal,不同线程只能从中 get,set,remove 自己的变量,而不会影响其他线程的变量。
1、ThreadLocal 对象.get: 获取 ThreadLocal 中当前线程共享变量的值。
2、ThreadLocal 对象.set: 设置 ThreadLocal 中当前线程共享变量的值。
3、ThreadLocal 对象.remove: 移除 ThreadLocal 中当前线程共享变量的值。
package com.xx;
import com.alibaba.druid.pool.DruidDataSourceFactory;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.Properties;
public class JDBCTools {
private static DataSource ds;
private static ThreadLocal<Connection> tl = new ThreadLocal<>();
static {
try {
Properties pro = new Properties();
pro.load(ClassLoader.getSystemResourceAsStream("druid.properties"));
ds = DruidDataSourceFactory.createDataSource(pro);
} catch (Exception e) {
e.printStackTrace();
}
}
public static Connection getConnection() throws SQLException {
Connection connection = tl.get();
if (connection == null) {
connection = ds.getConnection();
tl.set(connection);
}
return connection;
}
public static void free() throws SQLException {
Connection connection = tl.get();
if (connection != null) {
tl.remove();
connection.setAutoCommit(true);
connection.close();
}
}
}
3、封装 BaseDao
基本上每一个数据表都应该有一个对应的 DAO 接口及其实现类,发现对所有表的操作(增、删、改、查)代码重复度很高,所以可以抽取公共代码,给这些 DAO 的实现类可以抽取一个公共的父类,我们称为 BaseDao
package com.xx;
import java.lang.reflect.Field;
import java.sql.*;
import java.util.ArrayList;
public abstract class BaseDao {
protected int update(String sql, Object... args) throws SQLException {
Connection connection = JDBCTools.getConnection();
PreparedStatement ps = connection.prepareStatement(sql);
if (args != null && args.length > 0) {
for (int i = 0; i < args.length; i++) {
ps.setObject(i + 1, args[i]);
}
}
int len = ps.executeUpdate();
ps.close();
if (connection.getAutoCommit()) {
JDBCTools.free();
}
return len;
}
protected <T> ArrayList<T> query(Class<T> clazz, String sql, Object... args) throws Exception {
Connection connection = JDBCTools.getConnection();
PreparedStatement ps = connection.prepareStatement(sql);
if (args != null && args.length > 0) {
for (int i = 0; i < args.length; i++) {
ps.setObject(i + 1, args[i]);
}
}
ArrayList<T> list = new ArrayList<>();
ResultSet res = ps.executeQuery();
ResultSetMetaData metaData = res.getMetaData();
int columnCount = metaData.getColumnCount();
while (res.next()) {
T t = clazz.newInstance();
for (int i = 1; i <= columnCount; i++) {
Object value = res.getObject(i);
String columnName = metaData.getColumnLabel(i);
Field field = clazz.getDeclaredField(columnName);
field.setAccessible(true);
field.set(t, value);
}
list.add(t);
}
res.close();
ps.close();
if (connection.getAutoCommit()) {
JDBCTools.free();
}
return list;
}
protected <T> T queryBean(Class<T> clazz, String sql, Object... args) throws Exception {
ArrayList<T> list = query(clazz, sql, args);
if (list == null || list.size() == 0) {
return null;
}
return list.get(0);
}
}
相关免费在线工具
- Keycode 信息
查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online
- Escape 与 Native 编解码
JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online
- JavaScript / HTML 格式化
使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online
- JavaScript 压缩与混淆
Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online
- Base64 字符串编码/解码
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
- Base64 文件转换器
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online