JDBC
记录 JDBC 相关知识
学习笔记,仅供参考
JDBC 指 Java database connectivity,它是一个 Java api 即允许用户通过 java 来访问操作多种关系型数据库的一套 规范。
✋ 上手体验
Java 中对应的 api 为 java.sql
相当于布料,而各大数据库就相当于服装厂,生产各种服装。为了能让布料运到各大服装厂,中间又有了 mysql-connector-java
(本文数据库以 MySQL 为例),相当于物流运输。
public class Test1 {
public static void main(String[] args) {
try {
// 1. 加载驱动程序 --> 订布料,MySQL 公司的
Class.forName("com.mysql.jdbc.Driver");
// 2. 获取数据库连接 --> 送货,走啥路线
Connection connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/frank_school",
"root", "123456"); // 对应参数为 URL, USER, PASSWORD
// 3. 获取数据库操作对象 --> 布料送到了,卸货
Statement statement = connection.createStatement();
// 4. 执行 sql 语句,操作数据(以查询为例) --> 服装厂开始生产各种服装
// 执行查询语句,并用结果集接收查询结果
ResultSet res = statement.executeQuery("select * from user");
// 打印结果集
while (res.next()) { // 分别获取每行结果的第一、第二、第三列数据
int id = res.getInt(1);
String name = res.getString(2);
int age = res.getInt(3);
System.out.println("Id: " + id +
"Name: " + name +
"Age: " + age);
}
// close stream
statement.close();
connection.close();
} catch (Exception e) {
e.printStackTrace();
}
statement.close();
}
}
通过上面程序,大致就能够明白 JDBC 操作数据的过程。
先通过类加载器来加载 Driver;
再通过 DriverManager 来获取 connection;
接着由 connection 来获取数据库的操作对象 statement
最后再调用 statement 的
statementExecuteQuery("sql")
完成查询、statementExecuteUpdate("sql")
完成增删改
🎨 整理优化
为了让 JDBC 的程序更加合理,还需对上述代码进行整理修改。如:
将常量(URL、USER、PASSWORD、DRIVER)和接收变量(connection、statement、res)提前声明定义好
将整个过程都用 try-catch-finally 包裹起来
在 finally 中再次用 try-catch 将变量的关流包起来
下面为整理后的程序
import java.sql.DriverManager;
import java.sql.Connection;
import java.sql.Statement;
import java.sql.ResultSet;
public class Test2 {
// 定义常量
private static final String DRIVER = "com.mysql.jdbc.Driver";
private static final String URL = "jdbc:mysql://localhost:3306/frank_school";
private static final String USER = "root";
private static final String PASSWORD = "123456";
// 定义变量
private static Connection connection;
private static Statement statement;
private static ResultSet res;
public static void main(String[] args) {
try {
// 1. 加载驱动程序
Class.forName("DRIVER");
// 2. 获取数据库连接
connection = DriverManager.getConnection(URL, USER, PASSWORD);
// 3. 获取数据库操作对象
statement = connection.createStatement();
// 4. 执行 sql 语句,获取结果
res = statement.executeQuery("select * from user");
// 打印结果集
while (res.next()) {
int id = res.getInt(1);
String name = res.getString(2);
int age = res.getInt(3);
System.out.println("Id: " + id +
"Name: " + name +
"Age: " + age);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
res.close();
statement.close();
connection.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
🚩 规范操作
为了让程序编写的更加规范,还可以将 JDBC 的前置工作,如定义 jdbc 所用到的常量、获取驱动程序等封装为一个 JDBCUtils 工具类,这样能够减少代码量。而且为了让数据读取更加方便、安全,还需要将常量放在 db.properties
的配置文件中,其中数据是以键值对的形式存储(如:user=root)
★ db.properties 文件内容
driver=com.mysql.jdbc.Driver
url=jdbc:mysql://localhost:3306/frank_school?characterEncoding=utf-8
user=root
password=123456
// JDBCUtil 工具类的内容
import java.sql.DriverManager;
import java.sql.Connection;
import java.sql.Statement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.io.InputStream;
import java.io.IOException;
import java.util.Properties;
public class JDBCUtil {
// 定义接收配置项的变量
private static String driver;
private static String url;
private static String user;
private static String password;
// 通过静态代码块预先读取配置文件中的配置项
static {
// 导入要读取的文件流
InputStream inputStream = ClassLoader.getSystemResourceAsStream("db.properties");
Properties properties = new Properties();
// 加载文件流
properties.load(inputStream);
// 获取配置项
driver = properties.getProperty("driver");
url = properties.getProperty("url");
user = properties.getProperty("user");
password = properties.getProperty("password");
}
/**
* 单例获取数据库连接
*/
public static Connection getConnection() throw SQLException {
return DriverManager.getConnection(url, user, password);
}
/**
* 给出 close() 的方法,关闭连接,并且重载来对应不同的数据库操作
*/
public static void close(Connection connection, Statement statement, ResultSet resultSet) {
try {
resultSet.close();
statement.close();
connection.close();
} catch (Exception e) {
e.printStackTrace();
}
}
public static void close(Connection connection, Statement statement) {
try {
statement.close();
connection.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
// JDBC 操作数据库
import java.sql.Connection;
import java.sql.Statement;
import java.sql.ResultSet;
public class JDBCTest {
private static Connection connection;
private static Statement statement;
private static ResultSet res;
public static void main(String[] args) {
try {
// 获取数据库连接
connection = JDBCUtil.getConnection();
// 获取数据库操作对象
statement = connection.createStatement();
// 执行 sql 语句
res = statement.ExecuteQuery("select * from user");
// 打印结果集
while (res.next()) {
int id = res.getInt(1);
String name = res.getString(2);
int age = res.getInt(3);
System.out.println("Id: " + id +
"Name: " + name +
"Age: " + age);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
JDBCUtil.close(connection, statement, res);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
下面对 .properties 文件及 Java 中的相关方法做简要说明,它主要用于 Java 相关的技术中,为了能存储应用的配置参数。其中每个参数都以字符串对的形式存储,一端是参数名(Key),另一端是参数值(Value)。参数的书写最好一行一个,且文件的编码在 Java 9 之后采用 UTF-8 的编码格式。
另外,像 XML、JSON、YAML 等配置文件更受欢迎,它们能够完成更复杂的配置
格式
key=value
key = value
key:value
key value
# 井号开头表注释
! 或叹号开头
从上面代码中可了解到,Properties 位于 java.util.Properties
包下,它能够将配置项加载到流中或者从流中读取配置项。因为它继承自 HashTable,所以能够调用 put 和 putAll 方法来添加数据,但并不鼓励这样做,而是用 setProperty 方法来代替,这能保证当配置项为非字符串时添加失败。
当然它还能像 HashMap 那样,通过 entrySet(), keySet(), values()
来返回一个迭代器,从而遍历配置项。
💉 SQL 注入
一般用户在前端页面提交表单时,程序总要获取表单的信息,然后通过 JDBC 执行对应的 sql 语句来操作数据库。比如说,通过用户名来查看用户信息
// 前端表单
Username: Tom
// 获取表单信息
username = getRequestString("username");
// 编写 sql 语句
sql = "select * from user where username =\"" +
username + "\"";
// 对应的 sql 语句
// SELECT * FROM user WHERE username="Tom";
这样处理看似很正常,但却有着潜在的危险。在这种情况下,黑客会钻 sql 语句的空子,从而绕过检查获取用户信息。
// 1. 利用 "1=1" 保证恒为真
// 前端表单
UserId: 1 OR 1=1
// 获取表单信息
id = getRequestString("UserId");
// 编写 sql 语句
sql = "select * from user where id =" +
id + "";
// 对应的 sql 语句
// SELECT * FROM user WHERE id=1 OR 1=1;
// 该 sql 相当于直接执行了 SELECT * FROM user;
// 直接获取到所有用户的信息
/**********************分割线************************/
// 2. 利用 "="
// 前端表单
Username: "or""="
Password: "or""="
// 获取表单信息
username = getRequestString("Username");
password = getRequestString("Password");
// 编写 sql 语句
sql = "select * from user where username =\"" +
username + "\" and password=\"" +
password + "\"";
// 对应的 sql 语句
// SELECT * FROM user WHERE username=""or""
// and password=""="";
// 也相当执行 SELECT * FROM user;
对于这种直接将用户的输入作为 sql 语句的参数,将会引起 SQL 注入从而数据库数据泄露或毁坏。于是 JDBC 中就有了 PreparedStatement
类,以此来防止 SQL 注入。
🌂 PreparedStatement
PreparedStatement 属于 java.sql.PreparedStatement
包,它的对象表示一个预先编译的 SQL 语句,SQL 语句会预先编译好然后再存放在该对象中,该对象能够被用来多次高效地执行 SQL 语句。PreparedStatement 的用法与 Statement 会有些许不同,具体看下面的例子
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.util.Scanner;
public class JDBCTest {
private static Connection con;
private static PreparedStatement preparedStatement;
private static ResultSet res;
private static Scanner scanner = new Scanner();
public static void main(String[] args) {
try {
// 与用户交互,获取参数
System.out.println("请输入用户 ID 来查询用户信息:");
int userId = scanner.nextInt();
// 编写 SQL 语句, 使用问号占位符暂时代替具体参数
String sql = "select * from user where id=?";
// 获取数据库连接,并获取preparedStatement对象
con = JDBCUtil.getConnection();
preparedStatement = con.prepareStatement(sql);
// 设置参数, 其中 1 指第一个占位符,不同的参数类型要使用
// 不同的方法。如整数用 setInt,字符串用 setString 等
preparedStatement.setInt(1, userId);
// 执行 sql 语句, 并获取结果集
res = preparedStatement.executeQuery();
// 打印结果集
while (res.next()) {
int id = res.getInt(1);
String name = res.getString(2);
int age = res.getInt(3);
System.out.println("Id: " + id +
"Name: " + name +
"Age: " + age);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
scanner.close();
JDBCUtil.close(con, preparedStatement, res);
}
}
}
从上面代码可看出,在获取 preparedStatement 时就要传入 sql 语句,且使用的是 prepareStatement(sql)
方法,然后再通过 setter 来设置对应占位符的参数,最后再执行 sql 语句。与 Statement 相同的是,执行 sql 语句所用的方法名是一样的。