MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Java JDBC中的PreparedStatement使用

2021-11-146.7k 阅读

Java JDBC中的PreparedStatement使用

一、什么是PreparedStatement

在Java的数据库连接(JDBC)编程中,PreparedStatementStatement 接口的子接口。Statement 用于执行静态SQL语句,而 PreparedStatement 则允许执行预编译的SQL语句。预编译意味着SQL语句在被发送到数据库之前,会在数据库驱动程序中进行编译和优化。这种机制带来了许多好处,包括提高性能、增强安全性以及代码的可维护性。

从本质上讲,PreparedStatement 将SQL语句中的参数部分使用占位符(通常用 ? 表示)代替。在执行时,通过方法如 setXXX 来为这些占位符设置具体的值。这样,数据库就可以对SQL语句进行优化,因为其结构在编译时就已经确定,而参数值可以在后续动态传入。

二、为什么使用PreparedStatement

  1. 提高性能 当一个SQL语句需要被多次执行时,PreparedStatement 的性能优势就会突显出来。由于SQL语句已经预编译,数据库可以对其执行计划进行缓存。每次执行时,只需传入不同的参数值,而不需要重复编译SQL语句。例如,在一个批量插入数据的场景中,如果使用 Statement,每插入一条数据都需要重新编译SQL语句,而 PreparedStatement 只需要编译一次。

  2. 增强安全性 PreparedStatement 可以有效防止SQL注入攻击。SQL注入是一种常见的网络攻击手段,攻击者通过在输入字段中注入恶意的SQL代码,从而破坏数据库或获取敏感信息。在 Statement 中,如果直接将用户输入拼接到SQL语句中,就很容易受到这种攻击。而 PreparedStatement 使用占位符,数据库驱动会对传入的参数进行正确的转义和处理,从而避免了SQL注入的风险。

  3. 代码可维护性 使用 PreparedStatement 使得SQL语句和参数值分离,代码结构更加清晰。当需要修改SQL语句的逻辑或者参数值时,更容易定位和修改,提高了代码的可维护性。

三、PreparedStatement的使用步骤

  1. 获取数据库连接 首先,需要使用 DriverManager 类获取一个数据库连接。以下是获取MySQL数据库连接的示例代码:
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;

public class DatabaseUtil {
    private static final String URL = "jdbc:mysql://localhost:3306/mydb";
    private static final String USER = "root";
    private static final String PASSWORD = "password";

    public static Connection getConnection() throws SQLException {
        return DriverManager.getConnection(URL, USER, PASSWORD);
    }
}
  1. 创建PreparedStatement对象 通过数据库连接对象的 prepareStatement 方法来创建 PreparedStatement 对象。SQL语句作为参数传入该方法,其中包含占位符。例如,要插入一条用户记录:
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;

public class UserDao {
    public void insertUser(String username, String password) {
        String sql = "INSERT INTO users (username, password) VALUES (?,?)";
        try (Connection conn = DatabaseUtil.getConnection();
             PreparedStatement pstmt = conn.prepareStatement(sql)) {
            pstmt.setString(1, username);
            pstmt.setString(2, password);
            pstmt.executeUpdate();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}

在上述代码中,prepareStatement 方法接收SQL语句 "INSERT INTO users (username, password) VALUES (?,?)",这里的两个 ? 就是占位符。

  1. 设置参数值 PreparedStatement 提供了一系列 setXXX 方法来为占位符设置具体的值。这些方法的第一个参数是占位符的索引,从1开始计数,第二个参数是要设置的值。例如,setString(int parameterIndex, String x) 用于设置字符串类型的参数,setInt(int parameterIndex, int x) 用于设置整数类型的参数等。

  2. 执行SQL语句 PreparedStatement 提供了几种执行SQL语句的方法,主要有:

  • executeUpdate():用于执行INSERT、UPDATE、DELETE等修改数据库数据的SQL语句。该方法返回一个整数,表示受影响的行数。
  • executeQuery():用于执行SELECT语句,返回一个 ResultSet 对象,用于遍历查询结果。
  • execute():通用的执行方法,可用于执行任何SQL语句。返回一个布尔值,表示执行结果是否为 ResultSet。如果返回 true,则可以通过 getResultSet() 方法获取结果集;如果返回 false,则可以通过 getUpdateCount() 方法获取受影响的行数。

四、PreparedStatement的具体使用示例

  1. 插入数据 假设我们有一个 students 表,结构如下:
CREATE TABLE students (
    id INT AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(50),
    age INT
);

下面是使用 PreparedStatement 插入数据的Java代码:

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;

public class StudentDao {
    public void insertStudent(String name, int age) {
        String sql = "INSERT INTO students (name, age) VALUES (?,?)";
        try (Connection conn = DatabaseUtil.getConnection();
             PreparedStatement pstmt = conn.prepareStatement(sql)) {
            pstmt.setString(1, name);
            pstmt.setInt(2, age);
            int rowsAffected = pstmt.executeUpdate();
            System.out.println(rowsAffected + " 条记录插入成功");
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}
  1. 更新数据 要更新 students 表中某个学生的年龄:
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;

public class StudentDao {
    public void updateStudentAge(int id, int newAge) {
        String sql = "UPDATE students SET age =? WHERE id =?";
        try (Connection conn = DatabaseUtil.getConnection();
             PreparedStatement pstmt = conn.prepareStatement(sql)) {
            pstmt.setInt(1, newAge);
            pstmt.setInt(2, id);
            int rowsAffected = pstmt.executeUpdate();
            System.out.println(rowsAffected + " 条记录更新成功");
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}
  1. 删除数据 删除 students 表中某个学生的记录:
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;

public class StudentDao {
    public void deleteStudent(int id) {
        String sql = "DELETE FROM students WHERE id =?";
        try (Connection conn = DatabaseUtil.getConnection();
             PreparedStatement pstmt = conn.prepareStatement(sql)) {
            pstmt.setInt(1, id);
            int rowsAffected = pstmt.executeUpdate();
            System.out.println(rowsAffected + " 条记录删除成功");
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}
  1. 查询数据 查询 students 表中所有学生的记录:
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

public class StudentDao {
    public void getAllStudents() {
        String sql = "SELECT id, name, age FROM students";
        try (Connection conn = DatabaseUtil.getConnection();
             PreparedStatement pstmt = conn.prepareStatement(sql);
             ResultSet rs = pstmt.executeQuery()) {
            while (rs.next()) {
                int id = rs.getInt("id");
                String name = rs.getString("name");
                int age = rs.getInt("age");
                System.out.println("ID: " + id + ", Name: " + name + ", Age: " + age);
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}

在查询示例中,通过 executeQuery() 方法执行SQL语句,返回 ResultSet 对象。然后使用 ResultSetnext() 方法逐行遍历结果集,并通过 getXXX 方法获取每列的值。

五、处理复杂类型参数

  1. 日期类型 如果数据库表中有日期类型的字段,例如 birth_date,可以使用 PreparedStatementsetDate 方法来设置日期参数。假设 students 表增加了 birth_date 字段:
ALTER TABLE students ADD COLUMN birth_date DATE;

插入带有日期的学生记录:

import java.sql.Connection;
import java.sql.Date;
import java.sql.PreparedStatement;
import java.sql.SQLException;

public class StudentDao {
    public void insertStudentWithDate(String name, int age, Date birthDate) {
        String sql = "INSERT INTO students (name, age, birth_date) VALUES (?,?,?)";
        try (Connection conn = DatabaseUtil.getConnection();
             PreparedStatement pstmt = conn.prepareStatement(sql)) {
            pstmt.setString(1, name);
            pstmt.setInt(2, age);
            pstmt.setDate(3, birthDate);
            int rowsAffected = pstmt.executeUpdate();
            System.out.println(rowsAffected + " 条记录插入成功");
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}

这里使用 java.sql.Date 类型来表示数据库中的日期。如果要将 java.util.Date 转换为 java.sql.Date,可以使用 new java.sql.Date(java.util.Date.getTime())

  1. 二进制类型 对于二进制数据,如图片、文件等,可以使用 setBlob 方法。假设 students 表增加了 photo 字段来存储学生照片(以二进制形式):
ALTER TABLE students ADD COLUMN photo BLOB;

插入带有照片的学生记录:

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;

public class StudentDao {
    public void insertStudentWithPhoto(String name, int age, File photoFile) {
        String sql = "INSERT INTO students (name, age, photo) VALUES (?,?,?)";
        try (Connection conn = DatabaseUtil.getConnection();
             PreparedStatement pstmt = conn.prepareStatement(sql)) {
            pstmt.setString(1, name);
            pstmt.setInt(2, age);
            if (photoFile != null) {
                try (FileInputStream fis = new FileInputStream(photoFile)) {
                    pstmt.setBlob(3, fis);
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            int rowsAffected = pstmt.executeUpdate();
            System.out.println(rowsAffected + " 条记录插入成功");
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}

在上述代码中,通过 FileInputStream 将文件内容读取并设置为 Blob 类型的参数。

六、批量操作

PreparedStatement 支持批量操作,这在需要执行多个相同SQL语句时非常有用,可以显著提高性能。例如,批量插入学生记录:

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;

public class StudentDao {
    public void batchInsertStudents(String[] names, int[] ages) {
        String sql = "INSERT INTO students (name, age) VALUES (?,?)";
        try (Connection conn = DatabaseUtil.getConnection();
             PreparedStatement pstmt = conn.prepareStatement(sql)) {
            for (int i = 0; i < names.length; i++) {
                pstmt.setString(1, names[i]);
                pstmt.setInt(2, ages[i]);
                pstmt.addBatch();
            }
            pstmt.executeBatch();
            System.out.println("批量插入成功");
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}

在上述代码中,通过 addBatch() 方法将每次设置好参数的SQL语句添加到批量执行队列中,最后通过 executeBatch() 方法一次性执行所有语句。这样可以减少数据库交互次数,提高效率。

七、错误处理

在使用 PreparedStatement 时,可能会遇到各种异常,最常见的是 SQLException。应该在代码中正确处理这些异常,以确保程序的健壮性。例如:

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;

public class StudentDao {
    public void insertStudent(String name, int age) {
        String sql = "INSERT INTO students (name, age) VALUES (?,?)";
        try (Connection conn = DatabaseUtil.getConnection();
             PreparedStatement pstmt = conn.prepareStatement(sql)) {
            pstmt.setString(1, name);
            pstmt.setInt(2, age);
            pstmt.executeUpdate();
        } catch (SQLException e) {
            System.err.println("插入数据时发生错误: " + e.getMessage());
            e.printStackTrace();
        }
    }
}

catch 块中,首先打印错误信息,然后调用 printStackTrace() 方法输出详细的异常堆栈跟踪信息,以便于调试和定位问题。

八、与Statement的比较

  1. 性能 正如前面提到的,PreparedStatement 在多次执行相同SQL语句时性能更好,因为它的预编译机制使得数据库可以缓存执行计划。而 Statement 每次执行都需要重新编译SQL语句,尤其是在批量操作时,性能差距更为明显。

  2. 安全性 Statement 存在SQL注入风险,因为它直接将用户输入拼接到SQL语句中。而 PreparedStatement 使用占位符,有效避免了这种风险,安全性更高。

  3. 代码可读性和可维护性 PreparedStatement 将SQL语句和参数分离,代码结构更清晰,当SQL语句或参数需要修改时更容易定位和修改,提高了代码的可读性和可维护性。而 Statement 的SQL语句中嵌入参数值,使得代码阅读和维护相对困难。

九、总结(此处虽不需要总结,但为结构完整性提及要点)

PreparedStatement 是Java JDBC编程中一个强大而重要的工具。通过使用预编译的SQL语句、占位符以及各种设置参数和执行方法,它提供了高性能、高安全性和良好的代码可维护性。无论是简单的数据操作还是复杂的批量处理和复杂类型参数处理,PreparedStatement 都能胜任。在实际的数据库应用开发中,应该优先选择 PreparedStatement 来进行数据库操作,以确保程序的质量和稳定性。同时,正确处理异常和合理使用批量操作等特性也是充分发挥 PreparedStatement 优势的关键。