文章目录
- 简介
- JDBC API
- JDBC Driver Manager
- JDBC 驱动
- JDBC 开发步骤
- 一,导入 JDBC 驱动包,并加载驱动类
- 二,建立数据库连接
- 三,发送 SQL 语句,并获取执行结果
- Statement 对象
- PreparedStatement 对象
- 四,处理返回结果集
- JDBC 实现单表增删改查
- 一,使用 Statement 访问数据库
- 实现“增、删、改”操作
- 实现“查询”操作
- 二,使用 PreparedStatement 访问数据库
- 实现“增、删、改”操作。
- 实现“查询”操作
- 三,JDBC 中的异常处理及资源关闭
- 四,Statement 和 PreparedStatement 的比较
- 1,提高了代码的可读性和可维护性
- 2,提高了 SQL 语句的性能
- 3,提高了安全性,能有效防止 SQL 注入
- 五,使用 JDBC 调用存储过程和存储函数
- 1,调用存储过程(无返回值)
- 2,调用存储函数(有返回值)
- JDBC 存取大文本和二进制数据
- 一,读写 CLOB 数据
- 1,创建 myTxt 表,并设置 CLOB 类型的字段 text,SQL 脚本如下程序清单所示
- 2,将小说写入 myTxt 表的 clob 字段(text 类型)
- 3,读取数据库中的小说
- 二,读写 BLOB 数据
- 1,创建 myPicture 表,并设置 BLOB 类型的字段 img,SQL 脚本如下所示,
- 2,将图片写入 myPicture 表的 img 字段(BLOB 类型)
- 3,读取数据库中的图片
- JDBC 总结
简介
JDBC 的顶层是开发人员自己编写的 Java 应用程序,如下图所示。Java 应用程序可以通过集成在 JDK 中的 java.sql 及 javax.sql 包中的 JDBC API 来访问数据库。
下面讲解上图中出现的一些 JDBC 重要组件,思维导图如下。
JDBC API
JDBC API 存在于 JDK 中,其中包含了 Java 应用程序与各种不同数据库交互的标准接口,如 Connection 是连接数据库的接口、Statement 是操作数据库的接口、ResultSet 是查询结果集接口、PreparedStatement 是预处理操作接口等,开发者可以使用这些 JDBC 接口操作关系型数据库。 JDBC API 中常用接口和类的介绍如下表所示:
接口 /类 | 简介 |
---|---|
DriverManager 类 | 根据不同的数据库,管理相应的 JDBC 驱动。可以通过 DriverManager 类的 getConnection()方法获取数据库连接对象(即 Connection 对象)。 |
Connection 接口 | 由 DriverManager 产生,用于连接数据库并传递数据。 |
Statement 接口 | 由 Connection 产生,用于执行增、删、改、查等 SQL 语句。 |
PreparedStatement 接口 | Statement 的子接口(该接口的定义是:public interface PreparedStatement extends Statement{…})。PreparedStatement 同样由 Connection 产生,同样用于执行增删改查等 SQL 语句。与 Statement 接口相比,Statement 具有更高的安全性(可以防止 SQL 注入等安全隐患)、更高的性能、更高的可读性和可维护性等优点。 |
CallableStatement 接口 | PreparedStatement 的子接口(该接口的定义是:public interface CallableStatement extends PreparedStatement {…}),CallableStatement 同样由 Connection 产生,用于调用存储过程或存储函数。 |
ResultSet 接口 | 接收 Statement 对象(或 PreparedStatement 对象)执行查询操作后,返回的结果集。 |
从开发的角度讲,JDBC API 主要完成三件事:
- 与数据库建立连接
- 向数据库发送 SQL 语句
- 返回数据库的处理结果
JDBC Driver Manager
JDBC Driver Manager 也存在于 JDK 中,负责管理各种不同数据库的 JDBC 驱动。
JDBC 驱动
JDBC 驱动由各个数据库厂商或第三方厂商提供,负责针对不同数据库实现 JDBC API。例如,应用程序访问 MySql 和 Oracle 时,就需要不同的 JDBC 驱动。这些 JDBC 驱动都各自实现了 JDBC API 中定义的各种接口。在使用 JDBC 连接数据库时,只要正确加载了 JDBC 驱动,就可以通过调用 JDBC API 来操作数据库。
JDBC 开发步骤
开发一个 JDBC 程序,有以下四个基本步骤:
一,导入 JDBC 驱动包,并加载驱动类
使用 JDBC 访问数据库前,需要先导入相应的驱动包(如 oracle 数据库的驱动包是 ojdbc 版本号.jar)。这里我们以 MySQL 为例介绍在 WebIDE 中导入驱动包的步骤:
- 新建一个 java 工程目录,本次实验的所有代码也将以工程的形式运行,所有代码将放在 demo/src 下:
mkdir demo demo/src demo/lib demo/bin
- 下载驱动包并移动到工程的 lib 目录下:\
wget https://labfile.oss.aliyuncs.com/courses/3232/mysql-connector-java-8.0.22.zip
unzip mysql-connector-java-8.0.22.zip
cp ./mysql-connector-java-8.0.22/mysql-connector-java-8.0.22.jar ./demo/lib
注:也可以到 MySQL 的官网上下载驱动包:Connector/J
驱动包放入 lib 目录下之后,就可以使用 Class.forName() 方法将具体的 JDBC 驱动类加载到 JVM 中,加载的代码如下:
Class.forName("JDBC 驱动类名");
如果指定的驱动类名不存在,就会引发 ClassNotFoundException 异常。
之后在代码中,就可以利用连接字符串、用户名和密码等参数来获取数据库连接对象。常见关系型数据库的 JDBC 驱动包包名、驱动类类名及连接字符串如下表所示:
数据库 | JDBC 驱动包 | JDBC 驱动类 | 连接字符串 |
---|---|---|---|
Oracle | ojdbc 版本号.jar | oracle.jdbc.OracleDriver | jdbc:oracle:thin:@localhos t:1521:数据库实例名 |
MySQL | mysql-connector-java -版本号-bin.jar | com.mysql.jdbc.Driver | jdbc:mysql://localhost:3306/数据库实例名 |
SqlServer | sqljdbc 版本号.jar | com.microsoft.sqlserver.jdbc.SQLServerDriver | jdbc:microsoft:sqlserver://localhost:1433; databasename=数据库实例名 |
连接字符串 由协议、服务器地址、端口和数据库实例名构成,示例中 localhost 可被替换成服务器的 ip 地址,1521、3306 和 1433 分别是 Oracle、MySQL 和 SqlServer 三种数据库的默认端口号。
当程序调用 Class.forName(“JDBC 驱动类名”); 在使用 java 命令运行程序时,就需要使用以下命令将其加入到 classpath 中:
javac -d bin/ src/JDBCUpdateByStatement.java
java -cp bin/:lib/mysql-connector-java-8.0.22.jar JDBCUpdateByStatement
二,建立数据库连接
JDBC 使用 DriverManager 类来管理驱动程序,并通过其 getConnection() 获取连接对象,代码如下:
Connection connection = DriverManager.getConnection("连接字符串","数据库用户名","数据库密码");
Connection 接口的常用方法如下表所示:
方 法 | 简 介 |
---|---|
Statement createStatement() throws SQLException | 创建 Statement 对象 |
PreparedStatement prepareStatement(String sql) | 创建 PreparedStatement 对象 |
三,发送 SQL 语句,并获取执行结果
获得了 Connection 对象后,就可以通过 Connection 对象来获得 Statement 或 PreparedStatement 对象,并通过该对象向数据库发送 SQL 语句。
Statement 对象
//创建 Statement 对象
Statement stmt = connection.createStatement();
发送“增、删、改”类型的 SQL 语句:
int count = stmt.executeUpdate("增、删、改的 SQL 语句")
发送“查询”类型的 SQL 语句:
ResultSet rs = stmt.executeQuery("查询的 SQL 语句");
如果 SQL 语句是增、删、改操作,会返回一个 int 型结果,表示多少行受到了影响,即增、删、改了几条数据;如果 SQL 语句是查询操作,数据库会返回一个 ResultSet 结果集,该结果集包含了 SQL 查询的所有结果。
Statement 对象的常用方法如下表所示:
方法 | 简介 |
---|---|
int executeUpdate() | 用于执行 INSERT、UPDATE、DELETE 以及 DDL(数据定义语言)语句(如 CREATE TABLE… 和 DROP TABLE…)。 对于 CREATE TABLE 或 DROP TABLE 等 DDL 类型的语句,executeUpdate 的返回值总为零。 |
ResultSet executeQuery() | 用于执行 SELECT 查询语句,返回值是一个 ResultSet 类型的结果集。 |
void close() | 关闭 Statement 对象。 |
对于 CREATE TABLE 或 DROP TABLE 等 DDL 类型的语句,executeUpdate 的返回值总为零。
ResultSet executeQuery() 用于执行 SELECT 查询语句,返回值是一个 ResultSet 类型的结果集。
void close() 关闭 Statement 对象。
PreparedStatement 对象
//创建 PreparedStatement 对象
PreparedStatement pstmt = connection.prepareStatement("增、删、改、查的 SQL 语句");
发送”增、删、改“类型的 SQL 语句:
int count = pstmt.executeUpdate()
发送”查询“类型的 SQL 语句:
ResultSet rs = pstmt.executeQuery();
PreparedStatement 对象的常用方法如下表所示:
方 法 | 简 介 |
---|---|
executeUpdate() | 用法上,类似于 Statement 接口中的 executeUpdate()。 |
executeQuery() | 用法上,类似于 Statement 接口中的 executeQuery ()。 |
setXxx() | 有 setInt()、setString()、setDouble()等多个方法,用于给 SQL 中的占位符“?”赋值。setXxx()方法有两个参数,第一个参数表示占位符的位置(从 1 开始),第二个参数表示占位符所代表的具体值。 例如可以将 SQL 写成“select * from student where name=? and age = ? ”,其中两个问号代表两个占位符,之后再使用 setString(1,“张三”)和 setInt(2,23)来分别为两个占位符赋值(即给 name 和 age 赋值) |
close() | 关闭 PreparedStatement 对象 |
四,处理返回结果集
如果是查询操作,可以通过迭代的方式循环取出结果集中的所有数据:首先通过 rs.next() 判断是否还有下一行数据,如果有,rs 就会移动到下一行,之后再通过 rs.getXxx() 获取行内的每列数据,如下:
while(rs.next()) {
int stuNo = rs.getInt("stuNo");
String stuName = rs.getString("stuName");
…
}
ResultSet 的常用方法如下表所示:
方法 | 简介 |
---|---|
boolean next() | 将光标从当前位置向下移动一行,指向结果集中的下一行数据。通常用来判断查询到的结果集中是否还有数据。如果有,则返回 true,否则返回 false。 |
boolean previous() | 将光标从当前位置向上移动一行。 |
int getInt(int columnIndex) | 获取当前一行数据中指定列号的字段值,该列必须是整数类型的字段。例如,学生表中有 number 类型的 stuNo 字段在第一列,就可以使用 getInt(1)来获取值。 除此之外,还有 getFloat()、getString()、getDate()、getBinaryStream()等多个类似方法,用于获取不同类型的字段。 |
int getInt(String columnLabel) | 获取当前一行数据中指定列名的字段值,该列必须是整数类型的字段。例如,学生表中有 number 类型的 stuNo 字段,就可以使用 getInt(“stuNo”)来获取值。 除此之外,还有 getFloat()、getString()、getDate()等多个类似方法,用于获取不同类型的字段。 |
void close() | 关闭 ResultSet 对象。 |
JDBC 实现单表增删改查
本节以 MySQL 数据库为例,在实际业务场景中体会 JDBC 细节。 假设数据库中存在一张学生表 student,各字段名称及类型如下表所示:
字 段 名 | 类型 | 含义 |
---|---|---|
stuNo | int | 学号 |
stuName | varchar(20) | 学生姓名 |
stuAge | int | 学生年龄 |
重新打开一个 Terminal ,启动 MySQL:
sudo service mysql start
mysql -u root -p
实验楼的 MySQL 数据库,root 用户没有设置密码,直接按回车。
之后创建数据库和表:
create database if not exists shiyanlou default character set utf8; #创建数据库
use shiyanlou; #选择数据库
create table student
(
stuNo int not null,
stuName varchar(20),
stuAge int,
primary key (stuNo)
);
一,使用 Statement 访问数据库
之前已经介绍过了 JDBC 的开发步骤,并且知道在使用 JDBC 时需要区分增删改和查询操作,以下是具体的实现细节。
实现“增、删、改”操作
本案例先使用 Statement 提供的的 executeUpdate() 方法,执行插入操作,详见程序清单 JDBCUpdateByStatement.java 。
import java.sql.*;
public class JDBCUpdateByStatement{
final static String DRIVER = "com.mysql.jdbc.Driver";
//数据库的实例名是 shiyanlou
final static String URL = "jdbc:mysql://localhost:3306/shiyanlou?useUnicode=true&characterEncoding=utf8";
final static String USERNAME = "root";
final static String PASSWORD = "";
static Connection connection = null;
static Statement stmt = null;
static ResultSet rs = null;
//执行 `插入` 的方法
public static boolean executeUpdate() {
boolean flag = false ;
try{
//1 加载数据库驱动
Class.forName(DRIVER);
//2 获取数据库连接
connection = DriverManager.getConnection(URL, USERNAME, PASSWORD);
//3 通过连接,获取一个 Statement 的对象,用来操作数据库
stmt = connection.createStatement();
//4 通过 executeUpdate()实现插入操作
String addSql = "insert into student(stuNo,stuName,stuAge) values(5,'王五',25)" ;
int count = stmt.executeUpdate(addSql);
System.out.println("受影响的行数是:"+count);
flag = true ;//如果一切正常,没有发生异常,则将 flag 设置为 true
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (SQLException e) {
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
} finally{
try {
if(stmt != null)stmt.close();
if(connection != null)connection.close();
} catch (SQLException e) {
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
}
}
return flag ;
}
public static void main(String[] args){
executeUpdate();
}
}
执行 executeUpdate() 方法,即可插入数据。
编译运行:
cd demo
javac -d bin/ src/JDBCUpdateByStatement.java
java -cp bin/:lib/mysql-connector-java-8.0.22.jar JDBCUpdateByStatement
输出结果:
以上是增加方法的执行细节,如果要执行删除操作,只需要修改上述代码中 executeUpdate() 方法的 SQL 参数,如下:
//通过 executeUpdate()实现对数据库的删除操作
String deleteSql = "delete from student where stuNo = 5" ;
int count = stmt.executeUpdate(deleteSql );
类似的,如果要执行修改操作,也只需要修改 executeUpdate() 方法中的 SQL 参数,如下。
//通过 executeUpdate()实现对数据库的修改操作
String updateSql = "update student set stuName = '李四' where stuName='王五'" ;
int count=stmt.executeUpdate(updateSql);
即增删改操作唯一不同的就是 executeUpdate() 方法中的 SQL 语句。
实现“查询”操作
接下来使用 Statement 对象实现 查询 数据库的操作。此时,笔者的数据库中 student 表中的数据如下图所示:
查询数据库和增、删、改操作的步骤基本相同,详见程序清单 JDBCQueryByStatement.java 。
import java.sql.*;
public class JDBCQueryByStatement{
final static String DRIVER = "com.mysql.jdbc.Driver";
//数据库的实例名是 shiyanlou
final static String URL = "jdbc:mysql://localhost:3306/shiyanlou?useUnicode=true&characterEncoding=utf8";
final static String USERNAME = "root";
final static String PASSWORD = "";
static Connection connection = null;
static Statement stmt = null;
static ResultSet rs = null;
public static void executeQuery() {
try{
//1 加载数据库驱动
Class.forName(DRIVER);
//2 获取数据库连接
connection = DriverManager.getConnection(URL, USERNAME, PASSWORD);
//3 通过连接,获取一个操作数据库 Statement 的对象
stmt = connection.createStatement();
//4 通过 executeQuery()实现对数据库的查询,并返回一个结果集(结果集中包含了所有查询到的数据)
String querySql = "select stuNo,stuName,stuAge from student";
rs = stmt.executeQuery(querySql);
//5 通过循环读取结果集中的数据
while(rs.next()) {
//等价于 rs.getInt(1);
int stuNo = rs.getInt("stuNo");
// rs.getString(2);
String stuName = rs.getString("stuName");
//rs.getInt(3);
int stuAge = rs.getInt("stuAge");
System.out.println(stuNo+"\t"+stuName+"\t"+stuAge);
}
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (SQLException e) {
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
} finally{
try {
//注意 rs、stmt、connection 三个对象的关闭顺序
if(rs != null)rs.close();
if(stmt != null)stmt.close();
if(connection != null)connection.close();
} catch (SQLException e) {
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
}
}
}
public static void main(String[] args){
executeQuery();
}
}
执行 executeQuery() 方法,即可查询出 student 表中所有的 stuNo、stuName 和 stuAge 字段值。
编译运行:
cd demo
javac -d bin/ src/JDBCQueryByStatement.java
java -cp bin/:lib/mysql-connector-java-8.0.22.jar JDBCQueryByStatement
输出结果:
如果是根据 String 类型的 name 变量进行模糊查询,那么查询的 SQL 语句可写为以下形式。
"select stuNo,stuName,stuAge from student where stuName like '%"+name + "%' "
请注意 % 两侧的单引号。
二,使用 PreparedStatement 访问数据库
在写代码的时候,PreparedStatement 和 Statement 对象的使用步骤基本相同,只不过在方法的参数、返回值等细节上存在差异。请大家仔细阅读程序清单 JDBCUpdateByPreparedStatement.java 中的代码,并和 Statement 方式的增删改操作进行比较。
实现“增、删、改”操作。
import java.sql.*;
public class JDBCUpdateByPreparedStatement{
final static String DRIVER = "com.mysql.jdbc.Driver";
//数据库的实例名是 shiyanlou
final static String URL = "jdbc:mysql://localhost:3306/shiyanlou?useUnicode=true&characterEncoding=utf8";
final static String USERNAME = "root";
final static String PASSWORD = "";
static Connection connection = null;
static PreparedStatement pstmt = null;
public static boolean executeUpdate() {
boolean flag = false;
try {
Class.forName(DRIVER);
connection = DriverManager.getConnection(URL, USERNAME, PASSWORD);
//用占位符来代替参数值
String deleteSql = "delete from student where stuName = ? and stuAge = ?" ;
pstmt = connection.prepareStatement(deleteSql);
//将第一个占位符?的值替换为 `张三` (占位符的位置是从 1 开始的)
pstmt.setString(1, "张三");
//将第二个占位符?的值替换为 23
pstmt.setInt(2, 23);
int count = pstmt.executeUpdate();
System.out.println("受影响的行数是:" + count);
flag = true;
}
catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (SQLException e) {
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
} finally{
try {
if(connection != null)connection.close();
} catch (SQLException e) {
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
}
}
return flag;
}
}
可见,与 Statement 相比,本次使用 PreparedStatement 执行增、删、改操作的不同之处如下:
- SQL 语句提前写在了 prepareStatement() 方法参数中;
- 先在 SQL 语句中使用了占位符 ? ,然后使用 setXxx() 方法对占位符进行了替换。
实现“查询”操作
请大家仔细阅读程序清单 JDBCQueryByPreparedStatement.java 中的代码,并和使用 Statement 进行查询操作的代码进行对比。
import java.sql.*;
public class JDBCQueryByPreparedStatement{
final static String DRIVER = "com.mysql.jdbc.Driver";
//数据库的实例名是 shiyanlou
final static String URL = "jdbc:mysql://localhost:3306/shiyanlou?useUnicode=true&characterEncoding=utf8";
final static String USERNAME = "root";
final static String PASSWORD = "";
static Connection connection = null;
static Statement stmt = null;
static ResultSet rs = null;
public static void executeQuery() {
Scanner input = new Scanner(System.in);
try{
Class.forName(DRIVER);
connection = DriverManager.getConnection(URL, USERNAME, PASSWORD);
System.out.println("请输入用户名:");
String name = input.nextLine();
System.out.println("请输入密码:");
String pwd = input.nextLine();
//如果用户输入的 username 和 password 在表中有对应的数据(count(1)>0),
//则说明存在此用户
String querySql = "select count(1) from login where username = ? and password = ?" ;
pstmt = connection.preparedStatement(querySql);
pstmt.setString(1, name);
pstmt.setString(2, pwd);
rs = pstmt.executeQuery();
if (rs.next()){
//获取 SQL 语句中 count(1)的值
int count = rs.getInt(1);
if (count > 0)
System.out.println("登录成功");
else {
System.out.println("登录失败");
}
}
}
catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (SQLException e) {
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
} finally{
try {
//注意 rs、stmt、connection 三个对象的关闭顺序
if(rs != null)rs.close();
if(stmt != null)stmt.close();
if(connection != null)connection.close();
} catch (SQLException e) {
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
如果使用 PreparedStatement
进行模糊查询,可以在 setXxx()
方法的第二个参数中加入通配符(如 %
)。例如,根据 name 模糊查询代码如下。
PreparedStatement pstmt = ... ;
ResultSet rs = ... ;
...
String querySql = "select \* from book where name like ?" ;
pstmt.setString(1, "%" +name +"%");
rs = pstmt.executeQuery();
需要注意的是,如果使用的是 Statement
,当需要给 SQL 语句拼接 String
类型变量时,必须加上单引号,例如 select … from … where stuName like '%"+name + "%';
但如果使用的是 PreparedStatement
,则不需要加,例如: pstmt.setString(1, "%" +name +"%")
。
三,JDBC 中的异常处理及资源关闭
在编写 JDBC 代码时,经常会遇到异常处理。 下表列出了一些常见抛出异常的方法:
方法 | 抛出的异常类型 |
---|---|
Class.forName()方法 | ClassNotFoundException |
DriverManager.getConnection()方法 | SQLException |
Connection 接口的 createStatement()方法 | SQLException |
Statement 接口的 executeQuery()方法 | SQLException |
Statement 接口的 executeUpdate()方法 | SQLException |
Connection 接口的 preparedStatement()方法 | SQLException |
PreparedStatement 接口的 setXxx()方法 | SQLException |
PreparedStatement 接口的 executeUpdate()方法 | SQLException |
PreparedStatement 接口的 executeQuery()方法 | SQLException |
ResultSet 接口的 next()方法 | SQLException |
ResultSet 接口的 close()方法 | SQLException |
Statement 接口的 close()方法 | SQLException |
Connection 接口的 close()方法 | SQLException |
为了及时地释放不再使用的资源,需要在数据库访问结束时,调用各个对象的 close() 方法,如下表所示:
方法 | 立即释放的资源 |
---|---|
ResultSet 接口的 close()方法 | 此 ResultSet 对象的数据库 JDBC 资源 |
Statement 接口的 close()方法 | 此 Statement 对象的数据库 JDBC 资源(包含 ResultSet 对象) |
Connection 接口的 close()方法 | 此 Connection 对象的数据库 JDBC 资源(包含 ResultSet、 Statement 对象) |
可以发现,三个 close() 释放的资源存在包含关系,所以在编码时,释放资源的顺序应该写为:ResultSet 对象的 close() 方法(查询操作)→ Statement 的对象 close() 方法 → Connection 对象的 close() 方法。也就是先释放范围小的资源,再释放范围大的资源。
值得注意的是,因为 PreparedStatement 继承自 Statement ,所以 Statement 接口的 close() 方法实际也代表了 PreparedStatement 对象的 close() 方法。
如果不及时的通过 close() 方法释放资源,已创建的 Connection 对象、Statement 对象、ResultSet 对象也会在 GC 执行垃圾回收时自动释放。但自动释放的方式会造成资源的释放不及时(必须等待 GC 主动回收),故不推荐。
综上,JDBC 的代码结构如下:
try{
① Class.forName("驱动字符串")
② 获取 Connection 对象
③ Statement 对象(或 PreparedStatement 对象)相关代码
④(如果是查询操作)ResultSet 对象相关代码
} catch (ClassNotFoundException e){
...
} catch (SQLException e){
...
} catch (Exception e) {
...
} finally {
try{
(如果是查询操作)关闭 ResultSet 对象
关闭 Statement 对象
关闭 Connection 对象
} catch (SQLException e){
...
} catch (Exception e){
...
}
}
四,Statement 和 PreparedStatement 的比较
Statement 和 PreparedStatement 都可以实现数据库的增删改查等操作。但在实际开发中,一般推荐使用 PreparedStatement 。因为两者相比,PreparedStatement 有如下优势。
1,提高了代码的可读性和可维护性
PreparedStatement 可以避免烦琐的 SQL 语句拼接操作。例如,SQL 语句 insert into student(stuNo,stuName,stuAge,course) values(5,‘王五’,25) ,如果将其中的字段值用变量来表示(int stuNo=5;String stuName=“王五”;int stuAge=23;),用 Statement 方式执行时,需要写成:
stmt.executeUpdate("insert into student(stuNo,stuName,stuAge ) values("+stuNo+",'"+stuName+"',"+stuAge+")");
而如果用 PreparedStatement 方式执行时,就可以先用 ? 充当参数值的占位符,然后再用 setXxx() 方法设置 ? 的具体值,从而避免 SQL 语句的拼接操作。
2,提高了 SQL 语句的性能
在使用 Statement 和 PreparedStatement 向数据库发送 SQL 语句时,数据库都会解析并编译该 SQL 语句,并将解析和编译的结果缓存起来。但在使用 Statement 时,这些缓存结果仅仅适用于那些完全相同的 SQL 语句(SQL 主体和拼接的 SQL 参数均相同)。换个角度讲,如果某条 SQL 的 SQL 主体相同,但拼接的参数不同,也仍然不会使用之前缓存起来的结果,这就严重影响了缓存的使用效率。
而 PreparedStatement 就不会像 Statement 那样将 SQL 语句完整的编译起来,而是采用了预编译机制:只编译 SQL 主体,不编译 SQL 参数。因此,在使用 PreparedStatement 时,只要多条 SQL 语句的 SQL 主体相同(与 SQL 语句中的参数无关),就可以复用同一份缓存。这点就类似于 Java 中方法调用的流程:Java 编译器会预先将定义的方法编译好(但不会编译方法的参数值),之后在多次调用这个方法时,即使输入参数值不同,也可以复用同一个方法。因此,如果某个业务需要重复执行主体相同的 SQL 语句(无论 SQL 中的参数是否相同),就可以利用 PreparedStatement 这种预编译 SQL 的特性来提高数据库缓存的利用率,进而提升性能。
但要注意的是,PreparedStatement 虽然在执行重复的 SQL 语句时具有较高的性能,但如果某个 SQL 语句仅仅会被执行一次或者少数几次,Statement 的性能是高于 PreparedStatement 的。
3,提高了安全性,能有效防止 SQL 注入
在使用 Statement 时,可能会用以下代码来进行登录验证。
stmt = connection.createStatement();
String querySql = "select count(_) from login where username = '"+uname+"' and password = '"+upwd+"'" ;
rs = stmt.executeQuery(querySql);
…
if(rs.next()){
int result = rs.getInt("count(_)");
if(result>0) { //登录成功}
else{//登录失败}
}
上述代码看起来没有问题,但试想如果用户输入的 uname 值是 任意值 or 1=1-- 、upwd 的值是 任意值 ,则 SQL 语句拼接后的结果如下:
select count(\*) from login where username = '任意值' or 1=1-- and password = '任意值';
在这条 SQL 语句中,用 or 1=1 使 where 条件永远成立,并且用 – 将后面的 SQL 语句注释掉,这样就造成了安全隐患(SQL 注入),使得并不存在的用户名和密码也能登录成功。
而 PreparedStatement 方式传入的任何数据都不会和已经编译的 SQL 语句进行拼接,因此可以避免 SQL 注入攻击。综上所述,在实际开发中推荐使用 PreparedStatement 操作数据库。
五,使用 JDBC 调用存储过程和存储函数
JDBC 除了能够向数据库发送 SQL 语句以外,还可以通过 CallableStatement 对象调用数据库中的存储过程或存储函数。
CallableStatement 对象可以通过 Connection 对象创建,如下:
CallableStatement cstmt= connection.prepareCall(调用储过程或存储函数);
调用存储过程(无返回值)时,prepareCall() 方法的参数(字符串)格式为:
{ call 存储过程名(参数列表) }
调用存储函数(有返回值)时,prepareCall() 方法的参数(字符串)格式为:
{ ? = call 存储过程名(参数列表) }
对于参数列表,需要注意以下两点:
- 参数的索引,是从 1 开始编号的。
- 具体的参数,既可以是输入参数(IN 类型),也可以是输出参数(OUT 类型)。输入参数使用 setXxx() 方法进行赋值;输出参数(或返回值参数)必须先使用 registerOutParameter() 方法设置参数类型,然后调用 execute() 执行存储过程或存储函数,最后再通过 getXxx() 获取结果值。
下面,通过两个数相加的示例,分别演示调用存储过程和存储函数的具体步骤。
1,调用存储过程(无返回值)
先在 MySQL 中,创建存储过程 addTwoNum() ,SQL 脚本如下所示。
delimiter $
create procedure addTwoNum
(
in num1 int, #输入参数
in num2 int, #输入参数
out total int #输出参数
)
begin
set total = num1 + num2;
end$
delimiter ;
再使用 JDBC 调用刚才创建好的存储过程,详见程序清单 JDBCOperateByCallableStatement.java 。
import java.sql.*;
//package、import
public class JDBCOperateByCallableStatement{
final static String DRIVER = "com.mysql.jdbc.Driver";
//数据库的实例名是 shiyanlou
final static String URL = "jdbc:mysql://localhost:3306/shiyanlou?useUnicode=true&characterEncoding=utf8";
final static String USERNAME = "root";
final static String PASSWORD = "";
static Connection connection = null;
static Statement stmt = null;
static ResultSet rs = null;
static CallableStatement cstmt = null;
public static void executeByCallableStatement(){
try {
Class.forName(DRIVER);
connection = DriverManager.getConnection(URL, USERNAME, PASSWORD);
//创建 CallableStatement 对象,并调用数据库中的存储过程 addTwoNum()
cstmt = connection.prepareCall("{call addTwoNum(?,?,?)}");
//将第一个参数值设为 10
cstmt.setInt(1, 10);
//将第二个参数值设为 20
cstmt.setInt(2, 20);
//将第三个参数(输出参数)类型设置为 int
cstmt.registerOutParameter(3, Types.INTEGER);
//执行存储过程
cstmt.execute() ;
//执行完毕后,获取第三个参数(输出参数)的值
int result = cstmt.getInt(3);
System.out.println("相加结果是:"+result);
}
catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (SQLException e) {
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
} finally{
try {
//注意 rs、stmt、connection 三个对象的关闭顺序
if(rs != null)rs.close();
if(stmt != null)stmt.close();
if(connection != null)connection.close();
} catch (SQLException e) {
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
}
}
}
public static void main(String[] args){
executeByCallableStatementWithResult();
}
}
编译运行:
cd demo
javac -d bin/ src/JDBCOperateByCallableStatement.java
java -cp bin/:lib/mysql-connector-java-8.0.22.jar JDBCOperateByCallableStatement
输出结果:
2,调用存储函数(有返回值)
先在 MySQL 中,创建存储函数 addTwoNumAndReturn() ,SQL 脚本如下程序清单所示。
delimiter $
create function addTwoNumAndReturn
(
num1 INTEGER, #输入参数
num2 INTEGER #输入参数
)
returns INTEGER #返回值类型
begin
declare total INTEGER;
set total = num1 + num2;
return total; #返回值
end $
delimiter ;
再使用 JDBC 调用刚才创建好的存储函数,详见程序清单 JDBCOperateByCallableStatement2.java 的。
import java.sql.*;
//package、import
public class JDBCOperateByCallableStatement2{
final static String DRIVER = "com.mysql.jdbc.Driver";
//数据库的实例名是 shiyanlou
final static String URL = "jdbc:mysql://localhost:3306/shiyanlou?useUnicode=true&characterEncoding=utf8";
final static String USERNAME = "root";
final static String PASSWORD = "";
static Connection connection = null;
static Statement stmt = null;
static ResultSet rs = null;
static CallableStatement cstmt = null;
public static void executeByCallableStatementWithResult(){
try {
Class.forName(DRIVER);
connection = DriverManager.getConnection(URL, USERNAME, PASSWORD);
//创建 CallableStatement 对象,并调用数据库中的存储函数
cstmt = connection.prepareCall("{? = call addTwoNumAndReturn(?,?)}");
//将第一个参数(返回值)类型设置为 int
cstmt.registerOutParameter(1, Types.INTEGER);
//将第二个参数值设为 10
cstmt.setInt(2, 10);
//将第三个参数值设为 20
cstmt.setInt(3, 20);
//执行存储函数
cstmt.execute() ;
//执行完毕后,获取第三个参数的值(返回值)
int result = cstmt.getInt(1);
System.out.println("相加结果是:"+result);
}
catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (SQLException e) {
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
} finally{
try {
//注意 rs、stmt、connection 三个对象的关闭顺序
if(rs != null)rs.close();
if(stmt != null)stmt.close();
if(connection != null)connection.close();
} catch (SQLException e) {
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
}
}
}
public static void main(String[] args){
executeByCallableStatementWithResult();
}
}
编译运行:
cd demo
javac -d bin/ src/JDBCOperateByCallableStatement2.java
java -cp bin/:lib/mysql-connector-java-8.0.22.jar JDBCOperateByCallableStatement2
输出结果:
JDBC 存取大文本和二进制数据
实际开发中,经常会处理一些大文本数据(Oracle 中的 CLOB 类型)或二进制数据(Oracle 中的 BLOB 类型)。要想在数据库中读写 CLOB 或 BLOB 类型的数据,就必须综合使用 PreparedStatement 和 IO 流的相关技术。
一,读写 CLOB 数据
CLOB 用于存放大文本数据。以下是将一篇小说写入 CLOB 类型字段的具体步骤。
在此之前,请同学们在 /home/project 下输入以下命令,下载实验用的 txt 文件:
wget https://labfile.oss.aliyuncs.com/courses/3232/introduce.txt
1,创建 myTxt 表,并设置 CLOB 类型的字段 text,SQL 脚本如下程序清单所示
create table myTxt
(
id int primary key,
clob text
);
2,将小说写入 myTxt 表的 clob 字段(text 类型)
先将小说转为字符输入流,然后通过 PreparedStatement 的 setCharacterStream () 方法写入数据库,详见程序清单 WriteText.java 。
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.sql.*;
//package、import
public class WriteText{
final static String DRIVER = "com.mysql.jdbc.Driver";
//数据库的实例名是 shiyanlou
final static String URL = "jdbc:mysql://localhost:3306/shiyanlou?useUnicode=true&characterEncoding=utf8";
final static String USERNAME = "root";
final static String PASSWORD = "";
static Connection connection = null;
static Statement stmt = null;
static ResultSet rs = null;
static PreparedStatement pstmt = null;
//将小说写入数据库
public static void writeTextToClob() {
try {
Class.forName(DRIVER);
connection = DriverManager.getConnection(URL, USERNAME, PASSWORD);
String sql = "insert into myTxt(id,clob) values(?,?)" ;
//处理 clob/blob,必须使用 PreparedStatement 对象
pstmt = connection.prepareStatement(sql) ;
pstmt.setInt(1, 1); // id=1
//将小说转为字符输入流,并设置编码格式为中文 GBK 格式
File file = new File("/home/project/introduce.txt");
Reader reader = new InputStreamReader(new FileInputStream(file),"utf8");
//将字符输入流写入 myTxt 表
pstmt.setCharacterStream(2, reader,(int)file.length());
int result = pstmt.executeUpdate();
if(result >0){
System.out.println("小说写入成功!");
}else {
System.out.println("小说写入失败!");
}
}
catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (SQLException e) {
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
} finally{
try {
//注意 rs、stmt、connection 三个对象的关闭顺序
if(rs != null) {
rs.close();
}
if(stmt != null) {
stmt.close();
}
if(connection != null) {
connection.close();
}
} catch (SQLException e) {
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
}
}
}
public static void main(String[] args){
writeTextToClob();
}
}
编译运行:
cd demo
javac -d bin/ src/WriteText.java
java -cp bin/:lib/mysql-connector-java-8.0.22.jar WriteText
输出结果:
3,读取数据库中的小说
通过 ResultSet 的 getCharacterStream() 方法读取小说,然后通过 IO 流写入硬盘(src 根目录),详见程序清单 ReadText.java 。
import java.io.FileWriter;
import java.io.Reader;
import java.io.Writer;
import java.sql.*;
//package、import
public class ReadText{
final static String DRIVER = "com.mysql.jdbc.Driver";
//数据库的实例名是 shiyanlou
final static String URL = "jdbc:mysql://localhost:3306/shiyanlou?useUnicode=true&characterEncoding=utf8";
final static String USERNAME = "root";
final static String PASSWORD = "";
static Connection connection = null;
static Statement stmt = null;
static ResultSet rs = null;
static PreparedStatement pstmt = null;
//从数据库读取小说,并放入 src 目录
public static void readTextToClob(){
try {
Class.forName(DRIVER);
connection = DriverManager.getConnection(URL, USERNAME, PASSWORD);
String sql = "select * from myTxt where id = ?" ;
pstmt = connection.prepareStatement(sql) ;
pstmt.setInt(1, 1);//id=1
rs = pstmt.executeQuery() ;
if(rs.next()){
//将小说从数据库中读取出,类型为 Reader
Reader reader = rs.getCharacterStream("clob") ;
//通过 IO 流将小说写到项目中(硬盘)
//将小说的输出路径设置为 src(相对路径)
Writer writer = new FileWriter("src/new_introduce.txt");
char[] temp = new char[200];
int len = -1;
while( (len=reader.read(temp) )!=-1) {
writer.write(temp,0,len);
}
writer.close();
reader.close();
System.out.println("Text 读取成功!");
}
}
catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (SQLException e) {
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
} finally{
try {
//注意 rs、stmt、connection 三个对象的关闭顺序
if(rs != null) {
rs.close();
}
if(stmt != null) {
stmt.close();
}
if(connection != null) {
connection.close();
}
} catch (SQLException e) {
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
}
}
}
public static void main(String[] args){
readTextToClob();
}
}
编译运行:
cd demo
javac -d bin/ src/ReadText.java
java -cp bin/:lib/mysql-connector-java-8.0.22.jar ReadText
输出结果:
此时可以看到在 ./src 下,生成了一个名为 new_introduce.txt 的文件:
二,读写 BLOB 数据
BLOB 可用于存放二进制数据(常用于保存图片、视频、音频等格式的数据)。以下是将图片存入 BLOB 类型字段的具体步骤。
在此之前,请同学们在 /home/project 下输入以下命令,下载实验用的 png 文件:
wget https://labfile.oss.aliyuncs.com/courses/3232/myPic.png
1,创建 myPicture 表,并设置 BLOB 类型的字段 img,SQL 脚本如下所示,
create table myPicture
(
id int primary key,
img Blob
);
2,将图片写入 myPicture 表的 img 字段(BLOB 类型)
先将图片转为输入流,然后通过 PreparedStatement 对象的 setBinaryStream() 方法写入数据库,详见程序清单 WriteImg.java 。
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.sql.*;
//package、import
public class WriteImg{
final static String DRIVER = "com.mysql.jdbc.Driver";
//数据库的实例名是 shiyanlou
final static String URL = "jdbc:mysql://localhost:3306/shiyanlou?useUnicode=true&characterEncoding=utf8";
final static String USERNAME = "root";
final static String PASSWORD = "";
static Connection connection = null;
static Statement stmt = null;
static ResultSet rs = null;
static PreparedStatement pstmt = null;
//将图片写入数据库
public static void writeImgToBlob() {
try {
Class.forName(DRIVER);
connection = DriverManager.getConnection(URL, USERNAME, PASSWORD);
String sql = "insert into myPicture(id,img) values(?,?)" ;
//处理 clob/blob,必须使用 PreparedStatement 对象
pstmt = connection.prepareStatement(sql) ;
pstmt.setInt(1, 1);//id=1
//将图片转为输入流
File file = new File("/home/project/myPic.png");
InputStream in = new FileInputStream(file);
//将输入流写入 myPicture 表
pstmt.setBinaryStream(2, in,(int)file.length());
int result = pstmt.executeUpdate();
if(result >0){
System.out.println("图片写入成功!");
}else {
System.out.println("图片写入失败!");
}
}
catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (SQLException e) {
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
} finally{
try {
//注意 rs、stmt、connection 三个对象的关闭顺序
if(rs != null) {
rs.close();
}
if(stmt != null) {
stmt.close();
}
if(connection != null) {
connection.close();
}
} catch (SQLException e) {
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
}
}
}
public static void main(String[] args){
writeImgToBlob();
}
}
编译运行:
cd demo
javac -d bin/ src/WriteImg.java
java -cp bin/:lib/mysql-connector-java-8.0.22.jar WriteImg
输出结果:
3,读取数据库中的图片
通过 ResultSet 的 getBinaryStream() 方法读取图片,然后通过 IO 流写入硬盘(src 根目录),详见程序清单 10.16。
import java.io.*;
import java.sql.*;
//package、import
public class ReadImg{
final static String DRIVER = "com.mysql.jdbc.Driver";
//数据库的实例名是 shiyanlou
final static String URL = "jdbc:mysql://localhost:3306/shiyanlou?useUnicode=true&characterEncoding=utf8";
final static String USERNAME = "root";
final static String PASSWORD = "";
static Connection connection = null;
static Statement stmt = null;
static ResultSet rs = null;
static PreparedStatement pstmt = null;
//从数据库读取图片
public static void readImgToBlob(){
try {
Class.forName(DRIVER);
connection = DriverManager.getConnection(URL, USERNAME, PASSWORD);
String sql = "select * from myPicture where id = ?" ;
pstmt = connection.prepareStatement(sql) ;
pstmt.setInt(1, 1);//id=1
rs = pstmt.executeQuery() ;
if(rs.next()){
//将图片从数据库中读取出,类型为 InputStream
InputStream imgIn = rs.getBinaryStream("img") ;
//通过 IO 流,将图片写到项目中(硬盘)
InputStream in = new BufferedInputStream(imgIn) ;
//将图片的输出路径设置为 src(相对路径),图片名为 myPic.png
OutputStream imgOut =new FileOutputStream("src/new_myPic.png");
OutputStream out = new BufferedOutputStream(imgOut) ;
int len = -1;
while( (len=in.read() )!=-1) {
out.write(len);
}
out.close();
imgOut.close();
in.close();
imgIn.close();
System.out.println("图片读取成功!");
}
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (SQLException e) {
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
} finally{
try {
//注意 rs、stmt、connection 三个对象的关闭顺序
if(rs != null) {
rs.close();
}
if(stmt != null) {
stmt.close();
}
if(connection != null) {
connection.close();
}
} catch (SQLException e) {
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
}
}
}
public static void main(String[] args){
readImgToBlob();
}
}
编译运行:
cd demo
javac -d bin/ src/ReadImg.java
java -cp bin/:lib/mysql-connector-java-8.0.22.jar ReadImg
输出结果:
此时可以看到在 ./src 下,生成了一个名为 new_myPic.png 的图片文件:
JDBC 总结
本章介绍了如何使用 JDBC 访问关系型数据库 MySQL,具体如下:
- JDBC API 包含了 Java 应用程序与各种不同数据库交互的标准接口,如
Connection
连接接口、Statement
操作接口、ResultSet
结果集接口、PreparedStatement
预处理操作接口等,使用这些JDBC
接口可以操作各种关系型数据库; - 使用 JDBC 访问数据库的基本步骤是:使用
Class.forName
(“驱动字符串”)加载驱动类、获取Connection
对象、使用Statement
对象(或PreparedStatement
对象)向数据库发送 SQL 语句,如果是查询操作还需要通过 ResultSet 对象获取结果集; - 与
Statement
相比较,PreparedStatement
有着如下的优势:提高了代码的可读性和可维护性、提高了 SQL 语句的性能、能有效防止 SQL 注入; - 可以使用
CallableStatement
对象的prepareCall()
方法调用数据库中的存储过程和存储函数,调用存储过程(无返回值)时,该方法参数的格式是{ call 存储过程名(参数列表) }
;调用存储函数(有返回值)时,该方法参数格式是{ ? = call 存储过程名(参数列表) }
; - 可以使用
PreparedStatement
和IO
流,在数据库中读写CLOB
或BLOB
类型的数据。例如要将图片写入myPicture
表的img
字段(BLOB 类型),就可以先将图片转为输入流,然后通过 PreparedStatement 对象的setBinaryStream()
方法写入数据库。