Java JDBC 編程指北
前言
在我們?nèi)粘J褂玫?APP 或網(wǎng)站中,往往需要存取數(shù)據(jù),比如在微信中,需要存儲我們的用戶名、手機號、用戶密碼…… 等一系列信息。依靠之前所學(xué)習(xí)的 Java 相關(guān)知識已經(jīng)無法滿足這一需求。現(xiàn)在的應(yīng)用程序中最基本、應(yīng)用最廣的也就是關(guān)系型數(shù)據(jù)庫,如 MySQL。Java 語言中為了實現(xiàn)與關(guān)系型數(shù)據(jù)庫的通信,制定了標準的訪問捷克,即 JDBC(Java Database Connectivity)。本文主要介紹在 Java 中使用 JDBC 的相關(guān)知識,主要內(nèi)容如下:
JDBC 簡介 數(shù)據(jù)的增刪改查 事務(wù) 連接池
JDBC 簡介
JDBC(Java Database Connectivity),即 Java 數(shù)據(jù)庫連接。是 Java 語言中用于規(guī)范客戶端程序如何來訪問數(shù)據(jù)庫的應(yīng)用程序接口,它是面向關(guān)系型數(shù)據(jù)庫的,提供了查詢和更新數(shù)據(jù)庫中數(shù)據(jù)的方法。
本文以 MySQL 來演示如何使用 JDBC,所以需要事先在你的機器上準備好 MySQL,而且最好是懂一些 MySQL 的使用。
首先我們需要建立 MySQL 與 Java 程序間的聯(lián)系,所以需要事先好 mysql-connector-java 這個第三方包,下載地址:https://downloads.mysql.com/archives/c-j/
導(dǎo)入驅(qū)動包
以在 IDEA 中導(dǎo)入 jar 包為例,當(dāng)我們建立好項目后,導(dǎo)包過程如下:
首先依次打開 File -> Project Structure -> Modules -> Dependencies;

然后點擊 +號,選擇1 JARs or Directories,找到你下載好的 jar 包導(dǎo)入;

導(dǎo)入成功,點擊 OK即可;


初始化并建立連接
導(dǎo)入我們的 jar 包之后,就需要進行初始化工作。新建一個類,用于初始化并連接。先將驅(qū)動類加載到 JVM 中,加載過程中會執(zhí)行其中的靜態(tài)初始化塊,從而完成驅(qū)動的初始化工作。然后建立數(shù)據(jù)庫與程序之間的連接,此時需要提供數(shù)據(jù)庫的 IP 地址、端口號、數(shù)據(jù)庫名、編碼方式、用戶名、用戶密碼等信息。
首先,我們在數(shù)據(jù)庫中建立一個表 student,建表語句如下,用于后續(xù)實踐。
-- 創(chuàng)建數(shù)據(jù)庫 javalearning
CREATE DATABASE if not exists javalearning;
-- 創(chuàng)建表 students
USE javalearning;
CREATE TABLE students (
id BIGINT AUTO_INCREMENT NOT NULL, -- 學(xué)號
name VARCHAR(50) NOT NULL, -- 姓名
gender TINYINT(1) NOT NULL, -- 性別
grade INT NOT NULL, -- 年級
score INT NOT NULL, -- 分數(shù)
PRIMARY KEY(id) -- 主鍵
) Engine=INNODB DEFAULT CHARSET=UTF8;
-- 插入部分數(shù)據(jù)
INSERT INTO students (id, name, gender, grade, score) VALUES (101,'小紅', 0, 1, 100);
INSERT INTO students (id, name, gender, grade, score) VALUES (102,'小橙', 0, 1, 89);
INSERT INTO students (id, name, gender, grade, score) VALUES (201,'小黃', 1, 2, 97);
INSERT INTO students (id, name, gender, grade, score) VALUES (301,'小綠', 1, 3, 99);


創(chuàng)建好數(shù)據(jù)庫及表之后,我們就可以進行初始化和連接工作了,這里的步驟主要分為如下幾步:
首先需要加載驅(qū)動,主要是利用 Class.forName()將驅(qū)動類加載到 JVM;建立程序和數(shù)據(jù)庫之間的連接,主要是創(chuàng)建 Connection對象;接著是創(chuàng)建用于執(zhí)行 SQL 語句的 Statement對象;最后則是關(guān)閉連接從而釋放資源,先關(guān)閉 Statement,再關(guān)閉Connection;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.sql.Statement;
/**
* @author : cunyu
* @version : 1.0
* @className : InitJDBC
* @date : 2021/4/23 10:56
* @description : 初始化并建立連接
*/
public class InitJDBC {
public static void main(String[] args) {
Connection connection = null;
Statement statement = null;
try {
// 初始化,注冊驅(qū)動
Class.forName("com.mysql.cj.jdbc.Driver");
// 建立連接
connection = DriverManager.getConnection("jdbc:mysql://localhost/javalearning?characterEncoding=UTF-8", "root", "12345");
System.out.println("連接成功!");
// 創(chuàng)建 Statement 用于執(zhí)行 SQL 語句
statement = connection.createStatement();
System.out.println("Statement 對象:" + statement);
} catch (ClassNotFoundException | SQLException e) {
e.printStackTrace();
} finally {
try {
if (statement != null) {
statement.close();
}
} catch (SQLException throwables) {
throwables.printStackTrace();
}
try {
if (connection != null) {
connection.close();
}
} catch (SQLException throwables) {
throwables.printStackTrace();
}
}
}
}

對于上述關(guān)閉 Connection 和 Statement 的方式,可能略顯繁瑣,為了進一步簡化,可以使用 try-with-source 的方式自動關(guān)閉,簡化后的代碼如下;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.sql.Statement;
/**
* @author : cunyu
* @version : 1.0
* @className : InitJDBC2
* @date : 2021/4/23 13:53
* @description : 初始化與連接
*/
public class InitJDBC2 {
public static void main(String[] args) {
try {
Class.forName("com.mysql.cj.jdbc.Driver");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
try (Connection connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/javalearning?characterEncoding=UTF-8", "root", "12345"); Statement statement = connection.createStatement();) {
System.out.println("連接成功");
System.out.println("State 對象:" + statement);
} catch (SQLException throwables) {
throwables.printStackTrace();
}
}
}
JDBC 增刪改查
當(dāng)我們初始化并建立 JDBC 連接之后,我們就可以對數(shù)據(jù)庫進行 CRUD (增加、查詢、更新、刪除)等操作。
在正式開始 CRUD 前,我們最好先了解下 MySQL 中的數(shù)據(jù)類型在 Java 中所對應(yīng)的數(shù)據(jù)類型,以便后續(xù)操作數(shù)據(jù)。一般來講,兩者中的數(shù)據(jù)類型對應(yīng)關(guān)系如下表所示。
| SQL 中的數(shù)據(jù)類型 | 對應(yīng)的 Java 數(shù)據(jù)類型 |
|---|---|
BIT、BOOL | boolean |
INTEGER | int |
BIGINT | long |
REAL | float |
FLOAT、 DOUBLE | double |
CHAR、 VARCHAR | String |
DECIMAL | BigDecimal |
DATE | java.sql.Date、LocalDate |
TIME | java.sql.Time、 LocalTime |
此外,雖然我們在 JDBC 的簡介部分在初始化和建立連接時使用的是用 Statement 來創(chuàng)建一個對象并用于后續(xù)操作,但是在實際使用過程中時,SQL 參數(shù)基本都是從方法參數(shù)傳入的,這時使用 Statement 就十分容易引起 SQL 注入,為了解決這一問題,大牛們提出了如下兩個辦法:
對字符串中的參數(shù)進行轉(zhuǎn)義,然后利用轉(zhuǎn)義后的參數(shù)來進行操作。但是轉(zhuǎn)義十分麻煩,而且一使用 SQL,我們就必須增加轉(zhuǎn)義代碼。 利用 PreparedStatement,它利用?作為占位符,將數(shù)據(jù)聯(lián)通 SQL 本身傳遞給數(shù)據(jù)庫,從而保證每次傳給數(shù)據(jù)庫的 SQL 語句都是保持一致的,每次變動的只是占位符中的數(shù)據(jù)不同。通過使用PreparedStatement,我們就能夠 完全避免 SQL 注入 問題。
針對后續(xù)利用 JDBC 操作數(shù)據(jù)庫的過程,為了盡量避免 SQL 注入問題,我們優(yōu)先采用 PreparedStatement 而非 Statement.
查詢數(shù)據(jù)
首先,我們來進行查詢操作。進行查詢時,可以總結(jié)為如下幾個步驟:
通過創(chuàng)建一個 Connection對象從而建立連接;然后利用 prepareStatement()方法創(chuàng)建一個PreparedStatement對象并傳入 SQL 語句,用于執(zhí)行查詢操作;接著執(zhí)行 PreparedStatement對象所提供的executeQuery()方法,獲取查詢結(jié)果并返回到一個ResultSet結(jié)果集中;最后則是利用 ResultSet對象的next()方法去讀取我們所查詢返回的結(jié)果;
需要注意的地方:
如果你不是利用 try-with-source的方式,那么一定要記得在使用完連接之后記得釋放資源;結(jié)果集 ResultSet中,索引位置是從1開始的,而不是從0開始,這一點要特別注意!
import java.sql.*;
/**
* @author : cunyu
* @version : 1.0
* @className : QueryTest
* @date : 2021/4/23 14:01
* @description : 查詢
*/
public class QueryTest {
public static void main(String[] args) {
try {
Class.forName("com.mysql.cj.jdbc.Driver");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
String url = "jdbc:mysql://localhost:3306/javalearning?characterEncoding=UTF-8";
String username = "root";
String password = "0908";
String queryString = "SELECT * FROM students";
try (Connection connection = DriverManager.getConnection(url, username, password); PreparedStatement preparedStatement = connection.prepareStatement(queryString); ResultSet resultSet = preparedStatement.executeQuery();) {
System.out.println("連接成功");
System.out.println("查詢到的信息如下:");
while (resultSet.next()) {
// 查詢到的結(jié)果索引從 1 開始
System.out.println("id:" + resultSet.getLong(1) + "\tname:" + resultSet.getString(2) + "\tgender:" + resultSet.getInt(3) + "\tgrade:" + resultSet.getLong(4) + "\tscore:" + resultSet.getLong(5));
}
} catch (SQLException throwables) {
throwables.printStackTrace();
}
}
}

增加數(shù)據(jù)
即插入一條新記錄,和查詢語句很像,但是區(qū)別在于最后 PreparedStatement 對象執(zhí)行的不是 executeQuery(),而是 executeUpdate(). 插入記錄的步驟總結(jié)如下:
創(chuàng)建 Connection對象從而建立連接;利用 prepareStatement()方法創(chuàng)建一個PreparedStatement對象并傳入 SQL 語句,用于執(zhí)行插入操作;然后依次設(shè)置占位符所代表的值; 執(zhí)行 PreparedStatement對象所提供的executeUpdate()方法,此時返回的是一個int類型的數(shù),表示插入記錄的條數(shù);
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.SQLException;
/**
* @author : cunyu
* @version : 1.0
* @className : InsertTest
* @date : 2021/4/23 15:04
* @description : 新增數(shù)據(jù)
*/
public class InsertTest {
public static void main(String[] args) {
try {
Class.forName("com.mysql.cj.jdbc.Driver");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
String url = "jdbc:mysql://localhost:3306/javalearning?characterEncoding=UTF-8";
String username = "root";
String password = "110120";
String insertString = "INSERT INTO students VALUES (?,?,?,?,?)";
try (Connection connection = DriverManager.getConnection(url, username, password); PreparedStatement preparedStatement = connection.prepareStatement(insertString);) {
System.out.println("連接成功");
// 依次插入數(shù)據(jù)
preparedStatement.setLong(1, 302);
preparedStatement.setString(2, "小藍");
preparedStatement.setInt(3, 0);
preparedStatement.setLong(4, 3);
preparedStatement.setLong(5, 100);
System.out.println("插入數(shù)據(jù)成功");
preparedStatement.executeUpdate();
} catch (SQLException throwables) {
throwables.printStackTrace();
}
}
}
新增數(shù)據(jù)后,接著查詢數(shù)據(jù),得到如下結(jié)果,可以看到我們新插入的數(shù)據(jù)成功加入到了數(shù)據(jù)庫中!

刪除數(shù)據(jù)
刪除數(shù)據(jù)和新增數(shù)據(jù)的方式基本一樣,兩者最大的區(qū)別在于 SQL 語句的不同,刪除操作利用的是 DELETE 語句,能一次刪除若干列。
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.SQLException;
/**
* @author : cunyu
* @version : 1.0
* @className : DeleteTest
* @date : 2021/4/23 15:23
* @description : 刪除數(shù)據(jù)
*/
public class DeleteTest {
public static void main(String[] args) {
try {
Class.forName("com.mysql.cj.jdbc.Driver");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
String url = "jdbc:mysql://localhost:3306/javalearning?charactersetEncoding=UTF-8";
String username = "root";
String password = "0908";
String deleteString = "DELETE FROM students WHERE id = ?";
try (Connection connection = DriverManager.getConnection(url, username, password); PreparedStatement preparedStatement = connection.prepareStatement(deleteString);) {
System.out.println("連接成功");
preparedStatement.setLong(1, 101);
preparedStatement.executeUpdate();
System.out.println("刪除成功");
} catch (SQLException throwables) {
throwables.printStackTrace();
}
}
}
刪除數(shù)據(jù)后,接著查詢數(shù)據(jù),得到如下結(jié)果,可以看到 id = 101 的數(shù)據(jù)列已經(jīng)被刪除了,說明我們刪除數(shù)據(jù)成功了!

修改數(shù)據(jù)
修改數(shù)據(jù)的方式同刪除數(shù)據(jù)和新增數(shù)據(jù)基本一致,最大的區(qū)別在于 SQL 語句的不同,修改操作利用的是 UPDATE 語句,能一次更新若干列。
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.SQLException;
/**
* @author : cunyu
* @version : 1.0
* @className : UpdateTest
* @date : 2021/4/23 15:23
* @description : 更新數(shù)據(jù)
*/
public class UpdateTest {
public static void main(String[] args) {
try {
Class.forName("com.mysql.cj.jdbc.Driver");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
String url = "jdbc:mysql://localhost:3306/javalearning?charactersetEncoding=UTF-8";
String username = "root";
String password = "0908";
String updateString = "UPDATE students SET name = ? WHERE id = ?";
try (Connection connection = DriverManager.getConnection(url, username, password); PreparedStatement preparedStatement = connection.prepareStatement(updateString);) {
System.out.println("連接成功");
preparedStatement.setString(1, "村雨遙");
preparedStatement.setLong(2, 201);
preparedStatement.executeUpdate();
System.out.println("更新成功");
} catch (SQLException throwables) {
throwables.printStackTrace();
}
}
}
修改數(shù)據(jù)后,接著查詢數(shù)據(jù),得到如下結(jié)果,可以看到 id = 201 對應(yīng)的數(shù)據(jù)列中,name 從小黃變成了村雨遙,說明數(shù)據(jù)更新成功。

注意
當(dāng)我們的數(shù)據(jù)庫表設(shè)置自增主鍵后,在新增數(shù)據(jù)時無需指定主鍵也會自動更新。但是在獲取自增主鍵的值時,不能先插入再查詢,否則可能會導(dǎo)致沖突。要正確獲取自增主鍵,需要在創(chuàng)建 PreparedStatement 時,指定一個標志位 RETURN_GENERATED_KEYS,用于表示 JDBC 驅(qū)動必須返回插入的自增主鍵。
假設(shè)我們創(chuàng)建表時,設(shè)置了自增長的鍵:
CREATE TABLE students(
id int(11) AUTO_INCREMENT,
…
);
此時無論是 executeQuery() 還是 execureUpdate() 都不會返回這個自增長的 id,所以需要在創(chuàng)建 PreparedStatement 對象時加入 Statement.RETURN_GENERATED_KEYS 參數(shù)以確保會返回自增長 ID,然后通過 getGeneratedKeys 獲取該字段;
import java.sql.*;
/**
* @author : cunyu
* @version : 1.0
* @className : QueryTest
* @date : 2021/4/23 18:01
* @description : 自增主鍵查詢
*/
public class QueryTest {
public static void main(String[] args) {
try {
Class.forName("com.mysql.cj.jdbc.Driver");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
String url = "jdbc:mysql://localhost:3306/javalearning?characterEncoding=UTF-8";
String username = "root";
String password = "12345";
String queryString = "INSET INTO students VALUES(null,?,……)";
try (Connection connection = DriverManager.getConnection(url, username, password); PreparedStatement preparedStatement = connection.prepareStatement(queryString, Statement.RETURN_GENERATED_KEYS); ResultSet resultSet = preparedStatement.getGeneratedKeys();) {
System.out.println("連接成功");
preparedStatement.setString(1, "村雨遙");
……
preparedStatement.executeUpdate();
System.out.println("查詢到的信息如下:");
while (resultSet.next()) {
// 查詢到的結(jié)果索引從 1 開始
System.out.println("id:" + resultSet.getLong(1));
}
} catch (SQLException throwables) {
throwables.printStackTrace();
}
}
}
JDBC 工具類
觀察上面的代碼,我們可以注意到每次都需要注冊驅(qū)動、傳遞參數(shù),關(guān)閉連接等操作,為了提高工具通用性,我們利用配置文件來配置數(shù)據(jù)庫相關(guān)信息,然后創(chuàng)建一個 JDBC 工具類來簡化上述操作。
首先在 src目錄下創(chuàng)建一個配置文件jdbc.properties,并且填入數(shù)據(jù)庫的相關(guān)信息;
url=jdbc:mysql://localhost/demo?characterEncoding=UTF-8
user=root
password="12345"
driver=com.mysql.jdbc.cj.Driver
創(chuàng)建工具類
import java.io.FileReader;
import java.io.IOException;
import java.net.URL;
import java.sql.*;
import java.util.Properties;
/**
* @author : cunyu
* @version : 1.0
* @className : JDBCUtils
* @date : 2021/4/24 15:10
* @description : JDBC 工具類
*/
public class JDBCUtils {
// 配置文件中的各個參數(shù)
private static String url;
private static String user;
private static String password;
private static String driver;
// 靜態(tài)代碼塊
static {
try {
// 讀取配置文件并獲取參數(shù)值
// 創(chuàng)建集合類
Properties properties = new Properties();
// 獲取配置文件所在位置
ClassLoader classLoader = JDBCUtils.class.getClassLoader();
URL resource = classLoader.getResource("jdbc.properties");
String path = resource.getPath();
System.out.println("配置文件所在位置");
// 加載配置文件
properties.load(new FileReader(path));
// 獲取參數(shù)的值并賦值
url = properties.getProperty("url");
user = properties.getProperty("user");
password = properties.getProperty("password");
driver = properties.getProperty("driver");
// 注冊驅(qū)動
Class.forName(driver);
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
}
/**
* @param
* @return 連接對象
* @description 獲取連接
* @date 2021/4/24 15:24
* @author cunyu1943
* @version 1.0
*/
public static Connection getConnection() {
try {
return DriverManager.getConnection(url, user, password);
} catch (SQLException throwables) {
throwables.printStackTrace();
}
return null;
}
/**
* @param preparedStatement 預(yù)聲明
* @param connection 連接對象
* @return
* @description 關(guān)閉連接
* @date 2021/4/24 15:27
* @author cunyu1943
* @version 1.0
*/
public static void close(PreparedStatement preparedStatement, Connection connection) {
if (preparedStatement != null) {
try {
preparedStatement.close();
} catch (SQLException throwables) {
throwables.printStackTrace();
}
}
if (connection != null) {
try {
connection.close();
} catch (SQLException throwables) {
throwables.printStackTrace();
}
}
}
/**
* @param resultSet 結(jié)果集
* @param preparedStatement 預(yù)聲明對象
* @param connection 連接對象
* @return
* @description 關(guān)閉連接
* @date 2021/4/24 15:28
* @author cunyu1943
* @version 1.0
*/
public static void close(ResultSet resultSet, PreparedStatement preparedStatement, Connection connection) {
if (resultSet != null) {
try {
resultSet.close();
} catch (SQLException throwables) {
throwables.printStackTrace();
}
}
if (preparedStatement != null) {
try {
preparedStatement.close();
} catch (SQLException throwables) {
throwables.printStackTrace();
}
}
if (connection != null) {
try {
connection.close();
} catch (SQLException throwables) {
throwables.printStackTrace();
}
}
}
}
JDBC 事務(wù)
事務(wù) 4 大特性
事務(wù)是一個不可分割的數(shù)據(jù)庫操作序列,也是數(shù)據(jù)庫并發(fā)控制的基本單位,其執(zhí)行結(jié)果必須使數(shù)據(jù)庫從一種一致性狀態(tài)切換到另一中一致性狀態(tài)。事務(wù)是邏輯上的一組操作,要么都執(zhí)行,要么都不執(zhí)行。事務(wù)能夠在數(shù)據(jù)庫提交工作時確保要么所有修改都保存,要么所有修改都不保存。即事務(wù)是邏輯上的一組操作,要么都執(zhí)行,要么都不執(zhí)行。
原子性(Atomicity)
原子性是整個數(shù)據(jù)庫事務(wù)中不可分割的工作單位,只有事務(wù)中的所有的數(shù)據(jù)庫操作都執(zhí)行成功,才代表整個事務(wù)成功,如果其中任一環(huán)節(jié)執(zhí)行失敗,那么就算已經(jīng)執(zhí)行成功的 SQL 語句也必須撤銷,回滾到事務(wù)執(zhí)行前的狀態(tài)。即原子性能夠保證 動作要么全部完成,要么完全不起作用。 即事務(wù)是最小的執(zhí)行單位,不允許分割。
一致性(Consistency)
指事務(wù)將數(shù)據(jù)庫從一種一致性狀態(tài)變?yōu)榱硪环N一致性狀態(tài)。在事務(wù)開始前后,數(shù)據(jù)庫的完整性約束未被破壞。在事務(wù)執(zhí)行前后,數(shù)據(jù)能夠保持一致,多個事務(wù)對統(tǒng)一數(shù)據(jù)讀取的結(jié)果相同。
隔離性(Isolation)
并發(fā)訪問數(shù)據(jù)庫時,隔離性要求每個讀寫事務(wù)對其他事務(wù)的操作對象能夠相互分離,即一個用戶的事務(wù)不被其他事務(wù)所干擾,各并發(fā)事務(wù)間數(shù)據(jù)庫是獨立的;
持久性(Durability)
表示事務(wù)一旦被提交,其結(jié)果就是永久性的,它對數(shù)據(jù)庫中數(shù)據(jù)的改變是持久的,即便數(shù)據(jù)庫發(fā)生故障也不應(yīng)該對其產(chǎn)生影響;
臟讀、幻讀 & 不可重復(fù)讀
了解事務(wù)隔離級別之前,先來看看這幾個讀的概念:
臟讀(Dirty Read)
表示某一事務(wù)已經(jīng)更新了一份數(shù)據(jù),另一個事務(wù)在此時讀取了同一份數(shù)據(jù)。當(dāng)前一個事務(wù)撤銷操作后,就會導(dǎo)致后一個事務(wù)所讀取的數(shù)據(jù)不正確。
幻讀(Phantom Read)
在一個事務(wù)的兩次查詢中數(shù)據(jù)量不一致,假如有一個事務(wù)查詢了幾列數(shù)據(jù),同時另一個事務(wù)中在此時查詢了新的數(shù)據(jù),則查詢事務(wù)在后續(xù)查詢中,就會發(fā)現(xiàn)數(shù)據(jù)比最開始的查詢數(shù)據(jù)更豐富。
不可重復(fù)讀(Non-repeatable Read)
一個事務(wù)中兩次查詢數(shù)據(jù)不一致,有可能是因為兩次查詢過程中插入了一個更新原有數(shù)據(jù)的事務(wù)。
注意:不可重復(fù)讀和幻讀的區(qū)別在于:
不可重復(fù)讀的重點在于修改, 比如多次讀取一條記錄發(fā)現(xiàn)其中某些列的值被修改,而 幻讀的重點在于新增或刪除,比如多次讀取一條記錄發(fā)現(xiàn)記錄增多或減少了。
隔離級別
SQL 標準定義了 4 個隔離級別,隔離級別從低到高分別是:
READ-UNCOMMITTED(讀取未提交)
最低的隔離級別,允許讀取尚未提交的數(shù)據(jù)變更,可能導(dǎo)致臟讀、幻讀或不可重復(fù)讀。
READ-COMMITTED(讀取已提交)
允許讀取并發(fā)事務(wù)已經(jīng)提交的數(shù)據(jù),能夠阻止臟讀,但可能導(dǎo)致幻讀或不可重復(fù)讀。
REPEATABLE-READ(可重復(fù)讀)
對同一字段的多次讀取結(jié)果時一致的,除非數(shù)據(jù)是被本身事務(wù)自己所修改,能夠阻止臟讀和不可重復(fù)讀,但可能導(dǎo)致幻讀。
SERIALIZABLE(可串行化)
最高的隔離級別,完全服從 ACID 的隔離級別,所有事務(wù)依次逐個執(zhí)行,這樣事務(wù)之間就完全不可能產(chǎn)生干擾,能夠防止臟讀、幻讀以及不可重復(fù)讀。
以下是 SQL 隔離級別和各種讀之間的關(guān)系:
| 隔離級別 | 臟讀 | 不可重復(fù)讀 | 幻讀 |
|---|---|---|---|
READ-UNCOMMITTED | ? | ? | ? |
READ-COMMITTED | ? | ? | ? |
REPEATABLE-READ | ? | ? | ? |
SERIALIZABLE | ? | ? | ? |
實例
關(guān)于回滾,主要涉及 Connection 對象,常用的三個方法如下:
| 返回值 | 方法 | 描述 |
|---|---|---|
void | setAutoCommit(boolean autoCommit) | 設(shè)定連接的自動提交模式,true 表示自動提交,false 表示手動提交 |
void | commit() | 使上次提交/回滾以來所做的所有更改成為永久更改,并釋放此 Connection 對象當(dāng)前持有的所有數(shù)據(jù)庫鎖 |
void | rollback() | 撤銷當(dāng)前十五中所做的所有更改,并釋放此 Connection 對象當(dāng)前持有的所有數(shù)據(jù)庫鎖 |
以下是一個回滾實例,我們當(dāng)我們第一次插入一條數(shù)據(jù)時,由于是新數(shù)據(jù),所以不會報錯,但是如果我們執(zhí)行一次程序之后再次執(zhí)行,此時按理來說就會報錯,因為插入的數(shù)據(jù)重復(fù),這時候利用事務(wù)就可以十分方便的解決這個問題,我們設(shè)置插入出錯就回滾到未出錯之前的狀態(tài),這樣就能保證插入數(shù)據(jù)不會報錯了。
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.SQLException;
/**
* @author : cunyu
* @version : 1.0
* @className : AffairTest
* @date : 2021/4/23 22:35
* @description : 事務(wù)
*/
public class AffairTest {
public static void main(String[] args) {
try {
Class.forName("com.mysql.cj.jdbc.Driver");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
String url = "jdbc:mysql://localhost:3306/javalearning?characterEncoding=UTF-8";
String username = "root";
String password = "12345";
String insertString = "INSERT INTO students VALUES (?,?,?,?,?)";
Connection connection = null;
PreparedStatement preparedStatement = null;
try {
connection = DriverManager.getConnection(url, username, password);
// 關(guān)閉自動提交
connection.setAutoCommit(false);
preparedStatement = connection.prepareStatement(insertString);
System.out.println("連接成功");
// 依次插入數(shù)據(jù)
preparedStatement.setLong(1, 401);
preparedStatement.setString(2, "小紫");
preparedStatement.setInt(3, 0);
preparedStatement.setLong(4, 4);
preparedStatement.setLong(5, 88);
preparedStatement.executeUpdate();
// 如果沒有出錯,則提交事務(wù)
connection.commit();
System.out.println("插入數(shù)據(jù)成功");
} catch (SQLException throwables) {
// 一旦出錯,則回滾事務(wù)
try {
connection.rollback();
} catch (SQLException e) {
e.printStackTrace();
}
} finally {
// 最后關(guān)閉連接
if (connection != null) {
try {
connection.close();
} catch (SQLException throwables) {
throwables.printStackTrace();
}
}
if (preparedStatement != null) {
try {
preparedStatement.close();
} catch (SQLException throwables) {
throwables.printStackTrace();
}
}
}
}
}
除了上述回滾的方式外,JDBC 還支持設(shè)置保存點的方式,我們可以使用事務(wù)回滾到指定的保存點,主要涉及的方法如下:
setSavepoint(String savePointName):創(chuàng)建新的保存點,返回一個SavePoint對象;rollback(String savePointName):回滾到指定保存點;
連接池
簡介
當(dāng)我們使用多線程時,每個線程如果都需要連接數(shù)據(jù)庫來執(zhí)行 SQL 語句,那么每個線程都得創(chuàng)建一個連接,然后在使用之后關(guān)閉。這個創(chuàng)建和關(guān)閉連接的過程是十分耗時的,一旦多線程并發(fā)時,就容易導(dǎo)致系統(tǒng)卡頓。針對這一問題,提出使用數(shù)據(jù)庫連接池。數(shù)據(jù)庫連接池,其實就相當(dāng)于一個集合,是一個存放數(shù)據(jù)庫連接的容器。當(dāng)我們的系統(tǒng)初始化好之后,集合就被創(chuàng)建,集合中會申請一些連接對象,當(dāng)用戶來訪問數(shù)據(jù)庫時,從集合中獲取連接對象,一旦用戶訪問完畢,就將連接對象返還給容器。
使用數(shù)據(jù)庫連接池的優(yōu)點:一來是節(jié)約資源,二來提高了用戶訪問的效率。
常用數(shù)據(jù)庫連接池
C3P0
導(dǎo)包
首先需要導(dǎo)包,先去下載 C3P0 對象的 jar 包,下載地址:https://sourceforge.net/projects/c3p0/,然后將其中的如下兩個包導(dǎo)入;

定義配置文件
創(chuàng)建 C3P0 對應(yīng)的配置文件,注意:配置文件一般放在 src 路徑下,而且文件的名稱要必須為以下其中的一個:
c3p0.propertiesc3p0-config.xml
<c3p0-config>
<!-- 使用默認的配置讀取連接池對象 -->
<default-config>
<!-- 連接參數(shù) -->
<property name="driverClass">com.mysql.cj.jdbc.Driver</property>
<property name="jdbcUrl">jdbc:mysql://localhost:3306/javalearning?characterEncoding=UTF-8</property>
<property name="user">root</property>
<property name="password">0908</property>
<!-- 連接池參數(shù) -->
<!-- 初始化申請的連接數(shù)-->
<property name="initialPoolSize">5</property>
<!-- 最大連接數(shù)-->
<property name="maxPoolSize">10</property>
<!-- 超時時間-->
<property name="checkoutTimeout">3000</property>
</default-config>
</c3p0-config>
創(chuàng)建連接池對象
獲取連接對象
import com.mchange.v2.c3p0.ComboPooledDataSource;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.SQLException;
/**
* @author : cunyu
* @version : 1.0
* @className : C3POTest
* @date : 2021/4/24 16:01
* @description : C3PO 連接池
*/
public class C3POTest {
public static void main(String[] args) {
// 創(chuàng)建數(shù)據(jù)庫連接池對象
DataSource dataSource = new ComboPooledDataSource();
// 獲取連接對象
try {
Connection connection = dataSource.getConnection();
System.out.println(connection);
} catch (SQLException throwables) {
throwables.printStackTrace();
}
}
}

Druid
導(dǎo)包
導(dǎo)入 Druid 的 jar 包,下載地址:https://repo1.maven.org/maven2/com/alibaba/druid/
定義配置文件
配置文件名稱無要求,但是后綴名為 .properties,而且可以存放在任意目錄下;
driver=com.mysql.cj.jdbc.Driver
url=jdbc:mysql://localhost:3306/javalearning?characterEncoding=UTF-8
username=root
password=12345
initialSize=5
maxActive=10
maxWait=3000
加載配置文件 創(chuàng)建連接池對象 獲取連接對象
import com.alibaba.druid.pool.DruidDataSourceFactory;
import javax.sql.DataSource;
import java.io.InputStream;
import java.sql.Connection;
import java.util.Properties;
/**
* @author : cunyu
* @version : 1.0
* @className : DruidTest
* @date : 2021/4/24 19:56
* @description : Druid 連接池
*/
public class DruidTest {
public static void main(String[] args) {
try {
// 加載配置文件
Properties properties = new Properties();
InputStream resourceAsStream = DruidTest.class.getClassLoader().getResourceAsStream("druid.properties");
properties.load(resourceAsStream);
// 獲取連接池對象
DataSource dataSource = DruidDataSourceFactory.createDataSource(properties);
// 獲取連接
Connection connection = dataSource.getConnection();
System.out.println(connection);
} catch (Exception e) {
e.printStackTrace();
}
}
}

總結(jié)
今天的內(nèi)容到此就結(jié)束了,老規(guī)矩,點贊關(guān)注走一波 ??。
對于文中有錯或遺漏的地方,還煩請各位大佬在評論區(qū)指出來。我是村雨遙,一個技術(shù)棧主要為 Java 的菜鳥程序員,關(guān)注我,一起學(xué)習(xí)成長吧!



