1.1 简介
JDBC 的全称是 Java Database Connectivity,即 Java 数据库连接,它是一种可以执行 SQL 语句的 Java API。程序可通过 JDBC API 连接到关系数据库,并使用结构化查询语言(SQL,数据库标准的查询语言)来完成对数据库的查询、更新。 与其他数据库编程环境相比,JDBC 为数据库开发提供了标准的 API,所以使用 JDBC 开发的数据库应用可以跨平台运行,而且可以跨数据库(如果全部使用标准的 SQL)。也就是说,如果使用 JDBC 开发一个数据库应用,则该应用既可以在 Windows 平台上运行,也可以在 UNIX 等其他平台上运行;既可以使用 MySQL 数据库,也可以使用 Oracle 等数据库,而程序无须进行任何修改。 最早的时候,Sun 公司希望自己开发一组 Java API,程序员通过这组 Java API 即可操作所有的数据库系统,但后来 Sun 发现这个目标具有不可实现性,因为数据库系统太多了,而且各数据库系统的内部特性又各不相同。后来 Sun 就制定了一组标准的 API,它们只是接口,没有提供实现类(这些实现类由各数据库厂商提供实现),这些实现类就是驱动程序。而程序员使用 JDBC 时只要面向标准的 JDBC API 编程即可,当需要在数据库之间切换时,只要更换不同的实现类(即更换数据库驱动程序)就行,这是面向接口编程
java.sql.Driver
接口是所有 JDBC 驱动程序需要实现的接口。这个接口是提供给数据库厂商使用的,不同数据库厂商提供不同的实现。在程序中不需要直接去访问实现了 Driver 接口的类,而是由驱动程序管理器类 java.sql.DriverManager
去调用这些 Driver 实现。常见的 Driver 接口实现
♞ Oracle 的驱动:oracle.jdbc.driver.OracleDriver
♞ MySQL 的驱动:com.mysql.jdbc.Driver
☞ DriverManager
用于管理 JDBC 驱动的服务类。程序中使用该类的主要功能是获取 Connection 对象。提供如下方法:
♞ static Connection getConnection(String url, String user, String pass)
:该方法获得 url 对应数据库的连接。
☞ Connection
代表数据库连接对象,每个 Connection 代表一个物理连接会话。要想访问数据库,必须先获得数据库连接。提供如下方法:
♞ Statement createStatement()
:该方法返回一个 Statement 对象。
♞ PreparedStatement prepareStatement(String sql)
:该方法返回预编译的 Statement 对象,即将 SQL 语句进行预编译。
♞ CallableStatement prepareCall(String sql)
:该方法返回 CallableStatement 对象,该对象用于调用存储过程。
上面三个方法都返回用于执行 SQL 语句的 Statement 对象,PreparedStatement、CallableStatement 是 Statement 的子类,只有获得了 Statement 之后才可执行 SOL 语句。Java7 为 Connection 新增了 setSchema(String schema)、getSchema() 两个方法,这两个方法用于控制该 Connection 访问的数据库 Schema。Java7 还为 Connection 新增了setNetworkTimeout(Executor executor,int milliseconds)、getNetworkTimeout() 两个方法来控制数据库连接的超时行为。除此之外,Connection 还有如下几个用于控制事务的方法。
♞ Savepoint setSavepoint()
:创建一个保存点。
♞ Savepoint setSavepoint(String name)
:以指定名字来创建一个保存点。
♞ void set Transactionlsolation(int level)
:设置事务的隔离级别。
♞ void rollback()
:回滚事务。
♞ void rollback(Savepoint savepoint)
:将事务回滚到指定的保存点。
♞ void setAutoCommit(boolean autoCommit)
:关闭自动提交,打开事务。
♞ void commit()
:提交事务。
☞ Statement
用于执行 SOL 语句的工具接口。该对象既可用于执行 DDL、DCL 语句,也可用于执行 DML 语句,还可用于执行 SQL 查询。当执行 SQL 查询时,返回查询到的结果集。它的常用方法如下:
♞ ResultSet executeQuery(String sql)
:该方法用于执行查询语句,并返回查询结果对应的 ResultSet 对象。该方法只能用于执行查询语句。
♞ int executeUpdate(String sql)
:该方法用于执行 DML 语句,并返回受影响的行数;该方法也可用于执行 DDL 语句,执行 DDL 语句将返回 0。
♞ boolean execute(String sql)
:该方法可执行任何 SQL 语句。如果执行后第一个结果为 ResultSet 对象,则返回 true;如果执行后第一个结果为受影响的行数或没有任何结果,则返回 false。
Java 7为 Statement 新增了closeOnCompletion() 方法,如果 Statement 执行了该方法,则当所有依赖于该 Statement 的 ResultSet 关闭时,该 Statement 会自动关闭。Java7 还为 Statement 提供了一个 isCloseOnCompletion() 方法,该方法用于判断该 Statement 是否打开了“closeOnCompletion”。 Java 8 为 Statement 新增了多个重载的 executeLargeUpdate() 方法,这些方法相当于增强版的 executeUpdate() 方法,返回值类型为 long,也就是说,当 DML 语句影响的记录条数超过 Integer.MAX_VALUE 时,就应该使用 executeLargeUpdate() 方法。
☞ PreparedStatement
预编译的 Statement 对象。PreparedStatement 是 Statement 的子接口,它允许数据库预编译 SQL 语句(这些 SQL 语句通常带有参数),以后每次只改变 SQL 命令的参数,避免数据库每次都需要编译 SQL 语句,因此性能更好。相对于 Statement 而言,使用 PreparedStatement 执行 SQL 语句时,无须再传入 SQL 语句,只要为预编译的 SQL 语句传入参数值即可。所以它比 Statement 多了如下方法:
♞ void setXxx(int parameterIndex,Xxx value)
:该方法根据传入参数值的类型不同,需要使用不同的方法。传入的值根据索引传给 SQL 语句中指定位置的参数。
PreparedStatement 同样有 executeUpdate()、executeQuery() 和 execute() 三个方法,只是这三个方法无须接收 SQL 字符串,因为 PreparedStatement 对象已经预编译了 SQL 命令,只要为这些命令传入参数即可。Java8 还为 PreparedStatement 增加了不带参数的 executeLargeUpdate() 方法,执行 DML 语句影响的记录条数可能超过 Integer.MAX_VALUE 时,就应该使用 executeLargeUpdate() 方法。
☞ ResultSet
结果集对象。该对象包含访问查询结果的方法,ResultSet 可以通过列索引或列名获得列数据。它包含了如下常用方法来移动记录指针。
♞ void close()
:释放 ResultSet 对象。
♞ boolean absolute(int row)
:将结果集的记录指针移动到第 row 行,如果 row 是负数,则移动到倒数第 row 行。如果移动后的记录指针指向一条有效记录,则该方法返回 true。
♞ void beforeFirst()
:将 ResultSet 的记录指针定位到首行之前,这是 ResultSet 结果集记录指针的初始状态,记录指针的起始位置位于第一行之前。
♞ boolean first()
:将 ResultSet 的记录指针定位到首行。如果移动后的记录指针指向一条有效记录,则该方法返回 true。
♞ boolean previous()
:将 ResultSet 的记录指针定位到上一行。如果移动后的记录指针指向一条有效记录,则该方法返回 true。
♞ boolean next()
:将 ResultSet 的记录指针定位到下一行,如果移动后的记录指针指向一条有效记录,则该方法返回 true。
♞ boolean last()
:将 ResultSet 的记录指针定位到最后一行,如果移动后的记录指针指向一条有效记录,则该方法返回 true。
♞ void afterLast()
:将 ResultSet 的记录指针定位到最后一行之后。
当把记录指针移动到指定行之后,ResultSet 可通过 getxxx(int columnlndex) 或 getXxx(String columnLabel) 方法来获取当前行、指定列的值,前者根据列索引获取值,后者根据列名获取值。Java7 新增了 T getObject(int columnIndex,Class type)和 T getObject(String columnLabel,Class type) 两个泛型方法,它们可以获取任意类型的值。
☞ 加载驱动
MySQL5 以后的 JDBC 驱动已经可以通过 SPI 自动注册驱动类了,在 JDBC 驱动 JAR 包的 META-INF\services
路径下会包含一个 java.sql.Driver
文件,该文件指定了 JDBC 驱动类。因此,如果使用这种新的驱动 JAR 包,这一步其实可以省略,但不推荐省略加载驱动。(需要导入 mysql-connector-java.jar)
Class.forName(driverClass)
//加载 MySql 驱动
Class.forName("com.mysql.jdbc.Driver")
//加载 Oracle 驱动
Class.forName("oracle.jdbc.driver.OracleDriver")
☞ 获取数据库连接
当使用 DriverManager 获取数据库连接时,通常需要传入三个参数:数据库 URL、登录数据库的用户名和密码。这三个参数中用户名和密码通常由 DBA(数据库管理员)分配,而且该用户还应该具有相应的权限,才可以执行相应的 SQL 语句。
// url ☞ jdbc:mysql://主机名称:mysql服务端口号/数据库名称?参数=值&参数=值
Connection connection = DriverManager.getConnection(url, username, password);
// 获取 MySQL 连接
// 如果JDBC程序与服务器端的字符集不一致,会导致乱码,那么可以通过参数指定服务器端的字符集
// jdbc:mysql://localhost:3306/mydatabase?useUnicode=true&characterEncoding=utf8
Connection connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/mydatabase", "root", "root");
☞ 创建 Statement 对象
// 执行的 sql 语句
String sql = "insert into ···";
// 创建 Statement 对象
Statement statement = connection.createStatement();
☞ 执行 SQL
ResultSet executeQuery(String sql)
:该方法只能用于执行查询语句。
int executeUpdate(String sql)
:该方法用于执行 DML 语句,也可用于执行 DDL 语句。
boolean execute(String sql)
:该方法可执行任何 SQL 语句,比较麻烦。
☞ 操作结果集
如果执行的 SQL 语句是查询语句,则执行结果将返回一个 ResultSet 对象,该对象里保存了 SQL 语句查询的结果。程序可以通过操作该 ResultSet 对象来取出查询结果。执行的 SQL 语句是增、删、改语句,则执行结果返回的是受影响的行数。行的 SQL 语句是权限操作语句,则执行结果返回的是 0。
☞ 释放资源
// 采用倒序关闭
resultSet.close();
statement.close();
connection.close();
☞ 使用 Statement
import java.sql.*;
/**
* Created with IntelliJ IDEA.
*
* @author Demo_Null
* @date 2020/7/9
* @description JDBC 演示类
*/
public class JDBCTest {
private static String url = "jdbc:mysql://47.103.4.*:3306/mydatabase";
private static String username = "root";
private static String password = "root";
private static String drive = "com.mysql.jdbc.Driver";
private static Connection connection = null;
static {
try {
// 加载驱动程序
Class.forName(drive);
// 获得数据库连接
connection = DriverManager.getConnection(url, username, password);
} catch (Exception e) {
e.printStackTrace();
}
}
private static void sql(String id) {
try {
// sql 语句
String sql = "select * from dept where id = " + id;
// 获取执行器
Statement statement = connection.createStatement();
// 执行 sql
ResultSet resultSet = statement.executeQuery(sql);
// 处理结果集
while (resultSet.next()) {
String dname = resultSet.getString("dname");
System.out.println(dname);
}
// 释放资源
resultSet.close();
statement.close();
connection.close();
} catch (SQLException throwables) {
throwables.printStackTrace();
}
}
public static void main(String[] args) {
sql(" 1 ");
}
}
运行上述代码,成功的从数据库中获取到了 id = 1 的 dname,但是我们使用的是 Statement,Statement 存在着一些弊端,他需要我们将参数与 SQL 拼接起来,十分繁琐,而且由于拼接会导致 SQL 注入的问题。SQL 注入是利用某些系统没有对用户输入的数据进行充分的检查,而在用户输入数据中注入非法的 SQL 语句段或命令,从而利用系统的 SQL 引擎完成恶意行为的做法。例如:我们只想返回 id 为 1 的 dname,但是用户传入了1 or 1 = 1
,结果 SQL 语句就变为了 select * from dept where id = 1 or 1 = 1
甚至修改为 select * from dept where id = 1; drop table dept;
☞ 使用 PreparedStatement
使用 PreparedStatement 执行 SQL,可以在 SQL 语句中,对值所在的位置使用 ?
占位符,实际的值,可以通过另外的方法传入。此时 PreparedStatement 会对值做特殊的处理,处理后,会导致恶意注入的 SQL 代码失效
import java.sql.*;
/**
* Created with IntelliJ IDEA.
*
* @author Demo_Null
* @date 2020/7/9
* @description JDBC 演示类
*/
public class JDBCTest {
private static String url = "jdbc:mysql://47.103.4.*:3306/mydatabase";
private static String username = "root";
private static String password = "root";
private static String drive = "com.mysql.jdbc.Driver";
private static Connection connection = null;
static {
try {
// 加载驱动程序
Class.forName(drive);
// 获得数据库连接
connection = DriverManager.getConnection(url, username, password);
} catch (Exception e) {
e.printStackTrace();
}
}
private static void sql(String id) {
try {
// sql 语句
String sql = "select * from dept where id = ?";
// 获取执行器
PreparedStatement preparedStatement = connection.prepareStatement(sql);
// 设置参数
preparedStatement.setString(1, id);
// 执行 sql
ResultSet resultSet = preparedStatement.executeQuery();
// 打印执行语句
System.out.println(preparedStatement.toString());
// 处理结果集
while (resultSet.next()) {
String dname = resultSet.getString("dname");
System.out.println(dname);
}
// 释放资源
resultSet.close();
preparedStatement.close();
connection.close();
} catch (SQLException throwables) {
throwables.printStackTrace();
}
}
public static void main(String[] args) {
sql(" 1 or 1 = 1 ");
}
}
每次使用 JDBC 都需要写冗长的代码,而且代码大部分都是相同的,我们可以将其封装为一个工具类,提高代码的复用性。其次,我们的 MySQL 参数都是写死在代码中,不利于维护,在集合中有一个 Properties 集合,它可以从文本中读取数据。根据该思路对现有 JDBC 操作进行优化。
driver = com.mysql.jdbc.Driver
url = jdbc:mysql://47.103.4.*:3306/mydatabase
user = work
password = mymima
import java.io.IOException;
import java.io.InputStream;
import java.sql.*;
import java.util.Properties;
public class jdbc_utils {
static String driver;
static String url;
static String user;
static String password;
static {
/** 【需要获取配置文件路径是使用这种方式】
* // 加载本类字节码文件进内存,获取统一资源定位符(URL)
* URL url = jdbc_utils.class.getClassLoader().getResource("jdbc.properties");
* // 获取 URL 路径
* String s = url.getParth();
* // 使用 File
* pro.load(new File(s));
*/
// 加载字节码文件获取配置文件流
InputStream is = jdbc_utils.class.getClassLoader().getResourceAsStream("jdbc.properties");
try {
// 创建 properties 集合
Properties pro = new Properties();
// 从文件中加载 key-value 对
pro.load(is);
// 获取属性并复制
driver = pro.getProperty("driver");
url = pro.getProperty("url");
user = pro.getProperty("user");
password = pro.getProperty("password");
// 加载驱动
Class.forName(driver);
} catch (Exception e) {
e.printStackTrace();
}
}
// 获取数据库连接
public static Connection getConnection() throws SQLException {
return DriverManager.getConnection(url,user,password);
}
// 释放资源
// DML 释放两个资源
public static void close(PreparedStatement preparedStatement, Connection connection) {
if (preparedStatement != null) {
try {
preparedStatement.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if (connection != null) {
try {
connection.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
// DQL 释放三个资源
public static void close(ResultSet resultSet, PreparedStatement preparedStatement, Connection connection) {
if (resultSet != null) {
try {
resultSet.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if (preparedStatement != null) {
try {
preparedStatement.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if (connection != null) {
try {
connection.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}