本系列教程适用于JavaWeb初学者、爱好者,小白白。我们的天赋并不高,可贵在努力,坚持不放弃。坚信量最终引发质变,厚积薄发。
文中白话居多,尽量以小白视角呈现,帮助大家快速入门。
我是 蜗牛老师,之前网名是 Ongoing蜗牛,人如其名,干啥都慢,所以更新也慢。希望大家多多支持,让我动力十足!
本文主要讲述 JDBC 编程基本步骤,带领大家基本入门,通过 JDBC API 实现与数据库连接,并简单查询数据库表中的数据到 Java 中。但是总想把知识点的前前后后都说清楚,所以整体篇幅有点长。
大家还记得我们说过 Java EE 中有 JDBC API,我们一起再回顾一下。
Java 数据库连接 API
Java 数据库连接(JDBC) API 允许从Java编程语言方法调用 SQL 命令。当使用会话 Bean 访问数据库时,可以在企业 Bean 中使用 JDBC API。还可以使用来自 Servlet 或 JSP 页面的 JDBC API 直接访问数据库,而无需通过企业 Bean。JDBC API 有两个部分:
- 应用程序组件用来访问数据库的应用程序级接口
- 将 JDBC 驱动程序附加到 Java EE 平台的服务提供程序接口
Java EE 8 平台需要 JDBC 4.1。
Java EE 中的 JDBC API 其实是 Java SE 中集成库中的 API,JDBC API 由两个包组成:
java.sql
javax.sql
当我们下载 Java 平台标准版(Java SE) 时,将自动获得这两个包。
Java EE 8 平台需要 JDBC 4.1。那么 JDBC 4.1 其实是 Java SE 7 版本中增强的内容。
Java SE7 中 JDBC 4.1 引入了以下特性:
try-with-resources
语句自动关闭 Connection
、ResultSet
和statement
类型的资源的能力。RowSetFactory
接口和 RowSetProvider
类,它们能够创建 JDBC 驱动程序支持的所有类型的行集。Java SE8 中 JDBC 4.2 引入了以下特性:
REF_CURSOR
的支持。java.sql.DriverAction
接口。DriverManager
类中增加了对 deregisterDriver
方法的安全检查。java.sql.SQLType
接口。java.sql.JDBCType
枚举。大家也看到了,Java EE 8 使用的是 Java SE 7 中 JDBC 4.1,而 Java SE 8 中的 JDBC 版本是4.2,对于Java SE 9 中的 JDBC 版本是4.3,每个 JDBC 版本都有功能的增强,但是对于目前的我们来说,这些新增的特性没必要学习,我们需要是对 JDBC 有个基本入门即可。因为日后几乎不会使用原始的 JDBC 进行编程,它仅仅是我们学习持久层框架的基础而已,因为这些框架的底层就是 JDBC API。
Java Database Connectivity,Java 数据库连接(Database 数据库,Connectivity 连通、联接),简称 JDBC。是 Java 应用程序与各种数据库和数据源之间连接的 API(Application Programming Interface,应用程序编程接口)。
Java 数据库连接(JDBC) API 提供了来自 Java 编程语言的通用数据访问。使用 JDBC API,几乎可以访问任何数据源,可以使用基本的 JDBC API 来创建表、向表中插入值、查询表、检索查询结果以及更新表。
其实我们知道即使是关系型数据库,市面上也有很多种,比如 MySQL、Oracle 等,JDBC API 如何做到通用呢?也就是说如何做到访问不同的数据库呢?
其实要在特定的数据库管理系统中使用 JDBC API,需要一个基于 JDBC 技术的驱动程序来在 JDBC 技术和数据库之间进行中介。根据各种因素,驱动程序可以完全用 Java 编程语言编写,也可以混合使用 Java 编程语言和 Java 本机接口(Java Native Interface, JNI)本地方法编写。
简单理解就是 JDBC API 是 Java 程序连接数据库的标准接口,具体说各个数据库想要通过 Java 编程使用,就需要由各个数据库厂商自己按照 JDBC 标准提供具体实现。
来一张图进行说明吧
JDBC 为多种关系数据库提供了统一访问方式,作为特定厂商数据库访问 API 的一种高级抽象,它主要包含一些通用的接口类。然后各厂商提供自己的 JDBC 实现,也就是 JDBC 驱动程序。最后程序员就可以通过 JDBC 驱动程序实现该数据库的操作了。
这里简单介绍一下 JDBC 组件,大家了解即可。
JDBC 包括四个组件:
java.sql
和javax.sql
。这两个包都包含在 Java SE 和 Java EE 平台中。DriverManager
一直是 JDBC 体系结构的支柱。它是相当小和简单。前面大家尽量理解就好,本章节就是重点了,我们要使用 JDBC 进行实操了。
JDBC 帮助可以编写管理以下三种编程活动的 Java 应用程序:
JDBC 可以做的这三件事,就是 JDBC 编程的大致操作步骤。
我们打开客户端 MySQL Workbench,连接到本地 MySQL 服务器。接下来图形界面操作起来吧。
数据库中创建 teacher
表
给 teacher
表中添加数据,点击图中红色框中的图标,在数据界面添加数据。
打开 IDEA,创建一个普通的 Java 项目。
在 src
下新建 Test
类,生成主函数 main()
方法
做好以上准备后,我们来看 JDBC 详细编程步骤:
我们看具体演示:
第一步:将数据库驱动程序引入到项目,并在程序中加载
JDBC 驱动程序是由数据库厂商提供的,这里使用了 MySQL 数据库,所以我们可以到 MySQL 官网下载该驱动,也就是一个 jar 包。
下载步骤大家可以参考这篇文章:https://blog.csdn.net/Li_Ya_Fei/article/details/104583417
我本地的 MySQL 版本是8.0.30,所以从官网下载了对应版本的驱动 jar包。下载后的 jar包如下图所示:
在 test-jdbc
项目中新建 lib
文件夹(习惯性将 jar 包统一放在此文件夹管理,lib
也就代表 Library,库),将 jar 包复制进来,这时该 jar 只是放在了 lib
文件夹下,并没有引入到项目中,需要选中 jar 包右键,在菜单中点击 Add as Library...
,将其添加了项目库中。
将 MySQL 数据库驱动引入到项目后,如何在程序中加载呢?
我们使用 Class.forName(‘驱动路径’)
去动态加载 MySQL 的 JDBC 驱动实现。它的作用是加载指定路径下的 MySQL JDBC 驱动,并将其注册到 DriverManager
中,以便后续的数据库连接操作可以使用该驱动。
/**
* 敲入main,根据提示自动生成主函数main()方法
* @param args
*/
public static void main(String[] args) {
try {
// ①动态加载指定路径下的 MySQL JDBC驱动,将其注册到 DriverManager中。
Class.forName("com.mysql.cj.jdbc.Driver");
} catch (ClassNotFoundException e) {
// ①加载的类找不到时,会抛出 ClassNotFoundException异常。
e.printStackTrace();
}
}
需要注意的是,这段代码必须在其他数据库操作之前执行,以确保在使用其他数据库操作之前已经正确加载了 MySQL 的 JDBC 驱动。如果该驱动类不存在或无法被加载,则会抛出 ClassNotFoundException
异常,打印异常堆栈信息。
这里只需要记住 MySQL JDBC 驱动的位置是 com.mysql.cj.jdbc.Driver
。我们可以直接找到该类。
我们顺便看一眼源码,其实还挺简单的。
它定义了一个名为 Driver
的类,该类继承自 NonRegisteringDriver
并实现了 java.sql.Driver
接口。足以说明该类是 JDBC API 的一个实现。在静态代码块中,尝试使用 DriverManager.registerDriver()
方法注册这个驱动。如果注册失败,将抛出一个运行时异常。
第二步:建立 JDBC 和数据库之前的 Connection 连接
上一步将 MySQL JDBC 驱动注册到 DriverManager
,从字面我们知道DriverManager
就是驱动的管理器,管理诸多驱动程序。所以我们建立与数据库的连接,要从 DriverManager
获得。使用DriverManager.getConnection(‘连接信息’)
,方法中需要填写连接信息,我们使用客户端也需要先填写连接信息才能操作数据库,这里也是一样的,也需要有连接信息,才能连接到数据库。
我们先上代码,然后再解释说明。
/**
* 敲入main,根据提示自动生成主函数main()方法
* @param args
*/
public static void main(String[] args) {
try {
// ①动态加载指定路径下的 MySQL JDBC驱动,将其注册到 DriverManager中。
Class.forName("com.mysql.cj.jdbc.Driver");
// ②建立到给定数据库 URL的连接。
Connection connection = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/test_jdbc?serverTimezone=Asia/Shanghai", "root", "root");
} catch (ClassNotFoundException e) {
// ①加载的类找不到时,会抛出 ClassNotFoundException异常。
e.printStackTrace();
} catch (SQLException throwables) {
// ②如果数据库访问错误发生或 url为空时,会抛出 SQLException异常。
throwables.printStackTrace();
}
}
在上述代码中,②处用于建立到 MySQL 数据库的连接,它使用了DriverManager.getConnection()
方法来获取一个与指定 URL 相对应的数据库连接对象。该方法有 SQLException
这个异常需要处理。在代码中,DriverManager.getConnection()
方法接受三个参数:
jdbc:mysql://127.0.0.1:3306/test_jdbc?serverTimezone=Asia/Shanghai
,表示连接到本地主机(127.0.0.1)的 MySQL 服务器上,端口号为3306,数据库名为 test_jdbc
,并设置了时区为 Asia/Shanghai
。注意,不设置时区是会报错的哦!该方法返回 Connection
对象,我们接收一下就可以,Connection
对象就是与 MySQL 数据库的连接(会话)。我们可以在连接的上下文中执行 SQL 语句并返回结果。
这里大家其实需要注意 MySQL 数据库 URL 的写法,虽然现在网上查询 URL 很方便,但是自己能记忆最好。
第三步:创建 Statement 对象,用于向数据库发送 SQL 语句
这里使用 Connection
连接对象中的 createStatement()
方法创建 Statement
语句对象,用于向数据库发送 SQL 语句。Statement
用于实现不带参数的简单 SQL 语句。
/**
* 敲入main,根据提示自动生成主函数main()方法
* @param args
*/
public static void main(String[] args) {
try {
// ①动态加载指定路径下的MySQL JDBC驱动,将其注册到DriverManager中。
Class.forName("com.mysql.cj.jdbc.Driver");
// ②建立到给定数据库URL的连接。
Connection connection = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/test_jdbc?serverTimezone=Asia/Shanghai", "root", "root");
// ③创建一个Statement对象,用于向数据库发送SQL语句。
Statement statement = connection.createStatement();
} catch (ClassNotFoundException e) {
// ①加载的类找不到时,会抛出ClassNotFoundException异常。
e.printStackTrace();
} catch (SQLException throwables) {
// ②如果数据库访问错误发生或url为空时,会抛出SQLException异常。
throwables.printStackTrace();
}
}
大家看上述代码的③处,创建了 Statement
对象后赋值给 statement
变量。
第四步:执行 SQL 语句并处理返回结果
Statement
用于执行不带参数的简单的静态 SQL 语句,并返回其产生的结果。我们可以看一下 Statement
这个类提供的方法:
第一个 executeQuery()
,执行 select
查询语句,返回结果集 ResultSet
。execute
是执行的意思,query
就是查询的意思。这个两个单词在编程中经常见。
/**
* 执行给定的SQL语句,该语句返回单个ResultSet对象。
* 参数:sql—发送到数据库的sql语句,通常是静态sql SELECT语句
* 返回:一个ResultSet对象,其中包含给定查询产生的数据;没有空
*
*/
ResultSet executeQuery(String sql) throws SQLException;
第二个 executeUpdate()
,执行更新操作,比如 insert
、update
或 delete
语句,返回执行的行数。update
就是更新的意思。这个单词在编程中也经常见。
/**
* 执行给定的SQL语句,该语句可以是INSERT、UPDATE或DELETE语句,也可以是不返回任何结果的SQL语句,例如SQL DDL语句。
* 参数:sql—sql数据操作语言(DML)语句,如INSERT, UPDATE或DELETE;或者是不返回任何结果的SQL语句,比如DDL语句。
* 返回:(1)SQL数据操作语言(DML)语句的行数,或(2)不返回任何结果的SQL语句的0
*/
int executeUpdate(String sql) throws SQLException;
接下来我们使用 executeQuery()
方法,查询 teacher
表中的数据,上代码。
/**
* 敲入main,根据提示自动生成主函数main()方法
* @param args
*/
public static void main(String[] args) {
try {
// ①动态加载指定路径下的MySQL JDBC驱动,将其注册到DriverManager中。
Class.forName("com.mysql.cj.jdbc.Driver");
// ②建立到给定数据库URL的连接。
Connection connection = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/test_jdbc?serverTimezone=Asia/Shanghai", "root", "root");
// ③创建一个Statement对象,用于向数据库发送SQL语句。
Statement statement = connection.createStatement();
// ④执行SQL语句并返回结果
ResultSet resultSet = statement.executeQuery("select * from teacher");
} catch (ClassNotFoundException e) {
// ①加载的类找不到时,会抛出ClassNotFoundException异常。
e.printStackTrace();
} catch (SQLException throwables) {
// ②如果数据库访问错误发生或url为空时,会抛出SQLException异常。
throwables.printStackTrace();
}
}
上述代码④处,调用 Statement
对象中的 executeQuery()
执行 SQL 语句select * from teacher
,查询 teacher
表中全部数据,注意没有结束 ;
号。将查询结果封装在 ResultSet
结果集,我们需要接收一下。
第五步:处理 SQL 执行的结果
处理结果集,也就是我们需要从 ResultSet
中将查询的数据取出来。通过游标访问对象中的数据。它就是 ResultSet
类中的 next()
。请注意,此游标不是数据库游标。此光标是指向对象中一行数据的指针。最初,光标位于第一行之前。大家仔细阅读下面⑤处代码。
/**
* 敲入main,根据提示自动生成主函数main()方法
* @param args
*/
public static void main(String[] args) {
try {
// ①动态加载指定路径下的MySQL JDBC驱动,将其注册到DriverManager中。
Class.forName("com.mysql.cj.jdbc.Driver");
// ②建立到给定数据库URL的连接。
Connection connection = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/test_jdbc?serverTimezone=Asia/Shanghai", "root", "root");
// ③创建一个Statement对象,用于向数据库发送SQL语句。
Statement statement = connection.createStatement();
// ④执行SQL语句并返回结果
ResultSet resultSet = statement.executeQuery("select * from teacher");
// ⑤处理SQL语句执行的结果
while(resultSet.next()){
// 从结果集将当前行数据取出
int id = resultSet.getInt("id");
String name = resultSet.getString("name");
String sex = resultSet.getString("sex");
int age = resultSet.getInt(4);
// 打印输出当前行数据
System.out.println("id:" + id + ",name:" + name + ",sex:" + sex + ",age:" + age);
}
} catch (ClassNotFoundException e) {
// ①加载的类找不到时,会抛出ClassNotFoundException异常。
e.printStackTrace();
} catch (SQLException throwables) {
// ②如果数据库访问错误发生或url为空时,会抛出SQLException异常。
throwables.printStackTrace();
}
}
由于 select
语句查询的结果可能为多行,故使用 while
循环,条件就是 next()
方法,该方法返回 boolean
值。上面说过 ResultSet
游标最初定位在第一行之前,那么 while
循环中该方法的第一次调用 next
使第一行成为当前行,第二次调用将第二行作为当前行,依此类推。当对 next
方法的调用返回 false
时,游标定位在最后一行之后。
那么在循环体内,我们就可以将当前行数据取出,如何取出数据呢?ResultSet
类中提供了一系列方法。
取数据,我们使用 get
开头的方法,比如 getInt()
方法是取出某列的 int
类型数据,getString()
方法就是取出字符串类型数据。
我们可以看到 getInt()
方法有两个,这两个方法就是重载,方法重载(Overloading)是指在同一个类中,允许存在多个同名方法,但这些方法的参数列表必须不同。
获取其他类型数据也都有两个方法,它们的参数类型不同。
/**
* 以Java编程语言的int形式检索此ResultSet对象当前行的指定列的值。
* columnLabel—用SQL AS子句指定的列的标签。如果没有指定SQL AS子句,则标签是列的名称
*/
int getInt(String columnLabel) throws SQLException;
/**
* 以Java编程语言的int形式检索此ResultSet对象当前行的指定列的值。
* columnIndex -第一列是1,第二列是2,…
*/
int getInt(int columnIndex) throws SQLException;
简单来说第一个方法是根据列名获取数据,第二个方法是根据列号获取数据。哪种方式更好呢?当然是第一种根据列名,如果根据第几列获取,很容易搞混,而且代码不易维护。
第六步:关闭资源
当我们使用完对象后,调用其方法以立即释放它正在使用的资源。
/**
* 敲入main,根据提示自动生成主函数main()方法
* @param args
*/
public static void main(String[] args) {
try {
// ①动态加载指定路径下的MySQL JDBC驱动,将其注册到DriverManager中。
Class.forName("com.mysql.cj.jdbc.Driver");
// ②建立到给定数据库URL的连接。
Connection connection = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/test_jdbc?serverTimezone=Asia/Shanghai", "root", "root");
// ③创建一个Statement对象,用于向数据库发送SQL语句。
Statement statement = connection.createStatement();
// ④执行SQL语句并返回结果
ResultSet resultSet = statement.executeQuery("select * from teacher");
// ⑤处理SQL语句执行的结果
while(resultSet.next()){
// 从结果集将当前行数据取出
int id = resultSet.getInt("id");
String name = resultSet.getString("name");
String sex = resultSet.getString("sex");
int age = resultSet.getInt(4);
// 打印输出当前行数据
System.out.println("id:" + id + ",name:" + name + ",sex:" + sex + ",age:" + age);
}
// ⑥关闭资源,关闭顺序:从里到外的原则,保证该资源确定不再使用后关闭
resultSet.close();
statement.close();
connection.close();
} catch (ClassNotFoundException e) {
// ①加载的类找不到时,会抛出ClassNotFoundException异常。
e.printStackTrace();
} catch (SQLException throwables) {
// ②如果数据库访问错误发生或url为空时,会抛出SQLException异常。
throwables.printStackTrace();
}
}
上述代码⑥处,调用了三个对象的 close()
方法来关闭我们使用的资源。这些资源在使用完毕之后是一定要进行关闭的,不然这些资源可能一致被占用。
到这里一个 JDBC 编程基本步骤就完成了,最后就是验证成果的时候了。我们运行程序,查看控制台是否会打印出 teacher
表中的数据。
对 JDBC 编程基本步骤中的代码进行优化,有兴趣的可以看一下。
主要优化的地方是将关闭资源操作放在 finally
块中,即程序无论正确执行还是抛出异常都要关闭资源。
/**
* 敲入main,根据提示自动生成主函数main()方法
* @param args
*/
public static void main(String[] args) {
// 将Connection、Statement、ResultSet对象变量提取至try...catch外
Connection connection = null;
Statement statement = null;
ResultSet resultSet = null;
try {
// ①动态加载指定路径下的MySQL JDBC驱动,将其注册到DriverManager中。
Class.forName("com.mysql.cj.jdbc.Driver");
// ②建立到给定数据库URL的连接。
connection = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/test_jdbc?serverTimezone=Asia/Shanghai", "root", "root");
// ③创建一个Statement对象,用于向数据库发送SQL语句。
statement = connection.createStatement();
// ④执行SQL语句并返回结果
resultSet = statement.executeQuery("select * from teacher");
// ⑤处理SQL语句执行的结果
while(resultSet.next()){
// 从结果集将当前行数据取出
int id = resultSet.getInt("id");
String name = resultSet.getString("name");
String sex = resultSet.getString("sex");
int age = resultSet.getInt(4);
// 打印输出当前行数据
System.out.println("id:" + id + ",name:" + name + ",sex:" + sex + ",age:" + age);
}
} catch (ClassNotFoundException | SQLException e) {
/* 异常合并到一起
* ①加载的类找不到时,会抛出ClassNotFoundException异常。
* ②如果数据库访问错误发生或url为空时,会抛出SQLException异常。
*/
e.printStackTrace();
} finally {
try {
// ⑥关闭资源,关闭顺序:从里到外的原则,保证该资源确定不再使用
if (resultSet != null) {
resultSet.close();
}
if (statement != null) {
statement.close();
}
if (connection != null) {
connection.close();
}
} catch (SQLException throwables) {
throwables.printStackTrace();
}
}
}
其实除了上述的简单优化,还可以使用 with-resources
来自动关闭资源。with-resources
是 Java 7 引入的一个新特性,用于自动管理资源的关闭。它允许在 try-with-resources
语句中声明需要关闭的资源,当 try
代码块执行完毕后,资源会自动被关闭,无需显式调用 close()
方法。使用 with-resources
可以简化代码,避免资源泄漏和手动关闭资源的繁琐操作。它适用于实现了 java.lang.AutoCloseable
接口的资源对象,包括文件流、数据库连接、网络连接等。
Connection
、Statement
和 ResultSet
都实现了 java.lang.AutoCloseable
接口,也就说这三个资源可以使用 with-resources
来自动关闭。需要注意的是,使用 with-resources
语句时,资源对象的初始化必须在 try
代码块之前完成。以下是示例代码:
try (FileInputStream fis = new FileInputStream("file.txt");
BufferedReader br = new BufferedReader(new InputStreamReader(fis))) {
String line;
while ((line = br.readLine()) != null) {
System.out.println(line);
}
} catch (IOException e) {
e.printStackTrace();
}
从代码中可以看出要关闭的资源在 try
的 ()
内初始化的,那对于咱们 JDBC 来说 Connection
、Statement
和 ResultSet
都在 try
的 ()
内初始化,感觉乱,并不舒服。所以我就想了想,没有去尝试。有兴趣的小伙伴可以尝试一下,对于 JDBC 关闭资源,with-resources
这个特性是不是有优势。
Java EE 中关于 JDBC API,是使用 Java SE 中的 JDBC。
Java 提供一个标准的 JDBC 接口,也就是定义这一规范。然后由各数据库厂商负责实现。所以我们想用 Java 连接哪个数据库,就需要有哪个数据库的 JDBC 驱动程序。
JDBC 负责三件事:
JDBC 具体编程步骤:
只是看是学不会的,要赋予实际的行动——敲代码!