第01章 JDBC概述 什么是JDBC JDBC(Java DataBase Connectivity)就是Java数据库连接,说白了就是用Java语言来操作数据库。原来我们操作数据库是在控制台使用SQL语句来操作数据库,JDBC是用Java语言向数据库发送SQL语句。
JDBC是一套连接数据库的api,什么是APi?就是平时所说的一套类库,通常包括三项:
API字节码(String.class)
API源码(String.java)
API帮助文档(String类的帮助文档)
但是要注意查阅的版本要一致。
JDBC原理 早期SUN公司的天才们想编写一套可以连接天下所有数据库的API,但是当他们刚刚开始时就发现这是不可完成的任务,因为各个厂商的数据库服务器差异太大了。后来SUN开始与数据库厂商们讨论,最终得出的结论是,由SUN提供一套访问数据库的规范(就是一组接口),并提供连接数据库的协议标准,然后各个数据库厂商会遵循SUN的规范提供一套访问自己公司数据库服务器的API。SUN提供的规范命名为JDBC,而各个厂商提供的,遵循了JDBC规范的,可以访问自己数据库的API被称之为驱动!
JDBC是接口,而JDBC驱动才是接口的实现,没有驱动无法完成数据库连接!每个数据库厂商都有自己的驱动,用来连接自己公司的数据库。 当然还有第三方公司专门为某一数据库提供驱动,这样的驱动往往不是开源免费的!
jdbc的本质:
模拟JDBC接口 模拟JDBC接口 Java中接口的作用主要有以下几个方面:
定义标准:接口可以用于定义标准,规范应该如何完成某个任务或应该具有哪些属性、方法等。
隐藏实现:接口隔离了类的实现和外界的逻辑使用,使得外部无论是访问接口的常量或是接口的方法都不需要关心接口的实现。
实现多态:一个类实现多个接口,在实现接口的过程中,类便会具有接口中的所有方法。这样我们就可以在实际应用中方便的实现多态的效果。
扩展性和灵活性:通过接口可以为项目提供更好的扩展性和灵活性,接口定义了一个共同的标准,使得新的类可以很容易地加入到已有的系统中,而且不需要修改现有的代码。
总的来说,Java中的接口可以让我们通过规范来编写更加标准和灵活的代码,使得代码易于维护和扩展,并通过多态的特性来提高代码的重用性和可读性。**Java接口在使用场景中,一定是存在两个角色的,一个是接口的调用者,一个是接口的实现者,接口的出现让调用者和实现者解耦合了
编写程序模拟JDBC接口 接口的制定者 :SUN公司负责制定的
1 2 3 4 5 public interface JDBC { void getConnection () ; }
接口的实现者 :各大数据库厂商分别对JDBC接口进行实现,实现类被称为驱动 MySQL数据库厂商对JDBC接口的实现:MySQL驱动
1 2 3 4 5 public class MySQLDriver implements JDBC { public void getConnection () { System.out.println("与MySQL数据库连接建立成功,您正在操作MySQL数据库" ); } }
Oracle数据库厂商对JDBC接口的实现:Oracle驱动
1 2 3 4 5 public class OracleDriver implements JDBC { public void getConnection () { System.out.println("与Oracle数据库连接建立成功,您正在操作Oracle数据库" ); } }
接口的调用者 :要操作数据库的Java程序员(我们)
1 2 3 4 5 6 7 8 9 10 public class Client { public static void main (String[] args) { JDBC jdbc = new MySQLDriver (); JDBC jdbc = new OracleDriver (); jdbc.getConnection(); } }
可能你会说,最终还是修改了java代码,不符合OCP原则呀,如果你想达到OCP,那可以将创建对象的任务交给反射机制,将类名配置到文件中,例如: 配置文件如下:
Java代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 import java.util.ResourceBundle;public class javaProgrammer { public static void main (String[] args) throws Exception{ ResourceBundle bundle = ResourceBundle.getBundle("jdbc" ); String driverClassName = bundle.getString("driver" ); Class clazz = Class.forName(driverClassName); JDBC jdbc = (JDBC)c.newInstance(); jdbc.getConnection(); } }
JDBC的本质:它就是一套接口。
配置CLASSPATH JDBC接口在哪 JDBC接口在JDK中。对应的包是:java.sql.*; JDBC API帮助文档就在JDK的帮助文档当中。
驱动在哪 打开页面:https://dev.mysql.com/downloads/connector/j/
解压:上图中的“mysql-connector-j-8.2.0.jar”就是mysql数据库的驱动,可以使用解压工具打开这个jar包
可以看到这个jar包中都是xxx.class文件,这就是JDBC接口的实现类。这个jar包就是连接mysql数据库的驱动。如果是oracle的驱动就需要去oracle的官网下载了。这里不再赘述。
如果使用文本编辑器开发 如果使用文本编辑器开发,不使用集成开发环境的话,以上的jar包就需要手动配置到环境变量CLASSPATH当中,配置如下: 如果jar包放在这里:
就需要这样配置环境变量CLASSPATH:
注意:配置了这个目录路径就说明class文件会从这个jar包路径运行,如果默认的话是当前路径,就需要区分,用.来设置当前路径,用;来分开路径
如果使用IDEA工具开发 如果是采用集成开发工具,例如IDEA,就不需要手动配置CLASSPATH了,只需要将jar包放到IDEA中(实际上放到IDEA工具中的过程就是等同于在配置CLASSPATH)
第一步:创建lib目录,将jar包拷贝到lib目录
第二步:把lib包引入项目环境
第02章 JDBC的新增修改删除 JDBC编程六步 JDBC编程的步骤是很固定的,通常包含以下六步:
第一步:注册驱动
作用一:将 JDBC 驱动程序从硬盘上的文件系统中加载到内存中。
作用二:使得 DriverManager 可以通过一个统一的接口来管理该驱动程序的所有连接操作。
第二步:获取数据库连接
获取java.sql.Connection对象,该对象的创建标志着mysql进程和jvm进程之间的通道打开了。
第三步:获取数据库操作对象
获取java.sql.Statement对象,该对象负责将SQL语句发送给数据库,数据库负责执行该SQL语句。
第四步:执行SQL语句
执行具体的SQL语句,例如:insert delete update select等。
第五步:处理查询结果集
如果之前的操作是DQL查询语句,才会有处理查询结果集这一步。
执行DQL语句通常会返回查询结果集对象:java.sql.ResultSet。
对于ResultSet查询结果集来说,通常的操作是针对查询结果集进行结果集的遍历。
第六步:释放资源
释放资源可以避免资源的浪费。在 JDBC 编程中,每次使用完 Connection、Statement、ResultSet 等资源后,都需要显式地调用对应的 close() 方法来释放资源,避免资源的浪费。(有关闭顺序,从后往前关
释放资源可以避免出现内存泄露问题。在 Java 中,当一个对象不再被引用时,会被 JVM 的垃圾回收机制进行回收。但是在 JDBC 编程中,如果不显式地释放资源,那么这些资源就不会被 JVM 的垃圾回收机制自动回收,从而导致内存泄露问题。
数据的准备 使用PowerDesigner设计用户表t_user。 使用Navicat for MySQL创建数据库,创建表,插入数据。
PowerDesigner与Navicat for MySQL的区别 Navicat for MySQL 是一款常用的 MySQL 数据库管理工具,提供了丰富的数据库管理和开发工具,可以方便地进行数据库的连接、查询、管理、模型设计等操作,是 MySQL 开发和管理的效率工具。
而 PowerDesigner 工具则是一款专业的建模工具,它支持多种数据库和操作系统,可以完成数据库设计、数据建模、过程建模、企业业务建模等工作。PowerDesigner 可以帮助开发人员在数据库设计和开发过程中更好地理解和管理数据,便于协同开发和项目管理。各种数据库技术的建模形式都可以实现,有单个数据库建模到多个数据库建模和业务建模等高级功能,非常适用于大型项目中的数据库设计和建模 。同时,PowerDesigner 还支持 UML,Java 等编程语言的建模,可以与开发语言无缝整合。
因此,Navicat for MySQL 和 PowerDesigner 的功能是不同的,可以根据实际需要来选用。如果只是针对 MySQL 的数据库连接、查询、管理等操作,可以使用 Navicat for MySQL 工具,而如果需要进行更复杂的数据库设计、建模和整合等工作,可以使用 PowerDesigner 工具来实现。如果使用 MySQL 数据库进行开发,使用 Navicat for MySQL 和 PowerDesigner 这两个工具相互配合,可以提高开发效率和数据管理质量 。
PowerDesigner工具的安装 来这里下载该工具:链接:https://pan.baidu.com/s/1lRWC069K8GE-8rxr259ArQ?pwd=2009 提取码:2009 双击安装包,选择香港,以及接受,替换文件,完成
使用PowerDesigner进行物理数据建模 什么是物理数据模型PDM? `物理数据模型(Physical Data Model,PDM)是数据管理领域中表示数据库逻辑设计后,通过物理设计最终转化为实际数据结构的过程,即在逻辑模型的基础上,进行数据存储结构的设计。
注意:
Name:用来设置显示的表名
Code:用来设置数据库中真实创建的表名
Comment:对表的注释说明
从前往后是名字,真实表名,类型,长度,不知道,主键,不知道,是否为空。
双击这个1·会打开一个窗口,在comment里可以写注释,在preview里保存sql文件,打开数据库,新建数据库然后执行sql文件,就可以添加数据了
JDBC完成新增操作 新增操作就是让数据库执行insert语句。通过这个操作来学习一下JDBC编程的每一步。刚开始编写JDBC代码的时候,建议使用文本编辑器,先不借助任何IDE。
JDBC编程第一步:注册驱动 注册驱动有两个作用:
将 JDBC 驱动程序从硬盘上的文件系统中加载到内存。
让 DriverManager 可以通过一个统一的接口来管理该驱动程序的所有连接操作。
API帮助文档:
代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 import java.sql.Driver;import java.sql.DriverManager;import java.sql.SQLException;public class JDBCTest01 { public static void main (String[] args) { try { Driver driver = new com .mysql.cj.jdbc.Driver(); DriverManager.registerDriver(driver); } catch (SQLException e){ e.printStackTrace(); } } }
JDBC编程第二步:获取连接 获取java.sql.Connection对象,该对象的创建标志着mysql进程和jvm进程之间的通道打开了。
代码实现 代码如下:
1 2 3 4 5 6 7 8 9 10 11 String url = "jdbc:mysql://localhost:3306/jdbc" ; String user = "root" ; String password = "123456" ; Connection conn = DriverManager.getConnection(url, user, password); System.out.println("连接对象:" + conn);
JDBC编程第三步:获取数据库操作对象 数据库操作对象是这个接口:java.sql.Statement。这个对象负责将SQL语句发送给数据库服务器,服务器接收到SQL后进行编译,然后执行SQL。
获取数据库操作对象代码如下:
1 2 3 4 5 6 7 import java.sql.Statement; Statement stmt = conn.createStatement(); System.out.println("数据库操作对象stmt = " + stmt); Statement stmt2 = conn.createStatement(); System.out.println("数据库操作对象stmt = " + stmt);
执行结果如下:
同样可以看到:java.sql.Statement接口在MySQL驱动中的实现类是:com.mysql.cj.jdbc.StatementImpl。不过我们同样是不需要关心这个具体的实现类。因为后续的代码仍然是面向Statement接口写代码的。
JDBC编程第四步:执行SQL 当获取到Statement对象后,调用这个接口中的相关方法即可执行SQL语句。通过execute(String sql)返回布尔值
1 2 3 4 5 6 String sql = "insert into t_user(name,password,realname,gender,tel) values('tangsanzang','123','唐三藏','男','12566568956');" ; boolean isSuccess = stmt.execute(sql); System.out.println(isSuccess ? "插入失败" : "插入成功" );
API帮助文档如下:
返回1表示1条数据插入成功,返回2表示2条数据插入成功,以此类推。如果一条也没有插入,则返回0。
该方法适合执行的SQL语句是DML,包括:insert delete update。
1 2 3 4 String sql = "insert into t_user(name,password,realname,gender,tel) values('tangsanzang','123','唐三藏','男','12566568956')" ; int count = stmt.executeUpdate(sql); System.out.println("插入了" + count + "条记录" );
JDBC编程第六步:释放资源 第五步去哪里了?第五步是处理查询结果集,以上操作不是select语句,所以第五步直接跳过,直接先看一下第六步释放资源。【后面学习查询语句的时候,再详细看第五步】
为什么要释放资源 在 JDBC 编程中,建立数据库连接、创建 Statement 对象等操作都需要申请系统资源,例如打开网络端口、申请内存等。为了避免占用过多的系统资源和避免出现内存泄漏等问题,我们需要在使用完资源后及时释放它们。
释放资源的原则 原则1:在finally语句块中释放
建议在finally语句块中释放,因为程序执行过程中如果出现了异常,finally语句块中的代码是一定会执行的。也就是说:我们需要保证程序在执行过程中,不管是否出现了异常,最后的关闭是一定要执行的。当然了,也可以使用Java7的新特性:Try-with-resources。Try-with-resources 是 Java 7 引入的新特性。它简化了资源管理的代码实现,可以自动释放资源,减少了代码出错的可能性,同时也可以提供更好的代码可读性和可维护性。
原则2:释放有顺序
从小到大依次释放,创建的时候,先创建Connection,再创建Statement。那么关闭的时候,先关闭Statement,再关闭Connection。
原则3:分别进行try…catch…
关闭的时候调用close()方法,该方法有异常需要处理,建议分别对齐try…catch…进行异常捕获。如果只编写一个try…catch…进行一块捕获,在关闭过程中,如果某个关闭失败,会影响下一个资源的关闭。
代码如何实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 } catch (SQLException e){ e.printStackTrace(); } finally { if (stmt != null ){ try { stmt.close(); }catch (SQLException e){ e.printStackTrace(); } } if (conn != null ){ try { conn.close(); }catch (SQLException e){ e.printStackTrace(); } } }
什么是URL URL 是统一资源定位符 (Uniform Resource Locator) 的缩写,是互联网上标识、定位、访问资源的字符串。它可以用来指定互联网上各种类型的资源的位置,如网页、图片、视频等。
URL 通常由协议、服务器名、服务器端口、路径和查询字符串组成。其中:
协议是规定了访问资源所采用的通信协议,例如 HTTP、HTTPS、FTP 等;
服务器名是资源所在的服务器主机名或 IP 地址,可以是域名或 IP 地址;
服务器端口是资源所在的服务器的端口号;
路径是资源所在的服务器上的路径、文件名等信息;
查询字符串是向服务器提交的参数信息,用来定位更具体的资源。
URL 在互联网中广泛应用,比如在浏览器中输入 URL 来访问网页或下载文件,在网站开发中使用 URL 来访问 API 接口或文件,在移动应用和桌面应用中使用 URL 来访问应用内部的页面或功能,在搜索引擎中使用 URL 来爬取网页内容等等。
总之,URL 是互联网上所有资源的唯一识别标识,是互联网通信的基础和核心技术之一。
JDBC连接MySQL时的URL格式 JDBC URL 是在使用 JDBC 连接数据库时的一个 URL 字符串,它用来标识要连接的数据库的位置、认证信息和其他配置参数等。JDBC URL 的格式可以因数据库类型而异,但通常包括以下几个部分:
协议:表示要使用的数据库管理系统(DBMS)的类型,如 jdbc:mysql 表示要使用 MySQL 数据库,jdbc:postgresql 表示要使用 PostgreSQL 数据库。
主机地址和端口号:表示要连接的数据库所在的服务器的 IP 地址或域名,以及数据库所在服务器监听的端口号。
数据库名称:表示要连接的数据库的名称。
其他可选参数:这些参数包括连接的超时时间、使用的字符集、连接池相关配置等。
例如,连接 MySQL 数据库的 JDBC URL 的格式一般如下:可以在?后面加用户名密码
1 jdbc:mysql://<host>:<port>/<database_name>?<connection_parameters>
其中:
<host> 是 MySQL 数据库服务器的主机名或 IP 地址;
<port> 是 MySQL 服务器的端口号(默认为 3306);
<database_name> 是要连接的数据库名称;
<connection_parameters> 包括连接的额外参数,例如用户名、密码、字符集等。
JDBC URL 是连接数据库的关键,通过 JDBC URL,应用程序可以通过特定的 JDBC 驱动程序与数据库服务器进行通信,从而实现与数据库的交互。在开发 Web 应用和桌面应用时,使用 JDBC URL 可以轻松地连接和操作各种类型的数据库,例如 MySQL、PostgreSQL、Oracle 等。
以下是一个常见的JDBC MySQL URL:
1 jdbc:mysql://localhost:3306/jdbc
jdbc:mysql://是协议localhost表示连接本地主机的MySQL数据库,也可以写作127.0.0.13306是MySQL数据库的端口号jdbc是数据库实例名
MySQL URL中的其它常用配置 在 JDBC MySQL URL 中,常用的配置参数有:
serverTimezone:MySQL 服务器时区,默认为 UTC,可以通过该参数来指定客户端和服务器的时区;
在 JDBC URL 中设置 serverTimezone 的作用是指定数据库服务器的时区。这个时区信息会影响 JDBC 驱动在处理日期时间相关数据类型时如何将其映射到服务器上的日期时间值。如果不设置 serverTimezone,则 JDBC 驱动程序默认将使用本地时区,也就是客户端机器上的系统时区,来处理日期时间数据 。在这种情况下,如果服务器的时区和客户端机器的时区不同,那么处理日期时间数据时可能会出现问题,从而导致数据错误或不一致。 例如,假设服务器位于美国加州,而客户端位于中国上海,如果不设置 serverTimezone 参数,在客户端执行类似下面的查询:
1 SELECT * FROM orders WHERE order_date = '2022-11-11' ;
由于客户端和服务器使用了不同的时区,默认使用的是客户端本地的时区,那么实际查询的时间就是客户端本地时间对应的时间,而不是服务器的时间。这可能会导致查询结果不正确,因为服务器上的时间可能是比客户端慢或者快了多个小时。 通过在 JDBC URL 中设置 serverTimezone 参数,可以明确告诉 JDBC 驱动程序使用哪个时区来处理日期时间值,从而避免这种问题。在上述例子中,如果把时区设置为 America/Los_Angeles(即加州的时区):
1 jdbc:mysql://localhost:3306/mydatabase?user=myusername&password=mypassword&serverTimezone=America/Los_Angeles
那么上面的查询就会在数据库服务器上以加州的时间来执行,结果更加准确。
useSSL:是否使用 SSL 进行连接,默认为 true;
useSSL 参数用于配置是否使用 SSL(Secure Sockets Layer)安全传输协议来加密 JDBC 和 MySQL 数据库服务器之间的通信。其设置为 true 表示使用 SSL 连接,设置为 false 表示不使用 SSL 连接。其区别如下:
当设置为 true 时,JDBC 驱动程序将使用 SSL 加密协议来保障客户端和服务器之间的通信安全 。这种方式下,所有数据都会使用 SSL 加密后再传输,可以有效防止数据在传输过程中被窃听、篡改等安全问题出现。当然,也要求服务器端必须支持 SSL,否则会连接失败。
当设置为 false 时,JDBC 驱动程序会以明文方式传输数据,这种方式下,虽然数据传输的速度会更快,但也会存在被恶意攻击者截获和窃听数据的风险。因此,在不安全的网络环境下,或是要求数据传输安全性较高的情况下,建议使用 SSL 加密连接。
需要注意的是,使用 **SSL 连接会对系统资源和性能消耗有一定的影响,特别是当连接数较多时,对 CPU 和内存压力都比较大。**因此,在性能和安全之间需要权衡,根据实际应用场景合理设置 useSSL 参数。
useUnicode:是否使用Unicode编码进行数据传输,默认是true启用
useUnicode是 JDBC 驱动程序连接数据库时的一个参数,用于告诉驱动程序在传输数据时是否使用 Unicode 编码。主要是为了跨平台
具体地说,如果设置 useUnicode=true,JDBC 驱动程序会在传输数据时使用 Unicode 编码。这意味着,无论数据源中使用的是什么编码方案,都会先将数据转换为 Unicode 编码进行传输,确保数据能够跨平台、跨数据库正确传输。当从数据库中获取数据时,驱动程序会根据 characterEncoding 参数指定的字符集编码将数据转换为指定编码格式,以便应用程序正确处理数据。
需要注意的是,如果设置 useUnicode=false,则表示使用当前平台默认的字符集进行数据传输。这可能会导致在跨平台或跨数据库时出现字符编码不一致的问题,因此通常建议在进行数据传输时启用 Unicode 编码。
characterEncoding:连接使用的字符编码,默认为 UTF-8;
characterEncoding 参数用于设置 MySQL 服务器和 JDBC 驱动程序之间进行字符集转换时使用的字符集编码。其设置为 UTF-8 表示使用 UTF-8 编码进行字符集转换,设置为 GBK 表示使用 GBK 编码进行字符集转换。其区别如下: UTF-8 编码是一种可变长度的编码方式,可以表示世界上的所有字符,包括 ASCII、Unicode 和不间断空格等字符,是一种通用的编码方式。UTF-8 编码在国际化应用中被广泛使用,并且其使用的字节数较少,有利于提高数据传输的效率和节约存储空间。
注意:useUnicode和characterEncoding有什么区别?
useUnicode设置的是数据在传输过程中是否使用Unicode编码方式。
characterEncoding设置的是数据被传输到服务器之后,服务器采用哪一种字符集进行编码。
例如,连接 MySQL 数据库的 JDBC URL 可以如下所示:
1 jdbc:mysql://localhost:3306/jdbc?useUnicode=true&serverTimezone=Asia/Shanghai&useSSL=true&characterEncoding=utf-8
这里演示的是使用本地 MySQL 数据库,使用Unicode编码进行数据传输,服务器时区为 Asia/Shanghai,启用 SSL 连接,服务器接收到数据后使用 UTF-8 编码。
注册驱动的常用方式 上面在注册驱动的时候,执行了这样的代码:
1 2 java.sql.Driver driver = new com .mysql.cj.jdbc.Driver(); java.sql.DriverManager.registerDriver(driver);
这种方式是自己new驱动对象,然后调用DriverManager的registerDriver()方法来完成驱动注册,还有另一种方式,并且这种方式是常用的:
1 Class.forName("com.mysql.cj.jdbc.Driver" );
为什么这种方式常用?
第一:代码少了很多。
第二:这种方式可以很方便的将com.mysql.cj.jdbc.Driver类名配置到属性文件当中。
实现原理是什么?找一下com.mysql.cj.jdbc.Driver的源码:
在com.mysql.cj.jdbc.Driver类中有一个静态代码块,在这个静态代码块中调用了java.sql.DriverManager.registerDriver(new Driver());完成了驱动的注册。而Class.forName("com.mysql.cj.jdbc.Driver");代码的作用就是让com.mysql.cj.jdbc.Driver类完成加载,执行它的静态代码块。
这样写也主要是为了方便在配置文件里改动,而不是写死在代码里
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 import java.sql.*;public class JDBCTest04 { public static void main (String[] args) throws Exception{ String url = "jdbc:mysql://localhost:3306/jdbc?useUnicode=true&serverTimezone=Asia/Shanghai&useSSL=true&characterEncoding=utf-8" ; String user = "root" ; String password = "1234" ; Connection conn = DriverManager.getConnection(url, user, password); System.out.println(conn); } }
JDBC 4.0后不用手动注册驱动(了解) 从JDBC 4.0(也就是Java6 )版本开始,驱动的注册不需要再手动完成,由系统自动完成。
也就是第一步可以不用写了。
注意:虽然大部分情况下不需要进行手动注册驱动了,但在实际的开发中有些数据库驱动程序不支持自动发现功能,仍然需要手动注册。所以建议大家还是别省略了。
IDEA创建JDBK 先新建一个project,然后点file里的 设置一下jdk版本为21
然后新建一个目录 ,写上lib,然后把jdbc的jar包复制,选中lib粘贴就进去了,选中这个jar包右击有个add as library就添加进去了
动态配置连接数据库的信息 为了程序的通用性,为了切换数据库的时候不需要修改Java程序,为了符合OCP开闭原则,建议将连接数据库的信息配置到属性文件中,例如:
1 2 3 4 driver =com.mysql.cj.jdbc.Driver url =jdbc:mysql://localhost:3306/jdbc?useUnicode=true&serverTimezone=Asia/Shanghai&useSSL=true&characterEncoding=utf-8 user =root password =123456
然后使用IO流读取属性文件,动态获取连接数据库的信息:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 public class JDBCTest01 { public static void main (String[] args) { ResourceBundle bundle = ResourceBundle.getBundle("com.powernode.jdbc.jdbc01" ); String driver = bundle.getString("driver" ); String url = bundle.getString("url" ); String user = bundle.getString("user" ); String password = bundle.getString("password" ); Connection conn = null ; try { Class.forName(driver); conn = DriverManager.getConnection(url, user, password); System.out.println(conn); } catch (ClassNotFoundException e) { throw new RuntimeException (e); } catch (SQLException e) { throw new RuntimeException (e); } finally { if (conn != null ) { try { conn.close(); } catch (SQLException e) { throw new RuntimeException (e); } } } } }
获取连接的其他方式(了解) 上面我们讲到了第一种获取连接的方式:
1 Connection conn = DriverManager.getConnection(url, user, password);
getConnection(String url) 这种方式参数只有一个url,那用户名和密码放在哪里呢?可以放到url当中,代码如下:
1 2 3 4 5 6 Class.forName("com.mysql.cj.jdbc.Driver" ); String url = "jdbc:mysql://localhost:3306/jdbc?user=root&password=liuqi3711022&useUnicode=true&serverTimezone=Asia/Shanghai&useSSL=true&characterEncoding=utf-8" ; Connection conn = DriverManager.getConnection(url); System.out.println(conn);
getConnection(String url, Properties info) 这种方式有两个参数,一个是url,一个是Properties对象。
url:可以单纯提供一个url地址
info:可以将url的参数存放到该对象中
代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 Class.forName("com.mysql.cj.jdbc.Driver" ); String url = "jdbc:mysql://localhost:3306/jdbc" ; Properties info = new Properties (); info.setProperty("user" , "root" ); info.setProperty("password" , "1234" ); info.setProperty("useUnicode" , "true" ); info.setProperty("serverTimezone" , "Asia/Shanghai" ); info.setProperty("useSSL" , "true" ); info.setProperty("characterEncoding" , "utf-8" ); Connection conn = DriverManager.getConnection(url, info); System.out.println("====>" + conn);
第03章 JDBC的查询 JDBC的查询操作 ResultSet 是 JDBC (Java 数据库连接) API 提供的接口,它用于表示 SQL 查询的结果集。ResultSet 对象中包含了查询结果的所有行,可以通过 next() 方法逐行地获取并处理每一行的数据。它最常用于执行 SELECT 语句查询出来的结果集。
ResultSet 的遍历是基于 JDBC 的流式处理机制的,即一行一行地获取结果,避免将所有结果全部取出后再进行处理导致内存溢出问题。
在使用完 ResultSet 对象之后,需要及时关闭它,有正确关闭它们的话,可能会导致数据库连接过多,从而影响系统的稳定性和性能。
通过列索引获取数据(以String类型获取) 代码:jdbcTest03’
执行insert delete update语句的时候,调用Statement接口的executeUpdate()方法。 执行select语句的时候,调用Statement接口的executeQuery()方法 。执行select语句后返回结果集对象:ResultSet。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 String sql = "select realname,id,name,password from t_user" ; rs = stmt.executeQuery(sql); while (rs.next()){ String s1 = rs.getString(1 ); String s2 = rs.getString(2 ); String s3 = rs.getString(3 ); String s4 = rs.getString(4 ); System.out.println(s1 + "\t" + s2 + "\t" + s3 + "\t" + s4); }
rs.next() 将游标移动到下一行,如果移动后指向的这一行有数据则返回true,没有数据则返回false。
while循环体当中的代码是处理当前游标指向的这一行的数据。(注意:是处理的一行数据)
rs.getString(int columnIndex) 其中 int columnIndex 是查询结果的列下标,列下标从1开始,以1递增。
rs.getString(…) 方法在执行时,不管底层数据库中的数据类型是什么,统一以字符串String类型来获取。
ResultSet最终也是需要关闭的。先关闭ResultSet,再关闭Statement,最后关闭Connection 。
通过列名获取数据(以String类型获取) 获取当前行的数据,不仅可以通过列下标获取,还可以通过查询结果的列名来获取,通常这种方式是被推荐的,因为可读性好。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 String sql3 = "select empno a, ename b, sal c from emp" ; rs = stmt.executeQuery(sql3); while (rs.next()){ int empno = rs.getInt("a" ); String ename = rs.getString("b" ); String sal = rs.getString("c" ); System.out.println(empno + "," + ename + "," + sal); }
注意:是根据查询结果的列名,而不是表中的列名。以上查询的时候将字段起别名abc了,所以要根据abc来获取,而不能再根据以前的来获取了。
以指定的类型获取数据 前面的程序可以看到,不管数据库表中是什么数据类型,都以String类型返回。当然,也能以指定类型返回。
使用PowerDesigner再设计一张商品表:t_product,使用Navicat for MySQL工具准备数据如下:
id以long类型获取,name以String类型获取,price以double类型获取,create_time以java.sql.Date类型获取,代码如下:
createTime.getTime()是获取到毫秒级
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 String sql = "select id,name,price,create_time from t_product" ; rs = stmt.executeQuery(sql); SimpleDateFormat sdf = new SimpleDateFormat ("yyyy年MM月dd日" ); while (rs.next()){ long id = rs.getLong("id" ); String name = rs.getString("name" ); double price = rs.getDouble("price" ); Date createTime = rs.getDate("create_time" ); System.out.println(id + "," + name + "," + price + "," + createTime); java.util.Date date = new java .util.Date(createTime.getTime()); String strDate = sdf.format(date); System.out.println(strDate); }
获取结果集的元数据信息(了解)
这组数据整个叫ResultSet对象,而id的那一行数据叫做结果集元数据对象ResultSetMetaData ResultSetMetaData 是一个接口,用于描述 ResultSet 中的元数据信息,即查询结果集的结构信息,例如查询结果集中包含了哪些列,每个列的数据类型、长度、标识符等。
ResultSetMetaData 可以通过 ResultSet 接口的 getMetaData() 方法获取,一般在对 ResultSet 进行元数据信息处理时使用。
ResultSetMetaData rsmd = rs.getMetaData();//返回的是ResultSetMetaData
rsmd.getColumnCount();//获取列的数量
rsmd.getColumnName(i);// 获取列的名字
rsmd.getColumnTypeName(i); // 获取列的数据类型
rsmd.getColumnDisplaySize(i); // 获取列的长度
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 String sql = "select * from t_product" ; rs = stmt.executeQuery(sql); ResultSetMetaData rsmd = rs.getMetaData(); int columnCount = rsmd.getColumnCount(); System.out.println("共" + columnCount + "列" ); for (int i = 1 ; i < columnCount; i++) { String columnName = rsmd.getColumnName(i); System.out.println("第" + i + "列的列名" + columnName); String columnTypeName = rsmd.getColumnTypeName(i); System.out.println("第" + i + "列的类型是" + columnTypeName); int columnDisplaySize = rsmd.getColumnDisplaySize(i); System.out.println("第" + i + "列的长度" + columnDisplaySize); }
在上面的代码中,我们首先创建了一个 Statement 对象,然后执行了一条 SQL 查询语句,生成了一个 ResultSet 对象。接下来,我们通过 ResultSet 对象的 getMetaData() 方法获取了 ResultSetMetaData 对象,进而获取了查询结果中列的信息并进行输出。需要注意的是,在进行列信息的获取时,列的编号从 1 开始计算。该示例代码将获取查询结果集中所有列名、数据类型以及长度等信息。
获取新增行的主键值 有很多表的主键字段值都是自增的,在某些特殊的业务环境下,当我们插入了新数据后,希望能够获取到这条新数据的主键值,应该如何获取呢? 在 JDBC 中,如果要获取插入数据后的主键值,可以使用 Statement 接口的 executeUpdate() 方法的重载版本,该方法接受一个额外的参数,用于指定是否需要获取自动生成的主键值。然后,通过以下两个步骤获取插入数据后的主键值:
在执行 executeUpdate() 方法时指定一个标志位,表示需要返回插入的主键值。
调用 Statement 对象的 getGeneratedKeys() 方法,返回一个包含插入的主键值的 ResultSet 对象。
1 2 3 4 5 6 7 8 9 10 11 12 13 String sql = "insert into t_product(name,price,create_time) values('小米su7', 1.0, '2024-02-23')" ; int count = stmt.executeUpdate(sql, Statement.RETURN_GENERATED_KEYS); System.out.println("插入了" + count + "条记录" ); rs = stmt.getGeneratedKeys(); if (rs.next()){ long id = rs.getLong(1 ); System.out.println("新增行的主键值:" + id); }
使用IDEA工具类(DbUtils)编写JDBC程序 工具类的构造方法一般都是私有化的,因为工具类中的一般都是静态的,工具类就是为了方便编程,所以工具类中的方法都是直接采用“类名.”,的方式访问,因此不需要new对象。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 package com.powernode.jdbc.utils;import java.sql.*;import java.util.ResourceBundle;public class DbUtils { private DbUtils () {} private static String driver; private static String url; private static String user; private static String password; static { ResourceBundle bundle = ResourceBundle.getBundle("com.powernode.jdbc.jdbc01" ); driver = bundle.getString("driver" ); url = bundle.getString("url" ); user = bundle.getString("user" ); password = bundle.getString("password" ); try { Class.forName(driver); } catch (ClassNotFoundException e) { throw new RuntimeException (e); } } public static Connection getConnection () throws SQLException { Connection conn = DriverManager.getConnection(url, user, password); return conn; } public static void close (Connection conn, Statement stmt, ResultSet rs) { if (rs != null ) { try { rs.close(); } catch (SQLException e) { throw new RuntimeException (e); } } if (stmt != null ) { try { stmt.close(); } catch (SQLException e) { throw new RuntimeException (e); } } if (conn != null ) { try { conn.close(); } catch (SQLException e) { throw new RuntimeException (e); } } } }
第04章 SQL注入 SQL注入问题 SQL注入问题说的是:用户输入的信息中含有SQL语句关键字,和程序中的SQL语句进行字符串拼接,导致程序中的SQL语句改变了原意。(SQL注入问题是一种系统安全问题)
业务描述:系统启动后,给出登录页面,用户可以输入用户名和密码,用户名和密码全部正确,则登录成功,反之,则登录失败。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 public class JDBCTest08 { public static void main (String[] args) { Scanner scanner = new Scanner (System.in); System.out.println("欢迎使用用户管理系统,请登录!" ); System.out.println("用户名:" ); String loginName = scanner.next(); System.out.println("密码:" ); String loginPwd = scanner.next(); Connection conn = null ; Statement stmt = null ; ResultSet rs = null ; boolean loginSuccess = false ; String realname = null ; try { conn = DbUtils.getConnection(); stmt = conn.createStatement(); String sql = "select * from t_user where name = '" +loginName+"' and password = '" +loginPwd+"'" ; System.out.println(sql); rs = stmt.executeQuery(sql); if (rs.next()){ loginSuccess = true ; realname = rs.getString("realname" ); } } catch (SQLException e) { throw new RuntimeException (e); } finally { DbUtils.close(conn, stmt, rs); } System.out.println(loginSuccess ? "登录成功,欢迎" + realname : "登录失败,您的用户名不存在或者密码错误!" ); } }
然后把String loginName = scanner.next();改为String loginName = scanner.nextLine();每次读一行
用户提供的信息中如果还有sql语句的关键字,作为字符串的形式和java程序中的sql语句进行了字符串的拼接,可能会导致原sql语句的含义被扭曲
比如输入以下这样的信息
就会输出登录成功,欢迎张三,sql语句是select * from t_user where name = ‘fdsa’ and password = ‘gfds’ or ‘a’ = ‘a’
这种现象就叫做:SQL注入。导致这个现象的根本原因是:先进行了sql语句的字符串拼接,然后再进行sql语句的编译。
解决SQL注入问题 最根本的原因是:Statement造成的。
Statement执行原理是:先进行字符串的拼接,将拼接好的SQL语句发送给数据库服务器,数据库服务器进行SQL语句的编译,然后执行。因此用户提供的信息中如果含有SQL语句的关键字,那么这些关键字正好参加了SQL语句的编译,所以导致原SQL语句被扭曲。
因此,JDBC为了解决这个问题,引入了一个新的接口:PreparedStatement,我们称为:预编译的数据库操作对象。PreparedStatement是Statement接口的子接口。它俩是继承关系。
PreparedStatement执行原理是:先对SQL语句进行预先的编译,然后再向SQL语句指定的位置传值,也就是说:用户提供的信息中即使含有SQL语句的关键字,那么这个信息也只会被当做一个值传递给SQL语句,用户提供的信息不再参与SQL语句的编译了,这样就解决了SQL注入问题。
1 2 3 4 5 6 7 8 9 10 11 12 这个程序在实现了基本的登录功能的同时,演示了SQL注入的现象。 导致SQL注入现象的根本原因是什么? 第一:用户提供的信息中含有SQL语句的关键字了。 第二:这些用户提供的信息中的关键字参与了SQL语句的编译。 SQL注入是一种安全隐患。黑客通常使用SQL注入来攻击你的系统。 怎么避免SQL注入现象呢? java.sql.Statement 是存在SQL注入现象的,因为它的原理是:先拼接SQL语句字符串,然后再进行编译,所以它必然存在SQL注入的问题。 为了避免SQL注入的发生,JDBC API中为Statement接口提供了一个子接口:java.sql.PreparedStatement,被称为预编译的数据库操作对象。 java.sql.PreparedStatement可以解决SQL注入问题。 具体怎么解决的?PreparedStatement可以对SQL语句进行预先编译,然后给编译好的SQL语句占位符传值,通过这种方式来解决SQL注入问题。 最本质的解决方法是:用户虽然提供了SQL语句关键字,但是这些关键字不再参与SQL语句的编译了。因此解决了SQL注入的问题。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 Connection conn = null ;PreparedStatement ps = null ;ResultSet rs = null ;boolean loginSuccess = false ;String realname = null ;try { conn = DbUtils.getConnection(); String sql = "select * from t_user where name = ? and password = ?" ; ps = conn.prepareStatement(sql); ps.setString(1 , loginName); ps.setString(2 , loginPwd); rs = ps.executeQuery(); if (rs.next()){ loginSuccess = true ; realname = rs.getString("realname" ); }
PreparedStatement原理:
第一步:先对SQL语句进行预先编译
第二步:给SQL语句中占位符传值
解决SQL注入的本质是:先将带有占位符的SQL语句进行预先编译,然后给占位符传值。
即使用户提供的信息中含有SQL语句关键字,但是这些关键字不会参与SQL语句的编译,自然不会扭曲SQL语句的原意。
关于使用PreparedStatement要注意的是:
带有占位符 ? 的SQL语句我们称为:预处理SQL语句。
占位符 ? 不能使用单引号或双引号包裹。如果包裹,占位符则不再是占位符,是一个普通的问号字符。
在执行SQL语句前,必须给每一个占位符 ? 传值。
如何给占位符 ? 传值,通过以下的方法:
pstmt.setXxx(第几个占位符, 传什么值)
“第几个占位符”:从1开始。第1个占位符则是1,第2个占位符则是2,以此类推。
“传什么值”:具体要看调用的什么方法?
如果调用pstmt.setString方法,则传的值必须是一个字符串。
如果调用pstmt.setInt方法,则传的值必须是一个整数。
以此类推……
PreparedStatement和Statement都是用于执行SQL语句的接口,它们的主要区别在于:
Statement:是先拼接字符串然后再编译,这里的查询语句的变量会发生变化,发生变化会导致sql语句跟之前的不一样,就需要重新编译,所以效率会低一点点,而PreparedStatement是编译1次执行n次,都是在ps = conn.prepareStatement(sql);这里执行
PreparedStatement预编译SQL语句,Statement直接提交SQL语句;
PreparedStatement执行速度更快,可以避免SQL注入攻击;(PreparedStatement对于同一条SQL语句来说,编译一次,执行N次。而Statement是每次都要进行编译的。因此PreparedStatement效率略微高一些。)
PreparedStatement会做类型检查,是类型安全的;
PreparedStatement的使用 新增操作 需求:向 emp 表中插入这样一条记录: empno:8888 ename:张三 job:销售员 mgr:7369 hiredate:2024-01-01 sal:1000.0 comm:500.0 deptno:10
怎么把字符串转换成需要的sql date? LocalDate localDate = LocalDate.parse(“2024-01-01”);//parse是解析,将其转换成LocalDate这个类型 ps.setDate(5, java.sql.Date.valueOf(localDate));//java.sql.Date的valueOf里面传入一个LocalDate就可以获取了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 Connection conn = null ; PreparedStatement ps = null ; try { conn = DbUtils.getConnection(); String sql = "insert into emp(empno,ename,job,mgr,hiredate,sal,comm,deptno) values(?,?,?,?,?,?,?,?)" ; ps = conn.prepareStatement(sql); ps.setInt(1 , 8888 ); ps.setString(2 , "张三" ); ps.setString(3 , "销售员" ); ps.setInt(4 , 7369 ); LocalDate localDate = LocalDate.parse("2024-01-01" ); ps.setDate(5 , java.sql.Date.valueOf(localDate)); ps.setDouble(6 , 1000.0 ); ps.setDouble(7 , 500.0 ); ps.setInt(8 , 10 ); int count = ps.executeUpdate(); System.out.println("插入了" + count + "条记录" );
修改操作 需求:将员工编号为8888的员工,姓名修改为李四,岗位修改为产品经理,月薪修改为5000.0,其他不变。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 Connection conn = null ; PreparedStatement ps = null ; try { conn = DbUtils.getConnection(); String sql = "update emp set ename=?, job=?, sal=? where empno=?" ; ps = conn.prepareStatement(sql); ps.setString(1 , "李四" ); ps.setString(2 , "产品经理" ); ps.setDouble(3 , 5000.0 ); ps.setInt(4 , 8888 ); int count = ps.executeUpdate(); System.out.println("更新了" + count + "条数据" ); } catch (SQLException e) { throw new RuntimeException (e); } finally { DbUtils.close(conn,ps,null );
删除操作 需求:将员工编号为8888的删除。
1 2 3 4 5 6 7 8 9 10 11 12 13 String sql = "delete from emp where empno = ?" ; try (Connection conn = DbUtils.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setInt(1 , 8888 ); int count = ps.executeUpdate(); System.out.println("删除了" + count + "条记录" ); } catch (SQLException e) { throw new RuntimeException (e);
模糊查询 需求:查询员工名字中第二个字母是 O 的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 Connection conn = null ; PreparedStatement ps = null ; ResultSet rs = null ; try { conn = DbUtils.getConnection(); String sql = "select ename from emp where ename like ?" ; ps = conn.prepareStatement(sql); ps.setString(1 , "_O%" ); rs = ps.executeQuery(); while (rs.next()){ String ename = rs.getString("ename" ); System.out.println(ename); }
分页查询 对于MySQL来说,通用的分页SQL语句: 假设每页显示3条记录:pageSize = 3 第1页:limit 0, 3 第2页:limit 3, 3 第3页:limit 6, 3 *第pageNo页:limit (pageNo - 1)pageSize, pageSize 需求:查询所有员工姓名,每页显示3条(pageSize),显示第2页(pageNo)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 int pageSize = 3 ; int pageNo = 5 ; Connection conn = null ; PreparedStatement ps = null ; ResultSet rs = null ; try { conn = DbUtils.getConnection(); String sql = "select ename from emp limit ?,?" ; ps = conn.prepareStatement(sql); ps.setInt(1 , (pageNo - 1 ) * pageSize); ps.setInt(2 , pageSize); rs = ps.executeQuery(); while (rs.next()){ String ename = rs.getString("ename" ); System.out.println(ename); }
blob数据的插入和读取 blob类型的全称是binary large object,二进制大对象,用来接受图片这种,底层运行原理是这样的,用io流从硬盘读取到内存,然后再输出到数据库里,但是executeUpdate()底层会自动输出,所以这个输出流是看不见的。
准备一张表:t_img,三个字段,一个id主键,一个图片名字name,一个img。 建表语句如下:
1 2 3 4 5 create table `t_img` ( `id` bigint primary key auto_increment, `name` varchar (255 ), `img` blob ) engine= innodb;
需求1:向t_img 表中插入一张图片。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 Connection conn = null ; PreparedStatement ps = null ; FileInputStream in = null ; try { conn = DbUtils.getConnection(); String sql = "insert into t_img(name, img) values(?, ?)" ; ps = conn.prepareStatement(sql); ps.setString(1 , "狗狗的图片" ); in = new FileInputStream ("F:\\动力节点全套资料\\07-JDBC教程,jdbc从零到精通,动力节点JDBC,老杜jdbc\\resources\\dog.jpg" ); ps.setBlob(2 , in); int count = ps.executeUpdate(); System.out.println("插入了" + count + "条记录" ); } catch (Exception e) { throw new RuntimeException (e); }finally { if (in != null ){ try { in.close(); } catch (IOException e) { throw new RuntimeException (e); } } DbUtils.close(conn, ps, null ); }
需求2:从t_img 表中读取一张图片。(从数据库中读取一张图片保存到本地。)
需要输入流是因为要从数据库读取到内存里,然后再从内存输出到电脑硬盘上
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 Connection conn = null ; PreparedStatement ps = null ; ResultSet rs = null ; try { conn = DbUtils.getConnection(); String sql = "select img from t_img where name = ?" ; ps = conn.prepareStatement(sql); ps.setString(1 , "狗狗的图片" ); rs = ps.executeQuery(); if (rs.next()){ InputStream in = rs.getBinaryStream("img" ); OutputStream out = new FileOutputStream ("E:/dog.jpg" ); byte [] bytes = new byte [1024 ]; int readCount = 0 ; while ((readCount = in.read(bytes)) != -1 ){ out.write(bytes, 0 , readCount); } out.flush(); in.close(); out.close(); } } catch (Exception e) { throw new RuntimeException (e); }finally { DbUtils.close(conn, ps, rs); }
JDBC批处理(插入)操作 准备一张商品表:t_product 建表语句如下:
1 2 3 4 create table t_product( id bigint primary key , name varchar (255 ) );
不使用批处理 不使用批处理,向 t_product 表中插入一万条商品信息,并记录耗时!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 long begin = System.currentTimeMillis(); Connection conn = null ; PreparedStatement ps = null ; try { conn = DbUtils.getConnection(); String sql = "insert into t_batch(id,name) values(?,?)" ; ps = conn.prepareStatement(sql); int count = 0 ; for (int i = 1 ; i < 10000 ; i++) { ps.setLong(1 , i); ps.setString(2 , "batch" + i); count += ps.executeUpdate(); } System.out.println("插入了" + count + "条记录" ); } catch (SQLException e) { throw new RuntimeException (e); }finally { DbUtils.close(conn, ps, null ); } long end = System.currentTimeMillis(); System.out.println("总耗时" + (end - begin) + "毫秒" );
使用批处理 使用批处理,向 t_product 表中插入一万条商品信息,并记录耗时!注意:启用批处理需要在URL后面添加这个的参数:rewriteBatchedStatements=true
ps.addBatch();// 打包 executeBatch会批量执行处理一次,返回值是个int数组 executeBatch().length返回的是数组的长度
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 long begin = System.currentTimeMillis(); Connection conn = null ; PreparedStatement ps = null ; try { conn = DbUtils.getConnection(); String sql = "insert into t_batch(id,name) values(?,?)" ; ps = conn.prepareStatement(sql); int count = 0 ; for (int i = 1 ; i < 10000 ; i++) { ps.setLong(1 , i); ps.setString(2 , "batch" + i); ps.addBatch(); if (i % 500 == 0 ){ count += ps.executeBatch().length; } } count += ps.executeBatch().length; System.out.println("插入了" + count + "条记录" ); } catch (SQLException e) { throw new RuntimeException (e); }finally { DbUtils.close(conn, ps, null ); } long end = System.currentTimeMillis(); System.out.println("总耗时" + (end - begin) + "毫秒" );
插入了9999条记录 总耗时3617毫秒
在进行大数据量插入时,批处理为什么可以提高程序的执行效率?
减少了网络通信次数:JDBC 批处理会将多个 SQL 语句一次性发送给服务器,减少了客户端和服务器之间的通信次数,从而提高了数据写入的速度,特别是对于远程服务器而言,优化效果更为显著。
减少了数据库操作次数:JDBC 批处理会将多个 SQL 语句合并成一条 SQL 语句进行执行,从而减少了数据库操作的次数,减轻了数据库的负担,大大提高了数据写入的速度。
第05章 JDBC事务 什么是事务 事务是一个完整的业务,在这个业务中需要多条DML语句共同联合才能完成,而事务可以保证多条DML语句同时成功或者同时失败,从而保证数据的安全。例如A账户向B账户转账一万,A账户减去一万(update)和B账户加上一万(update),必须同时成功或者同时失败,才能保证数据是正确的。
另请参见老杜发布的2024版MySQL教学视频。在本套教程中详细讲解了数据库事务机制。
使用转账案例演示事务 表和数据的准备 t_act表
实现转账功能 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 public class JDBCTest19 { public static void main (String[] args) { double money = 10000.0 ; Connection conn = null ; PreparedStatement ps1 = null ; PreparedStatement ps2 = null ; try { conn = DbUtils.getConnection(); String sql1 = "update t_act set balance = balance - ? where actno = ?" ; ps1 = conn.prepareStatement(sql1); ps1.setDouble(1 , money); ps1.setString(2 , "act-001" ); int count1= ps1.executeUpdate(); String sql2 = "update t_act set balance = balance + ? where actno = ?" ; ps2 = conn.prepareStatement(sql2); ps2.setDouble(1 , money); ps2.setString(2 , "act-002" ); int count2 = ps2.executeUpdate(); } catch (SQLException e) { throw new RuntimeException (e); }finally { DbUtils.close(null , ps1, null ); DbUtils.close(null , ps2, null ); DbUtils.close(conn, null , null ); } } }
JDBC事务默认是自动提交的 DBC事务默认情况下是自动提交的,所谓的自动提交是指:只要执行一条DML语句则自动提交一次。测试一下,在以下代码位置添加断点:
让代码执行到断点处看看数据库表中的数据是否发生变化:
因为JDBC事务默认情况下是自动提交,只要执行一条DML语句则自动提交一次。这种自动提交是极其危险的。如果在此时程序发生了异常,act-002账户的余额未成功更新,则钱会丢失一万。
添加事务控制 如何解决以上问题,分三步: 第一步:将JDBC事务的自动提交机制修改为手动提交(即开启事务)
1 conn.setAutoCommit(false );
第二步:当整个业务完整结束后,手动提交事务(即提交事务,事务结束)
第三步:在处理业务过程中,如果发生异常,则进入catch语句块进行异常处理,手动回滚事务(即回滚事务,失败的事务提交后结束)
代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 try { conn = DbUtils.getConnection(); conn.setAutoCommit(false ); String sql1 = "update t_act set balance = balance - ? where actno = ?" ; ps1 = conn.prepareStatement(sql1); ps1.setDouble(1 , money); ps1.setString(2 , "act-001" ); int count1= ps1.executeUpdate(); String sql2 = "update t_act set balance = balance + ? where actno = ?" ; ps2 = conn.prepareStatement(sql2); ps2.setDouble(1 , money); ps2.setString(2 , "act-002" ); int count2 = ps2.executeUpdate(); conn.commit(); } catch (Exception e) { try { conn.rollback(); } catch (SQLException ex) { throw new RuntimeException (ex); } throw new RuntimeException (e); }finally { DbUtils.close(null , ps1, null ); DbUtils.close(null , ps2, null ); DbUtils.close(conn, null , null ); }
设置JDBC事务隔离级别 关于事务隔离级别相关内容另请参见:老杜发布的2024版MySQL教程
设置事务的隔离级别也是比较重要的,在JDBC程序中应该如何设置事务的隔离级别呢
1 conn.setTransactionIsolation
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public class JDBCTest20 { public static void main (String[] args) { Connection conn = null ; try { conn = DbUtils.getConnection(); conn.setTransactionIsolation(Connection.TRANSACTION_REPEATABLE_READ); } catch (SQLException e) { throw new RuntimeException (e); } finally { DbUtils.close(conn, null , null ); } } }
第06章 JDBC调用存储过程 在MySQL中创建存储过程 这是一个计算0-100之间所有偶数和的sql语句
1 2 3 4 5 6 7 8 9 10 11 create procedure mypro(in n int , out sum int )/ / 一个入参一个出参begin set sum := 0 ; repeat if n % 2 = 0 then set sum := sum + n; end if; set n := n - 1 ; until n <= 0 end repeat; end ;
使用JDBC代码调用存储过程 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 public class JDBCTest21 { public static void main (String[] args) { Connection conn = null ; CallableStatement cs = null ; try { conn = DbUtils.getConnection(); String sql = "{ call mypro(?,?) }" ; cs = conn.prepareCall(sql); cs.setInt(1 , 100 ); cs.registerOutParameter(2 , Types.INTEGER); cs.execute(); int result = cs.getInt(2 ); System.out.println(result); } catch (SQLException e) { throw new RuntimeException (e); }finally { DbUtils.close(conn, cs, null ); } } }
第07章 JDBC实现员工管理 数据库表的准备 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 package com.powernode.jdbc;import com.powernode.jdbc.utils.DbUtils;import java.sql.*;import java.util.Scanner;public class JDBCTest22 { public static void main (String[] args) { System.out.println("欢迎使用员工信息管理,请认真阅读使用说明:" ); System.out.println("本系统的功能主要包括:查看员工列表、查看某个员工详细信息、新增员工、修改员工、删除员工" ); System.out.println("请输入对应的功能编号选择功能:" ); System.out.println("[1]查看员工列表" ); System.out.println("[2]查看某个员工详细信息" ); System.out.println("[3]新增员工" ); System.out.println("[4]修改员工" ); System.out.println("[5]删除员工" ); System.out.println("[0]退出系统" ); Scanner scanner = new Scanner (System.in); while (true ){ System.out.println("请输入功能编号:" ); int no = scanner.nextInt(); if (1 == no){ System.out.println("员工信息列表如下:" ); doList(); } else if (2 == no){ doList(); System.out.print("请输入员工的id:" ); long id = scanner.nextLong(); System.out.println("员工[" + id + "]的详细信息如下:" ); doDetail(id); } else if (3 == no){ System.out.print("请输入员工姓名:" ); String name = scanner.next(); System.out.print("请输入员工岗位:" ); String job = scanner.next(); System.out.print("请输入员工月薪:" ); Double salary = scanner.nextDouble(); System.out.print("请输入员工入职日期:" ); String hiredate = scanner.next(); System.out.print("请输入员工住址:" ); String address = scanner.next(); doSave(name, job, salary, hiredate, address); } else if (4 == no){ doList(); System.out.print("请输入您要修改的员工id:" ); long id = scanner.nextLong(); doDetail(id); System.out.print("请输入员工姓名:" ); String name = scanner.next(); System.out.print("请输入员工岗位:" ); String job = scanner.next(); System.out.print("请输入员工月薪:" ); Double salary = scanner.nextDouble(); System.out.print("请输入员工入职日期:" ); String hiredate = scanner.next(); System.out.print("请输入员工住址:" ); String address = scanner.next(); doModify(id, name, job, salary, hiredate, address); System.out.println("员工[" + id + "]的信息更新成功!!!!" ); } else if (5 == no){ doList(); System.out.print("请输入要删除的员工id:" ); Long id = scanner.nextLong(); doDel(id); System.out.println("删除员工[" + id + "]成功了!" ); doList(); } else if (0 == no){ System.out.println("下次再见!" ); System.exit(0 ); } else { System.out.println("对不起,您输入的功能暂不支持!" ); } } } private static void doDel (Long id) { Connection conn = null ; PreparedStatement ps = null ; try { conn = DbUtils.getConnection(); String sql = "delete from t_employee where id = ?" ; ps = conn.prepareStatement(sql); ps.setLong(1 , id); int count = ps.executeUpdate(); } catch (SQLException e) { throw new RuntimeException (e); } finally { DbUtils.close(conn, ps, null ); } } private static void doModify (long id, String name, String job, Double salary, String hiredate, String address) { Connection conn = null ; PreparedStatement ps = null ; try { conn = DbUtils.getConnection(); String sql = "update t_employee set name=?, job=?, salary=?, hiredate=?, address=? where id=?" ; ps = conn.prepareStatement(sql); ps.setString(1 , name); ps.setString(2 , job); ps.setDouble(3 , salary); ps.setString(4 , hiredate); ps.setString(5 , address); ps.setLong(6 , id); int count = ps.executeUpdate(); } catch (SQLException e) { throw new RuntimeException (e); }finally { DbUtils.close(conn, ps, null ); } } private static void doSave (String name, String job, Double salary, String hiredate, String address) { Connection conn = null ; PreparedStatement ps = null ; try { conn = DbUtils.getConnection(); String sql = "insert into t_employee(name,job,salary,hiredate,address) values(?,?,?,?,?)" ; ps = conn.prepareStatement(sql); ps.setString(1 , name); ps.setString(2 , job); ps.setDouble(3 , salary); ps.setString(4 , hiredate); ps.setString(5 , address); int count = ps.executeUpdate(); } catch (SQLException e) { throw new RuntimeException (e); }finally { DbUtils.close(conn, ps, null ); } } private static void doDetail (Long id) { Connection conn = null ; PreparedStatement ps = null ; ResultSet rs = null ; try { conn = DbUtils.getConnection(); String sql = "select * from t_employee where id = ?" ; ps = conn.prepareStatement(sql); ps.setLong(1 , id); rs = ps.executeQuery(); if (rs.next()){ String name = rs.getString("name" ); String job = rs.getString("job" ); String hiredate = rs.getString("hiredate" ); Double salary = rs.getDouble("salary" ); String address = rs.getString("address" ); System.out.println("id = " + id); System.out.println("姓名 = " + name); System.out.println("岗位 = " + job); System.out.println("入职日期 = " + hiredate); System.out.println("月薪 = " + salary); System.out.println("住址 = " + address); } } catch (SQLException e) { throw new RuntimeException (e); }finally { DbUtils.close(conn, ps, rs); } } private static void doList () { Connection conn = null ; PreparedStatement ps = null ; ResultSet rs = null ; try { conn = DbUtils.getConnection(); String sql = "select id,name,job from t_employee" ; ps = conn.prepareStatement(sql); rs = ps.executeQuery(); while (rs.next()){ Long id = rs.getLong("id" ); String name = rs.getString("name" ); String job = rs.getString("job" ); System.out.println(id + "\t" + name + "\t" + job); } } catch (SQLException e) { throw new RuntimeException (e); }finally { DbUtils.close(conn, ps, rs); } } }
第08章 DAO 什么是DAO DAO是:Data Access Object,翻译为:数据访问对象。 一种JavaEE的设计模式,专门用来做数据增删改查的类。 在实际的开发中,通常我们会将数据库的操作封装为一个单独的DAO去完成,这样做的目的是:提高代码的复用性,另外也可以降低程序的耦合度,提高扩展力。 例如:操作用户数据的叫做UserDao,操作员工数据的叫做EmployeeDao,操作产品数据的叫做ProductDao,操作订单数据的叫做OrderDao等。
使用DAO改造员工信息管理 定义Employee封装数据 Employee类是一个Java Bean,这个类被称为bean,或者pojo类,专门用来封装员工的信息:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 public class DbUtils { private DbUtils () {} private static String driver; private static String url; private static String user; private static String password; static { ResourceBundle bundle = ResourceBundle.getBundle("com.powernode.jdbc.jdbc01" ); driver = bundle.getString("driver" ); url = bundle.getString("url" ); user = bundle.getString("user" ); password = bundle.getString("password" ); try { Class.forName(driver); } catch (ClassNotFoundException e) { throw new RuntimeException (e); } } public static Connection getConnection () throws SQLException { Connection conn = DriverManager.getConnection(url, user, password); return conn; } public static void close (Connection conn, Statement stmt, ResultSet rs) { if (rs != null ) { try { rs.close(); } catch (SQLException e) { throw new RuntimeException (e); } } if (stmt != null ) { try { stmt.close(); } catch (SQLException e) { throw new RuntimeException (e); } } if (conn != null ) { try { conn.close(); } catch (SQLException e) { throw new RuntimeException (e); } } } }
定义EmployeeDao 定义五个方法,分别完成五个功能:新增,修改,删除,查看一个,查看所有。增删改查也被简称为:CRUD。C:Create 增,R:Read 查,U:Update 改,D:Delete 删
注意:
DAO不负责任何业务逻辑的处理。只负责CRUD操作。
DAO是JavaEE的设计模式之一。
DAO中方法名起名也有讲究,一般都是以:insert delete update select开头。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 public class EmployeeDao { public int insert (Employee employee) { Connection conn = null ; PreparedStatement ps = null ; int count = 0 ; try { conn = DbUtils.getConnection(); String sql = "insert into t_employee(name,job,salary,hiredate,address) values(?,?,?,?,?)" ; ps = conn.prepareStatement(sql); ps.setString(1 , employee.getName()); ps.setString(2 , employee.getJob()); ps.setDouble(3 , employee.getSalary()); ps.setString(4 , employee.getHiredate()); ps.setString(5 , employee.getAddress()); count = ps.executeUpdate(); } catch (SQLException e) { throw new RuntimeException (e); } return count; } public int deleteByid (Long id) { Connection conn = null ; PreparedStatement ps = null ; int count = 0 ; try { conn = DbUtils.getConnection(); String sql = "delete from t_employee where id = ?" ; ps = conn.prepareStatement(sql); ps.setLong(1 , id); count = ps.executeUpdate(); } catch (SQLException e) { throw new RuntimeException (e); } finally { DbUtils.close(conn, ps, null ); } return count; } public int update (Employee newEmployee) { Connection conn = null ; PreparedStatement ps = null ; int count = 0 ; try { conn = DbUtils.getConnection(); String sql = "update t_employee set name=?, job=?, salary=?, hiredate=?, address=? where id=?" ; ps = conn.prepareStatement(sql); ps.setString(1 , newEmployee.getName()); ps.setString(2 , newEmployee.getJob()); ps.setDouble(3 , newEmployee.getSalary()); ps.setString(4 , newEmployee.getHiredate()); ps.setString(5 , newEmployee.getAddress()); ps.setLong(6 , newEmployee.getId()); count = ps.executeUpdate(); } catch (SQLException e) { throw new RuntimeException (e); } finally { DbUtils.close(conn, ps, null ); } return count; } public Employee selectById (Long id) { Connection conn = null ; PreparedStatement ps = null ; ResultSet rs = null ; Employee employee = null ; try { conn = DbUtils.getConnection(); String sql = "select name,job,salary,hiredate,address from t_employee where id = ?" ; ps = conn.prepareStatement(sql); ps.setLong(1 , id); rs = ps.executeQuery(); if (rs.next()){ employee = new Employee (); employee.setId(id); employee.setName(rs.getString("name" )); employee.setJob(rs.getString("job" )); employee.setSalary(rs.getDouble("salary" )); employee.setHiredate(rs.getString("hiredate" )); employee.setAddress(rs.getString("address" )); } } catch (SQLException e) { throw new RuntimeException (e); } finally { DbUtils.close(conn, ps, rs); } return employee; } public List<Employee> selectAll () { Connection conn = null ; PreparedStatement ps = null ; ResultSet rs = null ; List<Employee> employeeList = new ArrayList <>(); try { conn = DbUtils.getConnection(); String sql = "select id,name,job,salary,hiredate,address from t_employee" ; ps = conn.prepareStatement(sql); rs = ps.executeQuery(); while (rs.next()){ Employee employee = new Employee (); employee.setId(rs.getLong("id" )); employee.setName(rs.getString("name" )); employee.setJob(rs.getString("job" )); employee.setSalary(rs.getDouble("salary" )); employee.setHiredate(rs.getString("hiredate" )); employee.setAddress(rs.getString("address" )); employeeList.add(employee); } } catch (SQLException e) { throw new RuntimeException (e); } finally { DbUtils.close(conn, ps, rs); } return employeeList; } }
使用dao优化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 private static EmployeeDao employeeDao = new EmployeeDao (); private static void doDel (Long id) { employeeDao.deleteByid(id); } private static void doModify (long id, String name, String job, Double salary, String hiredate, String address) { Employee employee = new Employee (id, name, job, salary, hiredate, address); employeeDao.update(employee); } private static void doSave (String name, String job, Double salary, String hiredate, String address) { Employee employee = new Employee (name, job, salary, hiredate, address); employeeDao.insert(employee); } private static void doDetail (Long id) { Employee employee = employeeDao.selectById(id); System.out.println("id = " + employee.getId()); System.out.println("姓名 = " + employee.getName()); System.out.println("岗位 = " + employee.getJob()); System.out.println("入职日期 = " + employee.getHiredate()); System.out.println("月薪 = " + employee.getSalary()); System.out.println("住址 = " + employee.getAddress()); } private static void doList () { List<Employee> employees = employeeDao.selectAll(); System.out.println("id\tname\tjob" ); System.out.println("----------------------------" ); for (Employee e : employees) { System.out.println(e.getId() + "\t" + e.getName() + "\t" + e.getJob()); } } }
BaseDao的封装 可以写一个basedao,然后让EmployeeDao继承basedao,调用里面的方法,方法一个是查询,一个是增删改
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 public int executeUpdate (String sql, Object... params) { Connection conn = null ; PreparedStatement ps = null ; int count = 0 ; try { conn = DbUtils.getConnection(); ps = conn.prepareStatement(sql); if (params != null && params.length > 0 ){ for (int i = 0 ; i < params.length; i++) { ps.setObject(i + 1 ,params[i]); } } count = ps.executeUpdate(); } catch (SQLException e) { throw new RuntimeException (e); }finally { DbUtils.close(conn, ps, null ); } return count; }
之前的EmployeeDao就改成
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 public class EmployeeDao extends BaseDao { public int insert (Employee employee) { String sql = "insert into t_employee(name,job,salary,hiredate,address) values(?,?,?,?,?)" ; return executeUpdate(sql, employee.getName(), employee.getJob(), employee.getSalary(), employee.getHiredate(), employee.getAddress()); } public int deleteByid (Long id) { String sql = "delete from t_employee where id = ?" ; return executeUpdate(sql, id); } public int update (Employee newEmployee) { String sql = "update t_employee set name=?, job=?, salary=?, hiredate=?, address=? where id=?" ; return executeUpdate(sql, newEmployee.getName(), newEmployee.getJob(), newEmployee.getSalary(), newEmployee.getHiredate(), newEmployee.getAddress(),newEmployee.getId()); } }
而查询里的数组是需要创建对象的,但这个对象每次传入的都不一样,比如userDao extends BaseDao,需要传入的就是List,这里就需要使用泛型了,而泛型获取对象是通过反射机制也就是class clazz,clazz.newInstance();
另外只返回一个也可以当做list集合对待 可变长参数必须是最后一个
第09章 连接池 连接池的理解 不使用连接池有啥问题 Connection对象是重量级对象,创建Connection对象就是建立两个进程之间的通信,非常耗费资源。一次完整的数据库操作,大部分时间都耗费在连接对象的创建。 第一个问题:每一次请求都创建一个Connection连接对象,效率较低。 第二个问题:连接对象的数量无法限制。如果连接对象的数量过高,会导致mysql数据库服务器崩溃。
使用连接池来解决什么问题 提前创建好N个连接对象,将其存放到一个集合中(这个集合就是一个缓存)。 用户请求时,需要连接对象直接从连接池中获取,不需要创建连接对象,因此效率较高。 另外,连接对象只能从连接池中获取,如果没有空闲的连接对象,只能等待,这样连接对象创建的数量就得到了控制。 从连接池拿到的conn对象也要调用close,但是并不是关闭连接,而是归还到连接池里并设置状态为null
javax.sql.DataSource 连接池有很多,不过所有的连接池都实现了 javax.sql.DataSource 接口。也就是说我们程序员在使用连接池的时候,不管使用哪家的连接池产品,只要面向javax.sql.DataSource接口调用方法即可。
另外,实际上我们也可以自定义属于我们自己的连接池。只要实现DataSource接口即可。
连接池也可以叫做数据源
连接池的属性 对于一个基本的连接池来说,一般都包含以下几个常见的属性:
初始化连接数(initialSize):连接池初始化时创建的连接数。
最大连接数(maxActive):连接池中最大的连接数,也就是连接池所能容纳的最大连接数量,当连接池中的连接数量达到此值时,后续请求会被阻塞并等待连接池中有连接被释放后再处理。
最小空闲连接数量(minIdle): 指连接池中最小的空闲连接数,也就是即使当前没有请求,连接池中至少也要保持一定数量的空闲连接,以便应对高并发请求或突发连接请求的情况。
最大空闲连接数量(maxIdle): 指连接池中最大的空闲连接数,也就是连接池中最多允许保持的空闲连接数量。当连接池中的空闲连接数量达到了maxIdle设定的值后,多余的空闲连接将会被连接池释放掉。
最大等待时间(maxWait):当连接池中的连接数量达到最大值时,后续请求需要等待的最大时间,如果超过这个时间,则会抛出异常。
连接有效性检查(testOnBorrow、testOnReturn):为了确保连接池中只有可用的连接,一些连接池会定期对连接进行有效性检查,这里的属性就是配置这些检查的选项。
连接的driver、url、user、password等。
以上这些属性是连接池中较为常见的一些属性,不同的连接池在实现时可能还会有其他的一些属性,不过大多数连接池都包含了以上几个属性,对于使用者来说需要根据自己的需要进行灵活配置。
常用的连接池 市面上常用的数据库连接池有许多,以下是其中几种:
DBCP(不用了)
2001年诞生,最早的连接池。
Apache Software Foundation的一个开源项目。
DBCP的设计初衷是为了满足Tomcat服务器对连接池管理的需求。
c3p0(不用了)
2004年诞生
c3p0是由Steve Waldman于2004年推出的,它是一个高性能、高可靠性、易配置的数据库连接池。c3p0能够提供连接池的容错能力、自动重连等功能,适用于高并发场景和数据量大的应用。
Druid(多功能豪华版)
2012年诞生
Druid连接池由阿里巴巴集团开发,于2011年底开始对外公开,2012年正式发布。Druid是一个具有高性能、高可靠性、丰富功能的数据库连接池,不仅可以做连接池,还能做监控、分析和管理数据库,支持SQL防火墙、统计分析、缓存和访问控制等功能。
HikariCP(专精的非豪华版
2012年诞生
HikariCP是由Brett Wooldridge于2012年创建的开源项目,它被认为是Java语言下最快的连接池之一,具有快速启动、低延迟、低资源消耗等优点。HikariCP连接池适用于高并发场景和云端应用。
很单纯的一个连接池,这个产品只做连接池应该做的,其他的不做。所以性能是极致的。相对于Druid来说,它更加轻量级。
Druid连接池在连接管理之外提供了更多的功能,例如SQL防火墙、统计分析、缓存、访问控制等,适用于在数据库访问过程中,需要进行细粒度控制的场景
HikariCP则更侧重于性能方面的优化,对各种数据库的兼容性也更好
BoneCP
2015年诞生
BoneCP是一款Java语言下的高性能连接池,于2015年由Dominik Gruntz在GitHub上发布。BoneCP具有分布式事务、连接空闲检查、SQL语句跟踪和性能分析、特定类型的连接池等特点。BoneCP连接池适用于大型应用系统和高并发的负载场景
连接池的使用 Druid的使用 第一步:引入Druid的jar包 第二步:配置文件 在类的根路径下创建一个属性资源文件:jdbc.properties,也可以在JDBC_study目录下建立一个文件夹叫resources,然后右击选择 等同于放在src根目录下面
1 2 3 4 5 6 7 url =jdbc:mysql://localhost:3306/jdbc username =root password =1234 driverClassName =com.mysql.cj.jdbc.Driver initialSize =5 minIdle =10 maxActive =20
第三步:编写代码,从连接池中获取连接对象
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 import com.alibaba.druid.pool.DruidDataSourceFactory;InputStream in = Thread.currentThread().getContextClassLoader().getResourceAsStream("jdbc.properties" );Properties prop = new Properties ();prop.load(in); DataSource dataSource = DruidDataSourceFactory.createDataSource(prop);Connection conn = dataSource.getConnection();System.out.println(conn); conn.close();
第四步:关闭连接 仍然调用Connection的close()方法,但是这个close()方法并不是真正的关闭连接,只是将连接归还到连接池,让其称为空闲连接对象。这样其他线程可以继续使用该空闲连接。
DbUtils的升级改造 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 public class DbUtils2 { private DbUtils2 () {} private static DataSource dataSource; static { try { InputStream in = Thread.currentThread().getContextClassLoader().getResourceAsStream("jdbc.properties" ); Properties prop = new Properties (); prop.load(in); dataSource = DruidDataSourceFactory.createDataSource(prop); } catch (Exception e) { throw new RuntimeException (e); } } public static Connection getConnection () throws SQLException { return dataSource.getConnection(); } public static void close (Connection conn, Statement stmt, ResultSet rs) { if (rs != null ) { try { rs.close(); } catch (SQLException e) { throw new RuntimeException (e); } } if (stmt != null ) { try { stmt.close(); } catch (SQLException e) { throw new RuntimeException (e); } } if (conn != null ) { try { conn.close(); } catch (SQLException e) { throw new RuntimeException (e); } } } }
HikariCP的使用 第一步:引入jar包
第二步:编写配置文件 在类的根路径下创建一个属性资源文件:jdbc2.properties
1 2 3 4 5 6 jdbcUrl =jdbc:mysql://localhost:3306/jdbc username =root password =1234 driverClassName =com.mysql.cj.jdbc.Driver minimumIdle =5 maximumPoolSize =20
第三步:编写代码,从连接池中获取连接
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public class JDBCTest26 { public static void main (String[] args) throws Exception{ InputStream in = Thread.currentThread().getContextClassLoader().getResourceAsStream("jdbc2.properties" ); Properties properties = new Properties (); properties.load(in); HikariConfig config = new HikariConfig (properties); DataSource dataSource = new HikariDataSource (config); Connection conn = dataSource.getConnection(); System.out.println(conn); conn.close(); } }
第四步:关闭连接(调用conn.close(),将连接归还到连接池,连接对象为空闲状态。)