1、JDBC和JDBC驱动的基本概念
JDBC(Java DataBase Connectivity),指 Java 数据库连接,是一种标准Java应用编程接口(JAVA API),是 Java 语言用来连接和操作数据库的。使用Java程序访问数据库时,Java代码并不是直接通过TCP连接去访问数据库,而是通过JDBC接口来访问。而JDBC接口则通过JDBC驱动来实现真正对数据库的访问。
实际上,JDBC 就是官方定义的一套操作所有关系型数据库的规则,也就是接口。而 JDBC 驱动就是实现了这些接口的实现类,各个数据库厂商去实现这些接口,也就是 JDBC 驱动,提供数据库驱动 jar 包,我们可以使用这套接口(JDBC)编程,真正执行的代码是驱动 jar 包中的实现类。
JDBC 是一套接口规范,它在Java的标准库java.sql
里放着,不过这里面大部分都是接口。接口并不能直接实例化,而是必须实例化对应的实现类,然后通过接口引用这个实例。JDBC接口的实现类就是 JDBC 驱动。JDBC接口并不知道我们要使用哪个数据库,所以,用哪个数据库,我们就去使用哪个数据库的“实现类”,我们把某个数据库实现了JDBC接口的jar包称为 JDBC 驱动。
例如,我们在Java代码中要访问MySQL,那么必须编写代码操作JDBC接口。注意到JDBC接口是Java标准库自带的,所以可以直接编译。而具体的JDBC驱动是由数据库厂商提供的,例如,MySQL的JDBC驱动由Oracle提供。因此,访问某个具体的数据库,我们只需要引入该厂商提供的JDBC驱动,就可以通过JDBC接口来访问,这样保证了Java程序编写的是一套数据库访问代码,却可以访问各种不同的数据库,因为他们都提供了JDBC驱动:
如果从代码上来看,Java标准库自带的JDBC接口其实就是定义了一组接口,而某个具体的JDBC驱动其实就是实现了这些接口的类:
实际上,一个MySQL的JDBC的驱动就是一个jar包,它本身也是纯Java编写的。我们自己编写的代码只需要引用Java标准库提供的java.sql包下面的相关接口,由此再间接地通过MySQL驱动的jar包通过网络访问MySQL服务器,所有复杂的网络通讯都被封装到JDBC驱动中,因此,Java程序本身只需要引入一个MySQL驱动的jar包就可以正常访问MySQL服务器:
1.1、使用JDBC的好处
JDBC API 是一个 Java API,它可以访问任何类型的表格数据,特别是可以访问存储在关系数据库里的数据。JDBC 可以用 Java 语言在各种平台上实现,比如 Windows 系统, Mac OS 系统,和各种版本的 UNIX 系统。
并且:
-
各数据库厂商使用相同的接口,Java代码不需要针对不同数据库分别开发;
-
Java程序编译期仅依赖java.sql包,不依赖具体数据库的jar包;
-
可随时替换底层数据库,访问数据库的Java代码基本不变。
2、下载导入 JDBC 驱动
2.1、下载 JDBC 驱动
可参考:https://www.cnblogs.com/NyanKoSenSei/p/11510438.html
2.2、将 JDBC 驱动导入项目
将 JDBC 驱动导入项目就跟将普通的 jar 包导入项目一样。下载完 JDBC 驱动后,将其解压,可以看到里面有两个 jar 包。
在 java 项目中新建一个 lib 文件夹,将 JDBC 解压后里面的 xxx.bin.jar 包复制到项目的 lib 文件夹中,右键要使用的jar包,选择Build Path-->Add to Build Path,将其添加为项目依赖即可。
可参考:https://jingyan.baidu.com/article/bad08e1e23982609c851219e.html
3、使用JDBC操作数据库
构建一个 JDBC 应用程序包括以下六个步骤-
-
导入数据包:需要你导入含有需要进行数据库编程的 JDBC 类的包。大多数情况下,使用 import java.sql. 就足够了。
-
注册 JDBC 驱动器:需要你初始化一个驱动器,以便于你打开一个与数据库的通信通道。
-
打开连接:需要使用 DriverManager.getConnection() 方法创建一个 Connection 对象,它代表与数据库的物理连接。
-
执行查询:需要使用类型声明的对象建立并提交一个 SQL 语句到数据库。
-
提取结果数据:要求使用适当的 ResultSet.getXXX() 方法从结果集中检索数据。
- 清理环境:依靠 JVM 的垃圾收集来关闭所有需要明确关闭的数据库资源。
3.1、操作数据库代码示例
在导入驱动 jar 包后,我们就可以使用 JDBC 驱动来操作数据库了。
代码示例:
package jdbcTest; import java.net.URL; import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; import java.sql.Statement; public class JDBCTest { public static void main(String[] args) throws Exception { //1.注册驱动(mysql5之后的驱动jar包可以省略注册驱动的步骤) Class.forName("com.mysql.jdbc.Driver"); //2.获取数据库连接对象 Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/db_test", "root", "123456"); //3.定义sql语句 String sql = "update students set name = 'hahaha' where id = 1"; //4.获取执行sql的对象 Statement stmt = conn.createStatement(); 5.执行sql int count = stmt.executeUpdate(sql); System.out.println(count); //6.释放资源 stmt.close(); conn.close(); } }
3.2、常用的 JDBC 组件介绍
JDBC 的 API 提供了以下接口和类:
DriverManager :这个类管理一系列数据库驱动程序。匹配连接使用通信子协议从 JAVA 应用程序中请求合适的数据库驱动程序。识别 JDBC 下某个子协议的第一驱动程序将被用于建立数据库连接。
Driver : 这个接口处理与数据库服务器的通信。你将很少直接与驱动程序互动。相反,你使用 DriverManager 中的对象,它管理此类型的对象。它也抽象与驱动程序对象工作相关的详细信息。
Connection : 此接口具有接触数据库的所有方法。该连接对象表示通信上下文,即,所有与数据库的通信仅通过这个连接对象进行。
Statement : 使用创建于这个接口的对象将 SQL 语句提交到数据库。除了执行存储过程以外,一些派生的接口也接受参数。
ResultSet : 在你使用语句对象执行 SQL 查询后,这些对象保存从数据获得的数据。它作为一个迭代器,让您可以通过它的数据来移动。
SQLException : 这个类处理发生在数据库应用程序的任何错误。
3.3、先注册 JDBC 驱动程序(mysql5之后的驱动程序可省略)
在使用驱动程序之前,你必须在你的程序里面注册它。我们可以通过加载 Oracle 驱动程序的类文件到内存中来注册驱动程序,在程序里做一次注册即可。
注册一个驱动程序中最常用的方法是使用 Java 的Class.forName() 方法来动态加载驱动程序的类文件到内存中,它会自动将其注册。这种方法更优越一些,因为它允许你对驱动程序的注册信息进行配置,便于移植。
try { Class.forName("oracle.jdbc.driver.OracleDriver"); } catch(ClassNotFoundException ex) { System.out.println("Error: unable to load driver class!"); }
(注意:在mysql5之后的驱动jar包可以省略注册驱动的步骤)
3.4、然后使用JDBC连接数据库(Connection)
要通过 JDBC 操作数据库,我们需要先连接数据库。当你加载了驱动程序之后,你可以通过 DriverManager.getConnection() 方法建立一个连接。Connection代表一个JDBC连接,它相当于 Java 程序到数据库的连接(通常是TCP连接)。
打开一个Connection时,需要准备URL、用户名和密码,才能成功连接到数据库。
数据库连接的代码示例如下:
// JDBC连接的URL, 不同数据库有不同的格式: String JDBC_URL = "jdbc:mysql://localhost:3306/test"; //URL String JDBC_USER = "root"; //用户名 String JDBC_PASSWORD = "password"; //密码 // 获取连接 Connection conn = DriverManager.getConnection(JDBC_URL, JDBC_USER, JDBC_PASSWORD); ... // 最后需关闭连接 conn.close();
核心代码是DriverManager
提供的静态方法getConnection()
。DriverManager
会自动扫描classpath,找到所有的JDBC驱动,然后根据我们传入的URL自动挑选一个合适的驱动。
JDBC连接是一种昂贵的资源,使用后要及时释放。可以使用try (resource)
来自动释放JDBC连接:
try (Connection conn = DriverManager.getConnection(JDBC_URL, JDBC_USER, JDBC_PASSWORD)) { ... }
在 JDBC 程序的末尾,它必须明确关闭所有的连接到数据库的连接,以结束每个数据库会话。但是,如果忘了,Java 垃圾收集器也会关闭连接,它会完全清除过期的对象。依托垃圾收集器,特别是在数据库编程,是非常差的编程习惯,我们应该养成用 close()方法关闭连接对象的习惯。
3.4.1、数据库连接的URL格式
URL是由数据库厂商指定的格式,例如,MySQL的URL是:
jdbc:mysql://服务器IP:端口号/数据库名称?key1=value1&key2=value2
假设数据库运行在本机localhost,端口使用标准的3306,数据库名称是learnjdbc。示例如下:
jdbc:mysql://localhost:3306/learnjdbc?useSSL=false&characterEncoding=utf8 # 如果连接的是本地服务器,并且服务器默认端口是3306,那么可以简写为以下形式,即省略服务器地址和端口号: jdbc:mysql:///数据库名称
(后面的两个参数表示不使用SSL加密,使用UTF-8作为字符编码(注意MySQL的UTF-8是utf8),不写也行)
下表列出了常用的 JDBC 驱动程序名和数据库URL。
上表中 URL 格式所有加粗的部分都是静态的,你需要将剩余部分按照你的数据库实际情况进行设置。
3.4.2、如何创建连接对象
我们可以通过 DriverManager.getConnection() 方法来建立一个数据库连接,加载 DriverManager.getConnection() 参数有以下三种:
- getConnection(String url)
- getConnection(String url, Properties prop)
- getConnection(String url, String user, String password)
使用URL、用户名、密码:getConnection() 最常用的方式是需要你提供一个数据库 URL,用户名和密码:
String URL = "jdbc:oracle:thin:@amrood:1521:EMP"; String USER = "username"; String PASS = "password" Connection conn = DriverManager.getConnection(URL, USER, PASS);
只使用URL:在只使用url时,数据库的 URL ,包括用户名和密码,将表现为以下的格式:
jdbc:oracle:driver:username/password@database
示例:
String URL = "jdbc:oracle:thin:username/password@amrood:1521:EMP";
Connection conn = DriverManager.getConnection(URL);
使用数据库 URL 和 Properties 对象:Properties 对象保存了一组关键数值。它通过调用 getConnection() 方法,将驱动程序属性传递给驱动程序。
import java.util.*; String URL = "jdbc:oracle:thin:@amrood:1521:EMP"; Properties info = new Properties( ); info.put( "user", "username" ); info.put( "password", "password" ); Connection conn = DriverManager.getConnection(URL, info);
3.5、最后使用JDBC操作数据库
一旦我们获得了数据库的连接,我们就可以和数据库进行交互。JDBC 的 Statement,CallableStatement 和 PreparedStatement 接口定义的方法和属性,可以让你发送 SQL 命令或 PL/SQL 命令到数据库,并从你的数据库接收数据。在数据库中,它们还定义了帮助 Java 和 SQL 数据类型之间转换数据差异的方法。
下表提供了每个接口的用途概要,根据实际目的决定使用哪个接口。
查询数据库可以分为以下几步:
第一步,通过Connection
提供的createStatement()
方法创建一个Statement
对象,用于执行一个查询;
第二步,执行Statement
对象提供的executeQuery("SELECT * FROM students")
并传入SQL语句,执行查询并获得返回的结果集,使用ResultSet
来引用这个结果集;
第三步,反复调用ResultSet
的next()
方法并读取每一行结果。
完整查询代码如下:
try (Connection conn = DriverManager.getConnection(JDBC_URL, JDBC_USER, JDBC_PASSWORD)) { try (Statement stmt = conn.createStatement()) { try (ResultSet rs = stmt.executeQuery("SELECT id, grade, name, gender FROM students WHERE gender=1")) { while (rs.next()) { long id = rs.getLong(1); // 注意:索引从1开始 long grade = rs.getLong(2); String name = rs.getString(3); int gender = rs.getInt(4); } } } }
注意要点:
Statment
和ResultSet
都是需要关闭的资源,因此嵌套使用try (resource)
确保及时关闭;
rs.next()
用于判断是否有下一行记录,如果有,将自动把当前行移动到下一行(一开始获得ResultSet
时当前行不是第一行);
ResultSet
获取列时,索引从1
开始而不是0
;
必须根据SELECT
的列的对应位置来调用getLong(1)
,getString(2)
这些方法,否则对应位置的数据类型不对,将报错。
3.5.1、使用 Statement 对象来操作数据库
在你准备使用 Statement 对象执行 SQL 语句之前,你需要使用 Connection 对象的 createStatement() 方法先创建一个 Statement 对象。
Connection conn = DriverManager.getConnection(URL, 用户名, 密码);
Statement stmt = conn.createStatement( );
当你创建了一个 Statement 对象之后,你可以用它的三个执行方法的任一方法来执行 SQL 语句。
-
boolean execute(String SQL) : 如果 ResultSet 对象可以被检索,则返回的布尔值为 true ,否则返回 false 。当你需要使用真正的动态 SQL 时,可以使用这个方法来执行 SQL DDL 语句。
-
int executeUpdate(String SQL) : 一般使用该方法执行DML语句(增删改),也可执行DDL语句(操作数据库和表结构),它返回的是执行 SQL 语句影响的行的数目。
- ResultSet executeQuery(String SQL) : 一般使用该方法执行DQL语句(查询),它返回一个 ResultSet 对象。
String sql = "update students set name = 'hahaha' where id = 1"; Statement stmt = conn.createStatement(); int count = stmt.executeUpdate(sql);
在使用后我们应该关闭 Statement 对象。通过调用 close() 方法就可以关闭 Statement 对象。其实在我们关闭了 Connection 对象后,它也会自动关闭 Statement 对象。但我们应该始终明确关闭 Statement 对象,以确保真正的清除。
Statement stmt = null; try { stmt = conn.createStatement( ); . . . } catch (SQLException e) { . . . } finally { stmt.close(); }
3.5.2、使用PreparedStatement对象操作数据库
PreparedStatement 接口扩展了 Statement 接口,它让你用一个常用的 Statement 对象增加几个高级功能。这个 statement 对象可以提供灵活多变的动态参数。
创建 PreparedStatement 对象:
String SQL = "Update Employees SET age = ? WHERE id = ?";
PreparedStatement pstmt = conn.prepareStatement(SQL);
JDBC 中所有的参数都被用 ? 符号表示,这是已知的参数标记。在执行 SQL 语句之前,你必须赋予每一个参数确切的数值。
setXXX() 方法将值绑定到参数,其中 XXX 表示你希望绑定到输入参数的 Java 数据类型。如果你忘了赋予值,你将收到一个 SQLException。每个参数标记映射它的序号位置。第一标记表示位置 1 ,下一个位置为 2 等等。这种方法不同于 Java 数组索引,它是从 0 开始的。
String sql = "SELECT * FROM user WHERE login=? AND pass=?"; PreparedStatement ps = conn.prepareStatement(sql); ps.setObject(1, name); ps.setObject(2, pass);
所有的 Statement对象 的方法都与数据库交互,(a) execute(),(b) executeQuery(),及 (c) executeUpdate() 也能被 PreparedStatement 对象引用。然而,这些方法被 SQL 语句修改后是可以输入参数的。
PreparedStatement 对象在使用后也需要关闭,只需简单调用 close() 方法就可以完成这项工作。如果你关闭了 Connection 对象,那么它也会关闭 PreparedStatement 对象。然而,你应该始终明确关闭 PreparedStatement 对象,以确保真正的清除。
4、JDBC事务
数据库事务(Transaction)是由若干个SQL语句构成的一个操作序列,有点类似于Java的synchronized
同步。数据库系统保证在一个事务中的所有SQL要么全部执行成功,要么全部不执行,即数据库事务具有ACID特性:
- Atomicity:原子性
- Consistency:一致性
- Isolation:隔离性
- Durability:持久性
数据库事务可以并发执行,而数据库系统从效率考虑,对事务定义了不同的隔离级别。SQL标准定义了4种隔离级别,分别对应可能出现的数据不一致的情况:
要在JDBC中执行事务,本质上就是如何把多条SQL包裹在一个数据库事务中执行。
JDBC的事务代码如下:
Connection conn = openConnection(); try { // 关闭自动提交: conn.setAutoCommit(false); // 执行多条SQL语句: insert(); update(); delete(); // 提交事务: conn.commit(); } catch (SQLException e) { // 回滚事务: conn.rollback(); } finally { conn.setAutoCommit(true); //最后恢复至自动提交 conn.close(); }
其中,开启事务的关键代码是conn.setAutoCommit(false)
,表示关闭自动提交。提交事务的代码在执行完指定的若干条SQL语句后,调用conn.commit()
。要注意事务不是总能成功,如果事务提交失败,会抛出SQL异常(也可能在执行SQL语句的时候就抛出了),此时我们必须捕获并调用conn.rollback()
回滚事务。最后,在finally
中通过conn.setAutoCommit(true)
把Connection
对象的状态恢复到初始值。
实际上,默认情况下,我们获取到Connection
连接后,总是处于“自动提交”模式,也就是每执行一条SQL都是作为事务自动执行的,这种事务也就是“隐式事务”。只要关闭了Connection
的autoCommit
,那么就可以在一个事务中执行多条语句,事务以commit()
方法结束。
4.1、JDBC定义事务的隔离级别
如果要设定事务的隔离级别,可以使用如下代码:
// 设定隔离级别为READ COMMITTED: conn.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED);
如果没有调用上述方法,那么会使用数据库的默认隔离级别。MySQL的默认隔离级别是REPEATABLE READ
。
5、SQL注入问题
使用Statement
拼字符串非常容易引发SQL注入的问题,这是因为SQL参数往往是从方法参数传入的。
假设用户登录的验证方法如下:
User login(String name, String pass) { ... stmt.executeQuery("SELECT * FROM user WHERE login='" + name + "' AND pass='" + pass + "'"); ... }
其中,参数name
和pass
通常都是Web页面输入后由程序接收到的。
如果用户的输入是程序期待的值,就可以拼出正确的SQL。例如:name = "bob"
,pass = "1234"
:
SELECT * FROM user WHERE login='bob' AND pass='1234'
但是,如果用户的输入是一个精心构造的字符串,就可以拼出意想不到的SQL,这个SQL也是正确的,但它查询的条件不是程序设计的意图。例如:name = "bob' OR pass="
, pass = " OR pass='"
:
SELECT * FROM user WHERE login='bob' OR pass=' AND pass=' OR pass=''
这个SQL语句执行的时候,根本不用判断口令是否正确,这样一来,登录就形同虚设。
要避免SQL注入攻击,一个办法是针对所有字符串参数进行转义,但是转义很麻烦,而且需要在任何使用SQL的地方增加转义代码。
还有一个办法就是使用PreparedStatement
。使用PreparedStatement
可以完全避免SQL注入的问题,因为PreparedStatement
始终使用?
作为占位符,并且把数据连同SQL本身传给数据库,这样可以保证每次传给数据库的SQL语句是相同的,只是占位符的数据不同,还能高效利用数据库本身对查询的缓存。上述登录SQL如果用PreparedStatement
可以改写如下:
User login(String name, String pass) { ... String sql = "SELECT * FROM user WHERE login=? AND pass=?"; PreparedStatement ps = conn.prepareStatement(sql); ps.setObject(1, name); ps.setObject(2, pass); ... }
所以,PreparedStatement
比Statement
更安全,而且更快。
注意:使用Java对数据库进行操作时,必须使用PreparedStatement,严禁任何通过参数拼字符串的代码!
把上面使用Statement
的代码改为使用PreparedStatement,如下:
try (Connection conn = DriverManager.getConnection(JDBC_URL, JDBC_USER, JDBC_PASSWORD)) { try (PreparedStatement ps = conn.prepareStatement("SELECT id, grade, name, gender FROM students WHERE gender=? AND grade=?")) { ps.setObject(1, "M"); // 注意:索引从1开始 ps.setObject(2, 3); try (ResultSet rs = ps.executeQuery()) { while (rs.next()) { long id = rs.getLong("id"); long grade = rs.getLong("grade"); String name = rs.getString("name"); String gender = rs.getString("gender"); } } } }
使用PreparedStatement
和Statement
稍有不同,必须首先调用setObject()
设置每个占位符?
的值,最后获取的仍然是ResultSet
对象。另外注意到从结果集读取列时,使用String
类型的列名比索引要易读,而且不易出错。
注意到JDBC查询的返回值总是ResultSet
,即使我们写这样的聚合查询SELECT SUM(score) FROM ...
,也需要按结果集读取:
ResultSet rs = ... if (rs.next()) { double sum = rs.getDouble(1); }
使用JDBC的时候,我们需要在SQL数据类型和Java数据类型之间进行转换。JDBC在
java.sql.Types
定义了一组常量来表示如何映射SQL数据类型,但是平时我们使用的类型通常也就以下几种:
只有最新的JDBC驱动才支持LocalDate
和LocalTime
。