Detailed explanation of MySQL JDBC

Article directory


Introduction

On top of JDBC are Java applications written by developers themselves, as shown in the figure below. Java applications can access databases through the JDBC API integrated in the java.sql and javax.sql packages of the JDK.
insert image description here
The following explains some important JDBC components that appear in the above figure, and the mind map is as follows.

insert image description here

JDBC API

The JDBC API exists in the JDK, which contains standard interfaces for Java applications to interact with various databases, such as Connection is the interface for connecting to the database, Statement is the interface for operating the database, ResultSet is the query result set interface, and PreparedStatement is the preprocessing operation Interfaces, etc., developers can use these JDBC interfaces to operate relational databases. The common interfaces and classes in the JDBC API are introduced in the following table:

interface/class Introduction
DriverManager class According to different databases, manage the corresponding JDBC drivers. You can get the database connection object (that is, the Connection object) through the getConnection() method of the DriverManager class.
Connection interface Produced by DriverManager, it is used to connect to the database and transfer data.
Statement interface Produced by Connection, it is used to execute SQL statements such as addition, deletion, modification, and query.
PreparedStatement interface A subinterface of Statement (the definition of this interface is: public interface PreparedStatement extends Statement{…}). PreparedStatement is also generated by Connection, and is also used to execute SQL statements such as addition, deletion, modification, and query. Compared with the Statement interface, Statement has the advantages of higher security (it can prevent security risks such as SQL injection), higher performance, higher readability and maintainability.
CallableStatement interface The sub-interface of PreparedStatement (the definition of this interface is: public interface CallableStatement extends PreparedStatement {…}), CallableStatement is also generated by Connection, and is used to call stored procedures or stored functions.
ResultSet interface Receive the result set returned after the Statement object (or PreparedStatement object) executes the query operation.

From a development perspective, the JDBC API mainly accomplishes three things:

  1. Establish a connection with the database
  2. Send SQL statement to database
  3. Return the processing result of the database

insert image description here

JDBC Driver Manager

The JDBC Driver Manager also exists in the JDK and is responsible for managing JDBC drivers for various databases.

JDBC driver

JDBC drivers are provided by various database vendors or third-party vendors, and are responsible for implementing JDBC APIs for different databases. For example, when an application accesses MySql and Oracle, different JDBC drivers are required. Each of these JDBC drivers implements various interfaces defined in the JDBC API. When using JDBC to connect to the database, as long as the JDBC driver is correctly loaded, the database can be operated by calling the JDBC API.

JDBC development steps

To develop a JDBC program, there are four basic steps:

1. Import the JDBC driver package and load the driver class

Before using JDBC to access the database, you need to import the corresponding driver package (for example, the driver package of the oracle database is ojdbc version number.jar). Here we take MySQL as an example to introduce the steps of importing the driver package in WebIDE:

  1. Create a new java project directory, and all the codes in this experiment will also run in the form of projects, and all the codes will be placed under demo/src:
mkdir demo demo/src demo/lib demo/bin
  1. Download the driver package and move it to the lib directory of the project:\
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

Note: You can also download the driver package from the official website of MySQL: Connector/J

After the driver package is placed in the lib directory, you can use the Class.forName() method to load the specific JDBC driver class into the JVM. The loaded code is as follows:

Class.forName("JDBC 驱动类名");

If the specified driver class name does not exist, a ClassNotFoundException will be thrown.

Then in the code, you can use parameters such as connection string, user name and password to get the database connection object. The JDBC driver package name, driver class name and connection string of common relational databases are shown in the following table:

database JDBC driver package JDBC driver class connection string
Oracle ojdbc version number.jar oracle.jdbc.OracleDriver jdbc:oracle:thin:@localhos t:1521: database instance name
MySQL mysql-connector-java-version-bin.jar com.mysql.jdbc.Driver jdbc:mysql://localhost:3306/database instance name
SqlServer sqljdbc version number.jar com.microsoft.sqlserver.jdbc.SQLServerDriver jdbc:microsoft:sqlserver://localhost:1433; databasename=database instance name

The connection string is composed of protocol, server address, port and database instance name. In the example, localhost can be replaced with the server's ip address. 1521, 3306 and 1433 are the default port numbers of Oracle, MySQL and SqlServer databases respectively.

When the program calls Class.forName("JDBC driver class name"); When using the java command to run the program, you need to use the following command to add it to the classpath:

javac -d bin/ src/JDBCUpdateByStatement.java
java -cp bin/:lib/mysql-connector-java-8.0.22.jar JDBCUpdateByStatement

Second, establish a database connection

JDBC uses the DriverManager class to manage the driver, and obtains the connection object through its getConnection(), the code is as follows:

Connection connection = DriverManager.getConnection("连接字符串","数据库用户名","数据库密码");

Common methods of the Connection interface are shown in the following table:

method Introduction
Statement createStatement() throws SQLException Create a Statement object
PreparedStatement prepareStatement(String sql) Create a PreparedStatement object

3. Send the SQL statement and get the execution result

After obtaining the Connection object, you can obtain the Statement or PreparedStatement object through the Connection object, and send SQL statements to the database through the object.

Statement object

//创建 Statement 对象
Statement stmt = connection.createStatement();

Send SQL statements of the "add, delete, modify" type:

int count = stmt.executeUpdate("增、删、改的 SQL 语句")

Send an SQL statement of type "query":

ResultSet rs = stmt.executeQuery("查询的 SQL 语句");

If the SQL statement is an add, delete, or change operation, an int result will be returned, indicating how many rows have been affected, that is, how many pieces of data have been added, deleted, or changed; if the SQL statement is a query operation, the database will return a ResultSet result set , the result set contains all the results of the SQL query.

Common methods of the Statement object are shown in the table below:

method Introduction
int executeUpdate() Used to execute INSERT, UPDATE, DELETE, and DDL (Data Definition Language) statements such as CREATE TABLE… and DROP TABLE…. For DDL-type statements such as CREATE TABLE or DROP TABLE, the return value of executeUpdate is always zero.
ResultSet executeQuery() It is used to execute SELECT query statement, and the return value is a result set of ResultSet type.
void close() Closes the Statement object.

For DDL-type statements such as CREATE TABLE or DROP TABLE, the return value of executeUpdate is always zero.

ResultSet executeQuery() is used to execute SELECT query statement, and the return value is a result set of ResultSet type.

void close() Closes the Statement object.

PreparedStatement object

//创建 PreparedStatement 对象
PreparedStatement pstmt = connection.prepareStatement("增、删、改、查的 SQL 语句");

Send "add, delete, change" type of SQL statement:

int count = pstmt.executeUpdate()

Send "query" type SQL statement:

ResultSet rs = pstmt.executeQuery();

Common methods of the PreparedStatement object are shown in the following table:

method Introduction
executeUpdate() In terms of usage, it is similar to executeUpdate() in the Statement interface.
executeQuery() In terms of usage, it is similar to executeQuery () in the Statement interface.
setXxx() There are multiple methods such as setInt(), setString(), setDouble(), etc., which are used to assign values ​​to the placeholder "?" in SQL. The setXxx() method has two parameters, the first parameter indicates the position of the placeholder (starting from 1), and the second parameter indicates the specific value represented by the placeholder. For example, SQL can be written as "select * from student where name=? To assign values ​​to the two placeholders respectively (ie assign values ​​to name and age)
close() Close the PreparedStatement object

Fourth, process the returned result set

If it is a query operation, you can iteratively retrieve all the data in the result set: first, use rs.next() to determine whether there is still the next row of data. If so, rs will move to the next row, and then pass rs.getXxx () Get the data of each column in the row, as follows:

while(rs.next()) {
    int stuNo = rs.getInt("stuNo");
    String stuName = rs.getString("stuName");
}

Common methods of ResultSet are shown in the table below:

method Introduction
boolean next() Move the cursor one row down from the current position, pointing to the next row of data in the result set. It is usually used to judge whether there is any data in the query result set. Returns true if there is one, false otherwise.
boolean previous() Move the cursor up one line from the current position.
int getInt(int columnIndex) Get the field value of the specified column number in the current row of data, and the column must be an integer type field. For example, if there is a stuNo field of type number in the first column in the student table, you can use getInt(1) to get the value. In addition, there are multiple similar methods such as getFloat(), getString(), getDate(), getBinaryStream(), etc., which are used to obtain different types of fields.
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

输出结果:
insert image description here

以上是增加方法的执行细节,如果要执行删除操作,只需要修改上述代码中 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

输出结果:
insert image description here

如果是根据 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

输出结果:
insert image description here

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

输出结果:
insert image description here

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

输出结果:
insert image description here

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

输出结果:
insert image description here

此时可以看到在 ./src 下,生成了一个名为 new_introduce.txt 的文件:
insert image description here

二,读写 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

输出结果:
insert image description here

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

输出结果:
insert image description here
此时可以看到在 ./src 下,生成了一个名为 new_myPic.png 的图片文件:
insert image description here

JDBC 总结

本章介绍了如何使用 JDBC 访问关系型数据库 MySQL,具体如下:

  1. JDBC API 包含了 Java 应用程序与各种不同数据库交互的标准接口,如 Connection 连接接口、Statement 操作接口、ResultSet 结果集接口、PreparedStatement 预处理操作接口等,使用这些 JDBC 接口可以操作各种关系型数据库;
  2. 使用 JDBC 访问数据库的基本步骤是:使用 Class.forName(“驱动字符串”)加载驱动类、获取 Connection 对象、使用 Statement 对象(或 PreparedStatement 对象)向数据库发送 SQL 语句,如果是查询操作还需要通过 ResultSet 对象获取结果集;
  3. Statement 相比较,PreparedStatement 有着如下的优势:提高了代码的可读性和可维护性、提高了 SQL 语句的性能、能有效防止 SQL 注入;
  4. 可以使用 CallableStatement 对象的 prepareCall() 方法调用数据库中的存储过程和存储函数,调用存储过程(无返回值)时,该方法参数的格式是 { call 存储过程名(参数列表) } ;调用存储函数(有返回值)时,该方法参数格式是 { ? = call 存储过程名(参数列表) }
  5. You can use PreparedStatementand streams to read and write data of type orIO in the database . For example, if you want to write a picture into the field (BLOB type) of the table, you can first convert the picture into an input stream, and then write it into the database through the method .CLOBBLOBmyPictureimgsetBinaryStream()

Guess you like

Origin blog.csdn.net/m0_62617719/article/details/130960930
Recommended