一、持久化機制
持久化(persistence): 把資料保存到可掉電式存盤設備中以供之后使用,大多數情況下,特別是企業級應用,資料持久化意味著將記憶體中的資料保存到硬碟上加以”固化”,而持久化的實作程序大多通過各種關系資料庫來完成,就是將記憶體中的資料存盤在關系型資料庫中,當然也可以存盤在磁盤檔案、XML資料檔案中, 而在 Java中,資料庫存取技術只能通過 JDBC 來訪問資料庫,
JDBC 訪問資料庫的形式主要有兩種:
- 直接使用 JDBC 的 API 去訪問資料庫服務器 (MySQL/Oracle),
- 間接地使用 JDBC 的 API 去訪問資料庫服務器,使用第三方O/R Mapping工具,如 Hibernate, MyBatis 等,(底層依然是 JDBC ,JDBC 是 Java 訪問資料庫的基石,其他技術都是對 JDBC 的封裝)
二、JDBC概述
JDBC (Java DataBase Connectivity)是一個獨立于特定資料庫管理系統、通用的SQL資料庫存取和操作的公共介面(一組API),定義了用來訪問資料庫的標準Java類別庫,(java.sql,javax.sql)使用這些類別庫可以以一種標準的方法、方便地訪問資料庫資源, JDBC為訪問不同的資料庫提供了一種統一的途徑,為開發者屏蔽了一些細節問題, JDBC 的目標是使 Java 程式員使用 JDBC 可以連接任何提供了 JDBC 實作 (驅動程式) 的資料庫系統,這 樣就使得程式員無需對特定的資料庫系統的特點有過多的了解,從而大大簡化和加快了開發程序,

簡單來說,JDBC 本身是 Java 連接資料庫的一個標準,是進行資料庫連接的抽象層,由 Java撰寫的一組類和介面組成,介面的實作由各個資料庫廠商來完成,
JDBC撰寫的步驟如下:

ODBC(Open Database Connectivity,開放式資料庫連接),是微軟在Windows平臺下推出的,使用者在程式中只需要呼叫ODBC API,由 ODBC 驅動程式將呼叫轉換成為對特定的資料庫的呼叫請求,
三、獲取資料庫連接
3.1、加載注冊驅動
java.sql.Driver 介面是所有 JDBC 驅動程式需要實作的介面,這個介面是提供給資料庫廠商使用的,不同資料庫廠商提供不同的實作,在程式中不需要直接去訪問實作了 Driver 介面的類,而是由驅動程式管理器類(java.sql.DriverManager)去呼叫這些Driver實作,
- Oracle的驅動:oracle.jdbc.driver.OracleDriver
- mySql的驅動: com.mysql.jdbc.Driver
加載驅動:加載 JDBC 驅動需呼叫 Class 類的靜態方法 forName(),向其傳遞要加載的 JDBC 驅動的類名
- Class.forName(“com.mysql.jdbc.Driver”);
3.2、注冊驅動的原理
? 注冊驅動:DriverManager 類是驅動程式管理器類,負責管理驅動程式
-
使用DriverManager.registerDriver(com.mysql.jdbc.Driver)來注冊驅動
-
通常不用顯式呼叫 DriverManager 類的 registerDriver() 方法來注冊驅動程式類的實體,因為 Driver 介面的驅動程式類都包含了靜態代碼塊,在這個靜態代碼塊中,會呼叫 DriverManager.registerDriver() 方法來注冊自身的一個實體,下圖是MySQL的Driver實作類的原始碼:

? 他通常有以下兩個步驟:
1. 把 com.mysql.jdbc.Driver 這一份位元組碼加載進 JVM,
2. 位元組碼被加載進JVM,就會執行其靜態代碼塊.而其底層的靜態代碼塊在完成注冊驅動作業,將驅動注
冊到DriverManger 中,
復制代碼
3.3、獲取連接物件
? 我們一般使用 DriverManager 的 getConnection 方法創建 Connection 物件
Connection conn = DriverManager.getConnection(url,username,password);
// url=jdbc:mysql://localhost:3306/jdbcdemo
// 如果連接的是本機的 MySQL,并且埠是默認的 3306 ,則可以簡寫:
// url=jdbc:mysql:///jdbcdemo
// username:當前訪問資料庫的用戶名
// password:當前訪問資料庫的密碼
復制代碼
3.3.1、URL詳解
JDBC URL 用于標識一個被注冊的驅動程式,驅動程式管理器通過這個 URL 選擇正確的驅動程式,從而建立到資料庫的連接,
JDBC URL的標準由三部分組成(協議:子協議:子名稱),各部分間用冒號分隔,
- 協議:JDBC URL中的協議總是jdbc(固定寫法),
- 子協議:子協議用于標識一個資料庫驅動程式,
- 子名稱:一種標識資料庫的方法,子名稱可以依不同的子協議而變化,用子名稱的目的是為了定位資料庫提供足夠的資訊,包含主機名(對應服務端的ip地址),埠號,資料庫名,

- url的常見寫法:
- jdbc:mysql://主機名稱:mysql服務埠號/資料庫名稱?引數=值&引數=值
- jdbc:mysql://localhost:3306/atguigu
- jdbc:mysql://localhost:3306/atguigu**?useUnicode=true&characterEncoding=utf8**(如果JDBC程式與服務器端的字符集不一致,會導致亂碼,那么可以通過引數指定服務器端的字符集)
- jdbc:mysql://localhost:3306/atguigu?user=root&password=123456
3.4、資料庫連接方法
3.4.1、方式一
我們可以通過直接獲取注冊驅動,將資料庫資訊寫在代碼中,這個的問題也是很明顯的,那就是一個硬編碼問題,當我們需要修改資料庫的時候,需要直接修改Java源代碼,
@Test
public void testConnection4() {
try {
//1.資料庫連接的4個基本要素:
String url = "jdbc:mysql://localhost:3306/test";
String user = "root";
String password = "admin";
String driverName = "com.mysql.jdbc.Driver";
//2.加載驅動 (①實體化Driver ②注冊驅動)
Class.forName(driverName);
//3.獲取連接
Connection conn = DriverManager.getConnection(url, user, password);
System.out.println(conn);
} catch (Exception e) {
e.printStackTrace();
}
}
復制代碼
3.4.2、方式二
我們為了解決方式一硬編碼的問題,需要將驅動資訊放入組態檔,然后通過讀取的方式來進行賦值,
@Test
public void testConnection5() throws Exception {
//1.加載組態檔
InputStream is = ConnectionTest.class.getClassLoader().getResourceAsStream("jdbc.properties");
Properties pros = new Properties();
pros.load(is);
//2.讀取配置資訊
String user = pros.getProperty("user");
String password = pros.getProperty("password");
String url = pros.getProperty("url");
String driverClass = pros.getProperty("driverClass");
//3.加載驅動
Class.forName(driverClass);
//4.獲取連接
Connection conn = DriverManager.getConnection(url,user,password);
System.out.println(conn);
}
復制代碼
? 使用組態檔的好處:
-
實作了代碼和資料的分離,如果需要修改配置資訊,直接在組態檔中修改,不需要深入代碼,
-
如果修改了配置資訊,省去重新編譯的程序,
四、DAO思想
4.1、沒有DAO
在沒有 DAO 的時候,我們的代碼存在大量的重復,

4.2、DAO介紹
DAO(Data Access Object) 資料訪問物件是一個面向物件的資料庫介面. 顧名思義就是與資料庫打交道,夾在業務邏輯與資料庫資源中間,將所有對資料源的訪問操作抽象封裝在一個公共 API 中,程式書寫就是建立一個介面,介面中定義了此應用程式中將會用到的所有事務方法,DAO 中的主要操作: 增刪改查(CRUD),

通過以上圖,DAO 作為組件,那其主要的是方法的設計,方法設計需要注意什么呢?
- 在保存功能中,呼叫者需要傳遞多個引數進來,然后把這些資料保存到資料庫中,
- 在查詢功能中,結果集的每行資料有多個列的值,然后把這些資料回傳給呼叫者,
- 在開發程序中,如果遇到需要傳遞的資料有多個的時候,通常需要使用 JavaBean 對其進行封裝
4.3、DAO規范
DAO本質就是一個可以重復使用的組件,他包括了兩個規范:
- 分包規范(域名倒寫.專案模塊名.組件)
cn.util; //存放工具類
cn.domain; //裝pss模塊的domain類,模型物件.(Student)
cn.dao; //裝pss模塊的dao介面.呼叫者將需要保存的資料封裝到一個物件中,然后傳遞進來
cn.dao.impl; //裝dao介面的實作類.
cn.test; //暫時存盤DAO的測驗類,以后的測驗類不應該放這里.
復制代碼
- 命名規范
- DAO 介面 : 表示對某個模型的 CRUD 操作做規范,以 I 開頭,interface,例如: IStudentDAO
- DAO 實作類: 表示對某個 DAO 介面的實作,例如:EmployeeDAOImpl
- DAO 測驗類: 測驗 DAO 組件中的所有方法,例如:EmployeeDAOTest
呼叫建議:面向介面編程
-
傳統的做法 : EmployeeDAOImpl dao = new EmployeeDAOImpl();
-
面向介面編程 : IEmployeeDAO dao = new EmployeeDAOImpl();
五、JDBC之CRUD操作
5.1、Statement物件及其弊端
5.1.1、Statement物件
Statement物件時用于執行靜態 SQL 陳述句并回傳它所生成結果的物件,

- 通過呼叫 Connection 物件的 createStatement() 方法創建該物件,該物件用于執行靜態的 SQL 陳述句,并且回傳執行結果,
- Statement 介面中定義了下列方法用于執行 SQL 陳述句:
int excuteUpdate(String sql):執行更新操作INSERT、UPDATE、DELETE
ResultSet executeQuery(String sql):執行查詢操作SELECT
復制代碼
5.1.2、使用Statement物件的弊端
用Statement操作資料表存在弊端:
- 存在拼串操作,繁瑣
- 存在SQL注入問題
SQL 注入是利用某些系統沒有對用戶輸入的資料進行充分的檢查,而在用戶輸入資料中注入非法的 SQL 陳述句段或命令(如:SELECT user, password FROM user_table WHERE user='a' OR 1 = ' AND password = ' OR '1' = '1') ,從而利用系統的 SQL 引擎完成惡意行為的做法,
public class StatementTest {
// 使用Statement的弊端:需要拼寫sql陳述句,并且存在SQL注入的問題
@Test
public void testLogin() {
Scanner scan = new Scanner(System.in);
System.out.print("用戶名:");
String userName = scan.nextLine();
System.out.print("密 碼:");
String password = scan.nextLine();
// SELECT user,password FROM user_table WHERE USER = '1' or ' AND PASSWORD = '='1' or '1' = '1';
String sql = "SELECT user,password FROM user_table WHERE USER = '" + userName + "' AND PASSWORD = '" + password
+ "'";//字串拼接過于繁雜
User user = get(sql, User.class);
if (user != null) {
System.out.println("登陸成功!");
} else {
System.out.println("用戶名或密碼錯誤!");
}
}
// 使用Statement實作對資料表的查詢操作
public <T> T get(String sql, Class<T> clazz) {
T t = null;
Connection conn = null;
Statement st = null;
ResultSet rs = null;
try {
// 1.加載組態檔
InputStream is = StatementTest.class.getClassLoader().getResourceAsStream("jdbc.properties");
Properties pros = new Properties();
pros.load(is);
// 2.讀取配置資訊
String user = pros.getProperty("user");
String password = pros.getProperty("password");
String url = pros.getProperty("url");
String driverClass = pros.getProperty("driverClass");
// 3.加載驅動
Class.forName(driverClass);
// 4.獲取連接
conn = DriverManager.getConnection(url, user, password);
st = conn.createStatement();
rs = st.executeQuery(sql);
// 獲取結果集的元資料
ResultSetMetaData rsmd = rs.getMetaData();
// 獲取結果集的列數
int columnCount = rsmd.getColumnCount();
if (rs.next()) {
t = clazz.newInstance();
for (int i = 0; i < columnCount; i++) {
// //1. 獲取列的名稱
// String columnName = rsmd.getColumnName(i+1);
// 1. 獲取列的別名
String columnName = rsmd.getColumnLabel(i + 1);
// 2. 根據列名獲取對應資料表中的資料
Object columnVal = rs.getObject(columnName);
// 3. 將資料表中得到的資料,封裝進物件
Field field = clazz.getDeclaredField(columnName);
field.setAccessible(true);
field.set(t, columnVal);
}
return t;
}
} catch (Exception e) {
e.printStackTrace();
} finally {
// 關閉資源
if (rs != null) {
try {
rs.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if (st != null) {
try {
st.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if (conn != null) {
try {
conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
return null;
}
}
復制代碼

5.2、PreparedStatement
我們可以通過呼叫 Connection 物件的 preparedStatement(String sql) 方法獲取PreparedStatement物件,PreparedStatement 介面是 Statement 的子介面,它表示一條預編譯過的 SQL 陳述句,
PreparedStatement 物件所代表的 SQL 陳述句中的引數用問號(?)來表示,呼叫PreparedStatement物件的 setXxx() 方法來設定這些引數. setXxx() 方法有兩個引數,第一個引數是要設定的 SQL 陳述句中的引數的索引(從 1 開始),第二個是設定的 SQL 陳述句中的引數的值,
// 常用方法:
void setXxx(int parameterIndex,Xxx value); //設定第幾個占位符的真正引數值.
// Xxx 表示資料型別,比如 String,int,long,Date等.
void setObject(int parameterIndex, Object x); //設定第幾個占位符的真正引數值.
int executeUpdate(); //執行DDL/DML陳述句. 注意:沒有引數
// 若當前 SQL是 DDL陳述句,則回傳 0.
// 若當前 SQL是 DML陳述句,則回傳受影響的行數.
ResultSet executeQuery(); //執行DQL陳述句,回傳結果集.
close(); //釋放資源
復制代碼
5.3、PreparedStatement vs Statement
-
PreparedStatement物件比Statement物件的代碼的可讀性和可維護性, -
PreparedStatement能最大可能提高性能,- DBServer會對預編譯陳述句提供性能優化,因為預編譯陳述句有可能被重復呼叫,所以陳述句在被DBServer的編譯器編譯后的執行代碼被快取下來,那么下次呼叫時只要是相同的預編譯陳述句就不需要編譯,只要將引數直接傳入編譯過的陳述句執行代碼中就會得到執行,
- 在statement陳述句中,即使是相同操作但因為資料內容不一樣,所以整個陳述句本身不能匹配,沒有快取陳述句的意義.事實是沒有資料庫會對普通陳述句編譯后的執行代碼快取,這樣每執行一次都要對傳入的陳述句編譯一次,
-
PreparedStatement 可以防止 SQL 注入 ,
5.4、 ResultSet
查詢需要呼叫PreparedStatement 的 executeQuery() 方法,查詢結果是一個ResultSet 物件,ResultSet 物件以邏輯表格的形式封裝了執行資料庫操作的結果集,ResultSet 介面由資料庫廠商提供實作,
ResultSet 回傳的實際上就是一張資料表,有一個指標指向資料表的第一條記錄的前面,
ResultSet 物件維護了一個指向當前資料行的游標,初始的時候,游標在第一行之前,可以通過 ResultSet 物件的 next() 方法移動到下一行,呼叫 next()方法檢測下一行是否有效,若有效,該方法回傳 true,且指標下移,相當于Iterator物件的hasNext()和next()方法的結合體,
當指標指向一行時, 可以通過呼叫 getXxx(int index) 或 getXxx(int columnName) 獲取每一列的值,*Java與資料庫互動涉及到的相關Java API中的索引都從1開始,*例如:
getInt(1), getString("name")
復制代碼

5.5、CRUD操作
5.5.1、User類
package domain;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* @author Xiao_Lin
* @date 2021/1/2 19:44
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {
//id
private Integer id;
//用戶名
private String username;
//密碼
private String pwd;
//構造方法的多載
public User(String username, String pwd) {
this.username = username;
this.pwd = pwd;
}
}
復制代碼
5.5.2、IUserDAO
package dao;
import domain.User;
import java.util.List;
/**
* @author Xiao_Lin
* @date 2021/1/2 19:46
*/
public interface IUserDAO {
public void insert(User user);
public void delete(Integer id);
public void update(User user);
public List<User> selectAll();
public User selectUserById(Integer id);
}
復制代碼
5.5.3、UserDAOImpl
package dao.impl;
import dao.IUserDAO;
import dao.utils.DaoUtils;
import domain.User;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
/**
* @author Xiao_Lin
* @date 2021/1/2 19:49
*/
public class UserDAOImpl implements IUserDAO {
Connection connection = null;
PreparedStatement ps = null;
ResultSet rs = null;
@Override
public void insert(User user) {
try {
connection = DaoUtils.getConnection();
String sql = "insert into user (username,pwd) values (?,?)";
ps = connection.prepareStatement(sql);
ps.setString(1,user.getUsername());
ps.setString(2,user.getPwd());
ps.executeUpdate();
System.out.println("添加成功!!");
} catch (SQLException e) {
e.printStackTrace();
}finally {
DaoUtils.close(connection,ps,null);
}
}
@Override
public void delete(Integer id) {
try {
connection = DaoUtils.getConnection();
ps = connection.prepareStatement("delete from user where id = ?");
ps.setInt(1,id);
ps.executeUpdate();
System.out.println("洗掉成功!!");
} catch (Exception e) {
e.printStackTrace();
}finally {
DaoUtils.close(connection,ps,null);
}
}
@Override
public void update(User user) {
try {
connection = DaoUtils.getConnection();
ps = connection.prepareStatement("update user set username = ? , pwd = ? where id = ?");
ps.setString(1,user.getUsername());
ps.setString(2,user.getPwd());
ps.setInt(3,user.getId());
ps.executeUpdate();
System.out.println("修改成功");
} catch (Exception e) {
e.printStackTrace();
}finally {
DaoUtils.close(connection,ps,null);
}
}
@Override
public List<User> selectAll() {
List<User> users = new ArrayList<>();
try {
connection = DaoUtils.getConnection();
ps = connection.prepareStatement("select * from user");
rs = ps.executeQuery();
while (rs.next()){
users.add(new User(rs.getInt("id"),rs.getString("username"),rs.getString("pwd")));
}
} catch (Exception e) {
e.printStackTrace();
}finally {
DaoUtils.close(connection,ps,rs);
}
return users;
}
@Override
public User selectUserById(Integer id) {
User user = null;
try {
connection = DaoUtils.getConnection();
ps = connection.prepareStatement("select * from user where id = ?");
ps.setInt(1,id);
rs = ps.executeQuery();
while (rs.next()){
user = new User(rs.getInt("id"),rs.getString("username"),rs.getString("pwd"));
}
} catch (Exception e) {
e.printStackTrace();
}finally {
DaoUtils.close(connection,ps,rs);
}
return user;
}
}
復制代碼
5.5.4、DaoUtils
package dao.utils;
import java.io.IOException;
import java.io.InputStream;
import java.sql.Connection;
import java.sql.Driver;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Properties;
/**
* @author Xiao_Lin
* @date 2021/1/2 19:56
*/
public class DaoUtils {
static Connection connection = null;
static Properties properties = null;
static {
InputStream resourceAsStream = Thread.currentThread().getContextClassLoader()
.getResourceAsStream("db.properties");
properties = new Properties();
try {
properties.load(resourceAsStream);
Class.forName(properties.getProperty("DriverClassName"));
} catch (Exception e) {
e.printStackTrace();
}
}
public static Connection getConnection() throws SQLException {
connection= DriverManager.getConnection(properties.getProperty("url"),properties.getProperty("username"),properties.getProperty("password"));
return connection;
}
public static void close(Connection conn , PreparedStatement ps , ResultSet rs){
try {
if(rs!=null){
rs.close();
}
} catch (SQLException e) {
e.printStackTrace();
}finally{
try {
if(ps!=null){
ps.close();
}
} catch (SQLException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}finally{
try {
if(conn!=null){
conn.close();
}
} catch (SQLException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
}
復制代碼
5.5.5、測驗類
package dao.impl;
import dao.IUserDAO;
import dao.utils.DaoUtils;
import domain.User;
import java.sql.SQLException;
import java.util.List;
import org.junit.Test;
/**
* @author Xiao_Lin
* @date 2021/1/2 20:11
*/
public class UserDAOImplTest {
IUserDAO userDAO = new UserDAOImpl();
@Test
public void insert() {
userDAO.insert(new User("ghy","123"));
}
@Test
public void delete() {
userDAO.delete(14);
}
@Test
public void update() {
userDAO.update(new User(1,"張三","666"));
}
@Test
public void selectAll() {
List<User> users = userDAO.selectAll();
users.forEach(System.out::print);
}
@Test
public void selectUserById() {
User user = userDAO.selectUserById(1);
System.out.println(user);
}
}
復制代碼
六、操作BLOB型別欄位
6.1、BLOB型別簡介
MySQL中,BLOB是一個二進制大型物件,是一個可以存盤大量資料的容器,它能容納不同大小的資料,**插入BLOB型別的資料必須使用PreparedStatement,因為BLOB型別的資料無法使用字串拼接寫的,**MySQL有四種BLOB型別,他們除了在存盤的最大資訊量上不同外,除此之外他們是等同的,
如果在指定了相關的Blob型別以后,還報錯:xxx too large,那么在mysql的安裝目錄下,找my.ini檔案加上如下的配置引數: max_allowed_packet=16M,同時注意:修改了my.ini檔案之后,需要重新啟動mysql服務,

6.2、插入BLOB型別
//獲取連接
Connection conn = JDBCUtils.getConnection();
String sql = "insert into customers(name,email,birth,photo)values(?,?,?,?)";
PreparedStatement ps = conn.prepareStatement(sql);
// 填充占位符
ps.setString(1, "張三");
ps.setString(2, "zs@126.com");
ps.setDate(3, new Date(new java.util.Date().getTime()));
// 操作Blob型別的變數
FileInputStream fis = new FileInputStream("zs.png");
ps.setBlob(4, fis);
//執行
ps.execute();
fis.close();
JDBCUtils.closeResource(conn, ps);
復制代碼
6.3、修改BLOB型別
Connection conn = JDBCUtils.getConnection();
String sql = "update customers set photo = ? where id = ?";
PreparedStatement ps = conn.prepareStatement(sql);
// 填充占位符
// 操作Blob型別的變數
FileInputStream fis = new FileInputStream("coffee.png");
ps.setBlob(1, fis);
ps.setInt(2, 25);
ps.execute();
fis.close();
JDBCUtils.closeResource(conn, ps);
復制代碼
6.4、從資料庫表中讀取BLOG型別
String sql = "SELECT id, name, email, birth, photo FROM customer WHERE id = ?";
conn = getConnection();
ps = conn.prepareStatement(sql);
ps.setInt(1, 8);
rs = ps.executeQuery();
if(rs.next()){
Integer id = rs.getInt(1);
String name = rs.getString(2);
String email = rs.getString(3);
Date birth = rs.getDate(4);
Customer cust = new Customer(id, name, email, birth);
System.out.println(cust);
//讀取Blob型別的欄位
Blob photo = rs.getBlob(5);//這里也可以通過列的索引來讀取
InputStream is = photo.getBinaryStream();
OutputStream os = new FileOutputStream("c.jpg");
byte [] buffer = new byte[1024];
int len = 0;
while((len = is.read(buffer)) != -1){
os.write(buffer, 0, len);
}
JDBCUtils.closeResource(conn, ps, rs);
if(is != null){
is.close();
}
if(os != null){
os.close();
}
}
復制代碼
七、批量處理
八、資料庫事務
8.1、問題引出
案例: 銀行轉賬, 從張無忌賬戶上給趙敏轉 1000 塊錢,
我們有一張account(賬戶表),然后我們開始轉賬,
| id | name | balance |
|---|---|---|
| 1 | 張無忌 | 20000 |
| 2 | 趙敏 | 0 |
轉賬的步驟大概細分為以下幾個步驟:
- 查詢張無忌的賬戶余額是否大于等于1000,余額小于1000就提示溫馨提示:親,你的余額不足, 如果余額大于等于1000轉賬,
SELECT * FROM account WHERE name = '張無忌' AND balance >= 1000;
復制代碼
- 從張無忌的賬戶余額中減少1000,
UPDATE account SET balance = balance - 1000 WHERE name = '張無忌';
復制代碼
- 在趙敏的賬戶余額中增加1000,
UPDATE account SET balance = balance + 1000 WHERE name = '趙敏';
復制代碼
這個時候問題來了,當程式執行到第②步和第③步之間,突然出現一個例外,此時會造成轉賬前后資料不一致的問題,會造成轉賬了,但是對方的賬戶上沒有多錢, 造成這個問題的根本原因是因為轉入轉出是兩個單獨的操作,其中一個失敗后,不會影響到另一個的執行,但是在轉賬這個業務中,我們需要保證進出兩個操作要么都成功,要么都失敗,
這個時候就需要引出事務的概念,
import org.junit.Test;
@Test
public void testTx() throws Exception {
// 賈璉欲執事
// 1 查詢張無忌的賬戶余額是否大于等于1000
Connection conn = JDBCUtil.getConnection();
String sql = "SELECT * FROM account WHERE balance>=? AND name=?";
PreparedStatement pst = conn.prepareStatement(sql);
// 給 ? 設定資料
pst.setBigDecimal(1,new BigDecimal("1000"));
pst.setString(2,"張無忌");
ResultSet rs = pst.executeQuery();
if(!rs.next()){
System.out.println("余額不足");
return;
}
// 2 從張無忌的賬戶余額中減少1000.
sql = "UPDATE account SET balance = balance-? WHERE name=?";
pst = conn.prepareStatement(sql);
//設定? 的資料
pst.setBigDecimal(1,new BigDecimal("1000"));
pst.setString(2,"張無忌");
pst.executeUpdate();
// 模擬出例外
int a = 10/0;
// 3 在趙敏的賬戶余額中增加1000.
sql = "UPDATE account SET balance = balance+? WHERE name=?";
pst = conn.prepareStatement(sql);
//設定? 的資料
pst.setBigDecimal(1,new BigDecimal("1000"));
pst.setString(2,"趙敏");
pst.executeUpdate();
// 釋放資源
JDBCUtil.close(conn,pst,rs);
}
復制代碼
8.2、事務
事務(Transaction,簡寫為tx):一組邏輯操作單元,使資料從一種狀態變換到另一種狀態,
事務處理(事務操作):保證所有事務都作為一個作業單元來執行,即使出現了故障,都不能改變這種執行方式,當在一個事務中執行多個操作時,要么所有的事務都被提交(commit),那么這些修改就永久地保存下來;要么資料庫管理系統將放棄所作的所有修改,整個事務**回滾(rollback)**到最初狀態,回滾可以看成是撤銷操作,
為確保資料庫中資料的一致性,資料的操縱應當是離散的成組的邏輯單元:當它全部完成時,資料的一致性可以保持,而當這個單元中的一部分操作失敗,整個事務應全部視為錯誤,所有從起始點以后的操作應全部回退到開始狀態,
8.3、事務的ACID屬性
-
原子性(Atomicity) 原子性是指事務是一個不可分割的作業單位,事務中的操作要么都發生,要么都不發生,
-
一致性(Consistency) 事務必須使資料庫從一個一致性狀態變換到另外一個一致性狀態,
-
隔離性(Isolation) 事務的隔離性是指一個事務的執行不能被其他事務干擾,即一個事務內部的操作及使用的資料對并發的其他事務是隔離的,并發執行的各個事務之間不能互相干擾,
-
持久性(Durability) 持久性是指一個事務一旦被提交,它對資料庫中資料的改變就是永久性的,接下來的其他操作和資料庫故障不應該對其有任何影響,
8.4、資料庫的并發問題
對于同時運行的多個事務, 當這些事務訪問資料庫中相同的資料時, 如果沒有采取必要的隔離機制, 就會導致各種并發問題:
- 臟讀: 對于兩個事務 T1, T2, T1 讀取了已經被 T2 更新但還沒有被提交的欄位,之后, 若 T2 回滾, T1讀取的內容就是臨時且無效的,
- 不可重復讀: 對于兩個事務T1, T2, T1 讀取了一個欄位, 然后 T2 更新了該欄位,之后, T1再次讀取同一個欄位, 值就不同了,
- 幻讀: 對于兩個事務T1, T2, T1 從一個表中讀取了一個欄位, 然后 T2 在該表中插入了一些新的行,之后, 如果 T1 再次讀取同一個表, 就會多出幾行,
8.4.1、事務的隔離性
資料庫事務的隔離性: 資料庫系統必須具有隔離并發運行各個事務的能力, 使它們不會相互影響, 避免各種并發問題,
一個事務與其他事務隔離的程度稱為隔離級別,資料庫規定了多種事務隔離級別, 不同隔離級別對應不同的干擾程度, 隔離級別越高, 資料一致性就越好, 但并發性越弱,
8.4.2、資料庫的隔離級別
MySQL資料庫支持4種事務隔離級別,Mysql 默認的事務隔離級別為: REPEATABLE READ,

8.4.3、設定隔離級別
每啟動一個 mysql 程式, 就會獲得一個單獨的資料庫連接. 每個資料庫連接都有一個全域變數 @@tx_isolation, 表示當前的事務隔離級別,
8.4.3.1、查看當前的隔離級別
SELECT @@tx_isolation;
復制代碼
8.4.3.2、設定當前mysql隔離級別
set transaction isolation level read committed;
復制代碼
8.4.3.3、設定mysql的全域隔離級別
set global transaction isolation level read committed;
復制代碼
8.5、事務的操作步驟
- 先定義開始一個事務,然后對資料作修改操作,
- 執行程序中,如果沒有問題就
提交(commit)事務,此時的修改將永久地保存下來, - 如果執行程序中有問題(例外),回滾事務(rollback),資料庫管理系統將放棄所作的所有修改而回到 開始事務時的狀態,
try{
//取消事務的自動提交機制,設定為手動提交.
connection物件.setAutoCommit(false);
//操作1
//操作2
//例外
//操作3
//....
//手動提交事務
connection物件.commit();
}catch(Exception e){
//處理例外
//回滾事務
connection物件.rollback();
}
復制代碼
8.6、事務的注意事項
- 在默認情況下,事務會在執行完DML操作后會自動提交,
- 在進行查詢操作的時候一般是不需要事務的,但是我們一般也會在查詢中寫事務
- 在寫代碼的時候,如果代碼完全正常沒有例外,但是資料庫中的資料沒有任何改變的話,說明是沒有提交事務,
- 在MySQL中,只有InnoDB存盤引擎支持事務,支持外鍵,MyISAM是不支持事務的,
- 以后處理事務的時候,必須在service層中進行控制,
九、連接池
9.1、JDBC資料庫連接池的必要性
在使用開發基于資料庫的web程式時,傳統的模式基本是按以下步驟:
-
在主程式中建立資料庫連接,
-
進行sql操作,
-
斷開資料庫連接,
這種模式會存在幾個很顯著的問題:
-
普通的JDBC資料庫連接使用 DriverManager 來獲取,每次向資料庫建立連接的時候都要將 Connection 加載到記憶體中,再驗證用戶名和密碼(得花費0.05s~1s的時間),需要資料庫連接的時候,就向資料庫要求一個,執行完成后再斷開連接,這樣的方式將會消耗大量的資源和時間,**資料庫的連接資源并沒有得到很好的重復利用,**若同時有幾百人甚至幾千人在線,頻繁的進行資料庫連接操作將占用很多的系統資源,嚴重的甚至會造成服務器的崩潰,
-
**對于每一次資料庫連接,使用完后都得斷開,**否則,如果程式出現例外而未能關閉,將會導致資料庫系統中的記憶體泄漏,最終將導致重啟資料庫,
-
這種開發不能控制被創建的連接物件數,系統資源會被毫無顧及的分配出去,如連接過多,也可能導致記憶體泄漏,服務器崩潰,

9.2、資料庫連接池
為解決傳統開發中的資料庫連接問題,我們可以采用資料庫連接池技術,
資料庫連接池的基本思想:就是為資料庫連接建立一個“緩沖池”,預先在緩沖池中放入一定數量的連接,當需要建立資料庫連接時,只需從“緩沖池”中取出一個,使用完畢之后再放回去,
資料庫連接池負責分配、管理和釋放資料庫連接,它允許應用程式重復使用一個現有的資料庫連接,而不是重新建立一個,
資料庫連接池在初始化時將創建一定數量的資料庫連接放到連接池中,這些資料庫連接的數量是由最小資料庫連接數來設定的,無論這些資料庫連接是否被使用,連接池都將一直保證至少擁有這么多的連接數量,連接池的最大資料庫連接數量限定了這個連接池能占有的最大連接數,當應用程式向連接池請求的連接數超過最大連接數量時,這些請求將被加入到等待佇列中,

9.2.1、資料庫連接池的原理以及優勢

使用資料庫連接池的優點也是很明顯的:
-
資源重復使用
? 由于資料庫連接得以重用,避免了頻繁創建,釋放連接引起的大量性能開銷,在減少系統消耗的基礎上,另一方面也增加了系統運行環境的平穩性,
-
更快的系統反應速度
? 資料庫連接池在初始化程序中,往往已經創建了若干資料庫連接置于連接池中備用,此時連接的初始化作業均已完成,對于業務請求處理而言,直接利用現有可用連接,避免了資料庫連接初始化和釋放程序的時間開銷,從而減少了系統的回應時間,
-
新的資源分配手段
? 對于多應用共享同一資料庫的系統而言,可在應用層通過資料庫連接池的配置,實作某一應用最大可用資料庫連接數的限制,避免某一應用獨占所有的資料庫資源,
-
統一的連接管理,避免資料庫連接泄漏
? 在較為完善的資料庫連接池實作中,可根據預先的占用超時設定,強制回收被占用連接,從而避免了常規資料庫連接操作中可能出現的資源泄露,
9.2.2、資料庫連接池的屬性
基本屬性:連接池存了連接物件,而連接物件依賴四要素,所以四要素(driverClassName,url,username,password)是基本要求,
其他屬性:對連接物件做限制的配置
? 1. 初始化連接數:在連接池中事先準備好初始化Connection物件,
? 2. 最多連接數:在連接池中最多有一定數量的Connection物件,其他客戶端進入等待狀態,
? 3. 最少連接數:在連接池中最少一定數量的Connection物件,
? 4. 最長等待時間:使用一定時間來申請獲取Connection物件,如果時間到還沒有申請到,則提示,自
動放棄,
? 5. 最長超時時間:如果你在一定時間之內沒有任何動作,則認為是自動放棄Connection物件,
9.3、資料庫連接池的分類
JDBC 的資料庫連接池使用javax.sql.DataSource來表示,DataSource 只是一個介面,該介面通常由服務器(Weblogic, WebSphere, Tomcat)提供實作,也有一些開源組織提供實作:
- DBCP 是Apache提供的資料庫連接池,tomcat 服務器自帶dbcp資料庫連接池,速度相對c3p0較快,但因自身存在BUG,Hibernate3已不再提供支持,
- C3P0 是一個開源組織提供的一個資料庫連接池,**速度相對較慢,穩定性還可以,**hibernate官方推薦使用,
- Proxool 是sourceforge下的一個開源專案資料庫連接池,有監控連接池狀態的功能,穩定性較c3p0差一點,
- BoneCP 是一個開源組織提供的資料庫連接池,速度快,
- Druid 是阿里提供的資料庫連接池,據說是集DBCP 、C3P0 、Proxool 優點于一身的資料庫連接池,但是速度不確定是否有BoneCP快,
DataSource 通常被稱為資料源,它包含連接池和連接池管理兩個部分,習慣上也經常把 DataSource 稱為連接池,DataSource用來取代DriverManager來獲取Connection,獲取速度快,同時可以大幅度提高資料庫訪問速度,
? 注意:
- 資料源和資料庫連接不同,資料源無需創建多個,它是產生資料庫連接的工廠,因此整個應用只需要一個資料源即可,
- 當資料庫訪問結束后,程式還是像以前一樣關閉資料庫連接:conn.close(); 但conn.close()并沒有關閉資料庫的物理連接,它僅僅把資料庫連接釋放,歸還給了資料庫連接池,
9.4、DBCP連接池
DBCP 是 Apache 軟體基金組織下的開源連接池實作,該連接池依賴該組織下的另一個開源系統:Common-pool,如需使用該連接池實作,應在系統中增加如下兩個 jar 檔案:
- Commons-dbcp.jar:連接池的實作
- Commons-pool.jar:連接池實作的依賴庫
**Tomcat 的連接池正是采用該連接池來實作的,**該資料庫連接池既可以與應用服務器整合使用,也可由應用程式獨立使用,
資料源和資料庫連接不同,資料源無需創建多個,它是產生資料庫連接的工廠,因此整個應用只需要一個資料源即可,
當資料庫訪問結束后,程式還是像以前一樣關閉資料庫連接:conn.close(); 但上面的代碼并沒有關閉資料庫的物理連接,它僅僅把資料庫連接釋放,歸還給了資料庫連接池,
9.4.1、DBCP屬性說明
| 屬性 | 默認值 | 說明 |
|---|---|---|
| initialSize | 0 | 連接池啟動時創建的初始化連接數量 |
| maxActive | 8 | 連接池中可同時連接的最大的連接數 |
| maxIdle | 8 | 連接池中最大的空閑的連接數,超過的空閑連接將被釋放,如果設定為負數表示不限制 |
| minIdle | 0 | 連接池中最小的空閑的連接數,低于這個數量會被創建新的連接,該引數越接近maxIdle,性能越好,因為連接的創建和銷毀,都是需要消耗資源的;但是不能太大, |
| maxWait | 無限制 | 最大等待時間,當沒有可用連接時,連接池等待連接釋放的最大時間,超過該時間限制會拋出例外,如果設定-1表示無限等待 |
| poolPreparedStatements | false | 開啟池的Statement是否prepared |
| maxOpenPreparedStatements | 無限制 | 開啟池的prepared 后的同時最大連接數 |
| minEvictableIdleTimeMillis | 連接池中連接,在時間段內一直空閑, 被逐出連接池的時間 | |
| removeAbandonedTimeout | 300 | 超過時間限制,回收沒有用(廢棄)的連接 |
| removeAbandoned | false | 超過removeAbandonedTimeout時間后,是否進 行沒用連接(廢棄)的回收 |
9.4.2、獲取連接的方式
//使用dbcp資料庫連接池的組態檔方式,獲取資料庫的連接:推薦
// 創建一個DataSource物件
private static DataSource source = null;
static{
try {
//創建一個Properties,用于讀取組態檔
Properties pros = new Properties();
//讀取組態檔
InputStream is = DBCPTest.class.getClassLoader().getResourceAsStream("db.properties");
//加載組態檔
pros.load(is);
//根據提供的BasicDataSourceFactory創建對應的DataSource物件
source = BasicDataSourceFactory.createDataSource(pros);
} catch (Exception e) {
e.printStackTrace();
}
}
public static Connection getConnection4() throws Exception {
Connection conn = source.getConnection();
return conn;
}
復制代碼
driverClassName=com.mysql.jdbc.Driver
url=jdbc:mysql://localhost:3306/test?rewriteBatchedStatements=true&useServerPrepStmts=false
username=root
password=123456
initialSize=10
#...
復制代碼
9.4.3、注意事項
由于使用了DBCP,所以組態檔的key我們必須按照他官方給定的要求來書寫,
9.5、Druid(德魯伊)連接池
Druid是阿里巴巴開源平臺上一個資料庫連接池實作,它結合了C3P0、DBCP、Proxool等DB池的優點,同時加入了日志監控,可以很好的監控DB池連接和SQL的執行情況,可以說是針對監控而生的DB連接池,可以說是目前最好的連接池之一,
9.5.1、Druid引數詳解
| 配置 | 預設 | 說明 |
|---|---|---|
| name | 配置這個屬性的意義在于,如果存在多個資料源,監控的時候可以通過名字來區分開來, 如果沒有配置,將會生成一個名字,格式是:”DataSource-” + System.identityHashCode(this) | |
| url | 連接資料庫的url,不同資料庫不一樣,例如:mysql : jdbc:mysql://10.20.153.104:3306/druid2 oracle : jdbc:oracle:thin:@10.20.149.85:1521:ocnauto | |
| username | 連接資料庫的用戶名 | |
| password | 連接資料庫的密碼,如果你不希望密碼直接寫在組態檔中,可以使用ConfigFilter,詳細看這里:github.com/alibaba/dru… | |
| driverClassName | 根據url自動識別 這一項可配可不配,如果不配置druid會根據url自動識別dbType,然后選擇相應的driverClassName(建議配置下) | |
| initialSize | 0 | 初始化時建立物理連接的個數,初始化發生在顯示呼叫init方法,或者第一次getConnection時 |
| maxActive | 8 | 最大連接池數量 |
| maxIdle | 8 | 已經不再使用,配置了也沒效果 |
| minIdle | 最小連接池數量 | |
| maxWait | 獲取連接時最大等待時間,單位毫秒,配置了maxWait之后,預設啟用公平鎖,并發效率會有所下降,如果需要可以通過配置useUnfairLock屬性為true使用非公平鎖, | |
| poolPreparedStatements | false | 是否快取preparedStatement,也就是PSCache,PSCache對支持游標的資料庫性能提升巨大,比如說oracle,在mysql下建議關閉, |
| maxOpenPreparedStatements | -1 | 要啟用PSCache,必須配置大于0,當大于0時,poolPreparedStatements自動觸發修改為true,在Druid中,不會存在Oracle下PSCache占用記憶體過多的問題,可以把這個數值配置大一些,比如說100 |
| validationQuery | 用來檢測連接是否有效的sql,要求是一個查詢陳述句,如果validationQuery為null,testOnBorrow、testOnReturn、testWhileIdle都不會其作用, | |
| testOnBorrow | true | 申請連接時執行validationQuery檢測連接是否有效,做了這個配置會降低性能, |
| testOnReturn | false | 歸還連接時執行validationQuery檢測連接是否有效,做了這個配置會降低性能 |
| testWhileIdle | false | 建議配置為true,不影響性能,并且保證安全性,申請連接的時候檢測,如果空閑時間大于timeBetweenEvictionRunsMillis,執行validationQuery檢測連接是否有效, |
| timeBetweenEvictionRunsMillis | 有兩個含義: 1)Destroy執行緒會檢測連接的間隔時間2)testWhileIdle的判斷依據,詳細看testWhileIdle屬性的說明 | |
| numTestsPerEvictionRun | 不再使用,一個DruidDataSource只支持一個EvictionRun | |
| minEvictableIdleTimeMillis | ||
| connectionInitSqls | 物理連接初始化的時候執行的sql | |
| exceptionSorter | 根據dbType自動識別 當資料庫拋出一些不可恢復的例外時,拋棄連接 | |
| filters | 屬性型別是字串,通過別名的方式配置擴展插件,常用的插件有: 監控統計用的filter:stat日志用的filter:log4j防御sql注入的filter:wall | |
| proxyFilters | 型別是List,如果同時配置了filters和proxyFilters,是組合關系,并非替換關系 |
9.5.2、獲取連接方式
package com.utils;
import com.alibaba.druid.pool.DruidDataSource;
import com.alibaba.druid.pool.DruidDataSourceFactory;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.Properties;
import javax.sql.DataSource;
/**
* @author Xiao_Lin
* @date 2021/1/3 19:47
*/
public class DruidUtils {
static DataSource ds = null;
private DruidUtils(){
}
static {
InputStream stream = Thread.currentThread().getContextClassLoader()
.getResourceAsStream("db.properties");
Properties properties = new Properties();
try {
properties.load(stream);
ds = DruidDataSourceFactory.createDataSource(properties);
} catch (Exception e) {
e.printStackTrace();
}
}
public static Connection getConnection(){
try {
return ds.getConnection();
} catch (SQLException e) {
e.printStackTrace();
}
return null;
}
}
復制代碼
DriverClassName = com.mysql.jdbc.Driver
url = jdbc:mysql:///db?characterEncoding=utf-8&useSSL=false
username = root
password = 123456
復制代碼
9.5.3、注意事項
properties組態檔中的 key 一定要和 DruidDataSource 中對應的屬性名一致,

port java.io.IOException; import java.io.InputStream; import java.net.URL; import java.sql.Connection; import java.sql.SQLException; import java.util.Properties; import javax.sql.DataSource;
/**
- @author Xiao_Lin
- @date 2021/1/3 19:47
*/ public class DruidUtils { static DataSource ds = null; private DruidUtils(){
}
static { InputStream stream = Thread.currentThread().getContextClassLoader() .getResourceAsStream("db.properties"); Properties properties = new Properties(); try { properties.load(stream); ds = DruidDataSourceFactory.createDataSource(properties); } catch (Exception e) { e.printStackTrace(); } }
public static Connection getConnection(){ try { return ds.getConnection(); } catch (SQLException e) { e.printStackTrace(); } return null; } }
```properties
DriverClassName = com.mysql.jdbc.Driver
url = jdbc:mysql:///db?characterEncoding=utf-8&useSSL=false
username = root
password = 123456
作者:XiaoLin_Java
鏈接:https://juejin.cn/post/6986807373721501726
來源:掘金
著作權歸作者所有,商業轉載請聯系作者獲得授權,非商業轉載請注明出處,
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/289496.html
標籤:其他
