事務 & 資料庫連接配接池 & DBUtils
事務
Transaction,其實指的一組操作,裡面包含許多個單一的邏輯。
隻要有一個邏輯沒有執行成功,那麼都算失敗。 所有的資料都回歸到最初的狀态(復原)。
為什麼要有事務?
為了確定邏輯的成功。例子:銀行的轉賬。
1、使用指令行方式示範事務。
(1)開啟事務指令:start transaction;
(2)送出,復原事務指令:
commit:送出事務,資料将會寫到磁盤上的資料庫
rollback:資料復原,回到最初的狀态。
(3)關閉自動送出功能
(4)示範事務
2、使用代碼方式示範事務
代碼裡面的事務,主要是針對連接配接來的。
(1)通過conn.setAutoCommit(false) 來關閉自動送出的設定
(2)送出事務:conn.commit();
(3)復原事務:conn.rollback();
public static void main(String[] args){
Connection conn = null;
PreparedStatement ps = null;
ResultSet rs = null;
try {
conn = JDBCUtil.getConn();
//連接配接,事務預設就是自動送出的。 關閉自動送出。
conn.setAutoCommit(false);
String sql = "update account set money = money + ? where id = ?";
ps = conn.prepareStatement(sql);
//扣錢,扣ID為1 的100塊錢
ps.setInt(1, -100);
ps.setInt(2, 1);
ps.executeUpdate();
// 模拟中間出現異常
int a = 10 / 0 ;
//加錢,給ID為2 加100塊錢
ps.setInt(1, 100);
ps.setInt(2, 2);
ps.executeUpdate();
//成功: 送出事務。
conn.commit();
} catch (SQLException e) {
try {
//事變: 復原事務
conn.rollback();
} catch (SQLException e1) {
e1.printStackTrace();
}
e.printStackTrace();
} finally {
JDBCUtil.release(conn, ps, rs);
}
}
3、事務的特性
(1)原子性
事務中包含的邏輯,不可分割。
(2)一緻性
事務執行前後,資料完整性。
語句全部執行成功,或者全部失敗。
(3)隔離性
事務與事務之間沒有影響,互相隔離。
(4)持久性
事務執行成功,那麼資料應該持久儲存到磁盤上。
4、事務的安全隐患
不考慮隔離級别設定,那麼會出現以下問題。
讀的問題:髒讀,不可重讀讀,幻讀。
- 髒讀:A事務讀到了B事務還未送出的資料。
- 不可重複讀:A事務讀到了B事務送出的資料,造成了前後兩次查詢結果不一緻。
5、讀未送出 示範
(1)設定A視窗的隔離級别為:讀未送出
(2)AB兩個視窗都分别開啟事務
6、讀已送出 示範
(1)設定A視窗的隔離級别為:讀已送出
(2)AB兩個視窗都開啟事務,在B視窗執行更新操作。
(3)在A視窗兩次執行的查詢結果不一緻。
一次是在B視窗送出事務之前,一次是在B視窗送出事務之後。
這個隔離級别能夠屏蔽,髒讀的現象,但是引發了另一個問題,不可重複讀。
7、可串行化
(1)如果有一個連接配接的隔離級别設定為了串行化,那麼誰先打開了事務,誰就有了先執行的權利,誰後打開事務,誰就隻能得着,等前面的那個事務,送出或者復原後,才能執行。
但是這種隔離級别一般比較少用,容易造成性能上的問題,效率比較低。
(2)按效率劃分,從高到低:
讀未送出 > 讀已送出 > 可重複讀 > 可串行化
(3)按攔截程度,從高到底
可串行化 > 可重複讀 > 讀已送出 > 讀未送出
8、事務總結
(1)需要掌握的:
在代碼裡面會使用事務:
- conn.setAutoCommit(false);
- conn.commit();
- conn.rollback();
事務隻是針對連接配接連接配接對象,如果再開一個連接配接對象,那麼還是預設自動送出事務。
(2)需要了解的:
存在安全隐患
讀的問題:
- 髒讀:A事務讀到了B事務未送出的資料。
- 不可重複讀:A事務讀到了B事務已送出的資料,造成前後兩次查詢結果不一緻。
- 幻讀:A事務讀到了B事務insert的資料,造成前後查詢結果不一緻。
寫的問題:
- 丢失更新:A事務更新資料,先送出,B事務也更新對應資料,後送出,造成A事務的送出被覆寫。
(3)隔離級别:
-
讀未送出
引發了:髒讀
-
讀已送出
解決了:髒讀
引發了:不可重複讀
-
可重複讀
解決了:髒讀,不可重複讀
未解決:幻讀
-
可串行化
解決了:髒讀,不可重複讀,幻讀
問題全部解決,效率卻很低,事務排隊執行
MySQL:預設的隔離級别是:可重複讀。
Oracle:預設的隔離級别是:讀已送出。
(4)丢失更新:
(5)解決丢失更新
- 悲觀鎖:可以在查詢的時候,加入 for update。
- 樂觀鎖:要求程式員自己控制。
資料庫連接配接池
資料庫的連接配接對象建立工作,比較消耗性能。
可以一開始先在記憶體中開辟一塊空間(集合),開始先往池子裡面放置 多個連接配接對象。
後面需要連接配接的話,直接從池子裡面去。不要去自己建立連接配接了。使用完畢,要記得歸還連接配接。確定連接配接對象能循環利用。
1、自定義資料庫連接配接池
代碼實作:
public class MyDataSource implements DataSource {
// 預設初始化資料庫連接配接池中有20個連接配接對象
private static List<Connection> list = new ArrayList<>(20);
// 單例模式,設定一個執行個體化對象
private static MyDataSource mds = new MyDataSource();
// 類加載時,初始化資料庫連接配接池中的連接配接對象
static {
for (int i = 0; i < 20; i++) {
try {
Connection conn = DBUtil.getConnection();
mds.list.add(conn);
} catch (SQLException throwables) {
throwables.printStackTrace();
}
}
System.out.println("靜态代碼塊執行:" + mds.getList().size());
}
public static MyDataSource getMds() {
return mds;
}
public static List<Connection> getList() {
return list;
}
// 構造方法私有化,確定隻有一個執行個體化對象
private MyDataSource() {
}
// 從連接配接池中取出一個資料庫連接配接對象
@Override
public Connection getConnection() throws SQLException {
Connection conn = null;
if (list.size() > 1) {
Connection connRem = list.remove(0);
// 抛出包裝類對象
conn = new ConnectionWrap(connRem, list);
} else { // 池子裡的連接配接對象快用完了,再添加1個
list.add(list.get(0));
Connection connRem = list.remove(0);
conn = new ConnectionWrap(connRem, list);
}
return conn;
}
// 額外的方法,歸還連接配接對象
public static void addBack(Connection conn) {
list.add(conn);
}
// 這裡還有接口實作的其他方法,不用管它
}
出現的問題:
- 需要額外記住 addBack方法
- 單例。
- 無法面向接口程式設計。
怎麼解決? 以addBack為切入點。
解決自定義資料庫連接配接池出現的問題:
由于多了一個addBack()方法,是以使用這個連接配接池的地方,需要額外記住這個方法,并且還不能面向接口程式設計。
我們打算修改接口中的那個close方法。 原來的Connection對象的close方法,是真的關閉連接配接。
打算修改這個close方法,以後在調用close,并不是真的關閉,而是歸還連接配接對象。
如何擴充某一個方法?
原有的方法邏輯,不是我們想要的,想修改成自己的邏輯。
- 直接改源碼,可是無法實作。sun公司已經不允許修改源碼了。
- 繼承,必須得知道這個接口的具體實作是誰。也很難做到。
- 那就使用裝飾者模式,額外添加某個功能。 (還有更進階的,動态代理,這裡不講)
2、DBCP開源連接配接池
先導入jar檔案
(1)不使用配置檔案的方式:
// dbcp不使用配置檔案的方式
public class Test01 {
public static void main(String[] args) {
// 建立資料源對象
BasicDataSource dataSource = new BasicDataSource();
// 設定一系列配置資訊
dataSource.setDriverClassName("com.mysql.jdbc.Driver");
dataSource.setUrl("jdbc:mysql://127.0.0.1:3306/study_test");
dataSource.setUsername("root");
dataSource.setPassword("666666");
// 開始連接配接
Connection conn = null;
PreparedStatement ps = null;
ResultSet rs = null;
try {
// 擷取連接配接對象
conn = dataSource.getConnection();
// 擷取預編譯的資料庫連接配接對象
String sql = "select ename, job, sal from emp";
ps = conn.prepareStatement(sql);
// 執行sql語句
rs = ps.executeQuery();
// 處理查詢結果集
while (rs.next()) {
String ename = rs.getString("ename");
String job = rs.getString("job");
double sal = rs.getDouble("sal");
System.out.println(ename + " " + job + " " + sal);
}
} catch (SQLException throwables) {
throwables.printStackTrace();
} finally {
DBClose.close(conn, ps, rs);
}
}
}
(2)使用配置檔案的方式:
// dpcp使用配置檔案的方式
public class Test02 {
public static void main(String[] args) {
/*// 建立資料源對象
BasicDataSource dataSource = new BasicDataSource();
// 綁定配置檔案的資訊
dataSource.setConnectionProperties("dbcpconfig.properties");
// 經過測試,此方法行不通*/
Connection conn = null;
PreparedStatement ps = null;
ResultSet rs = null;
try {
// 擷取屬性配置檔案
InputStream in = new FileInputStream("src/dbcpconfig.properties");
Properties properties = new Properties();
properties.load(in);
// 綁定配置檔案的資訊
DataSource dataSource = BasicDataSourceFactory.createDataSource(properties);
conn = dataSource.getConnection();
String sql = "select ename, job, sal from emp";
ps = conn.prepareStatement(sql);
rs = ps.executeQuery();
while (rs.next()) {
String ename = rs.getString("ename");
String job = rs.getString("job");
double sal = rs.getDouble("sal");
System.out.println(ename + " " + job + " " + sal);
}
} catch (SQLException | FileNotFoundException throwables) {
throwables.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
} finally {
// 釋放資源
DBClose.close(conn, ps, rs);
}
}
}
3、C3P0開源連接配接池
先導入jar檔案到lib目錄。
(1)不使用配置檔案的方式
// c3p0不使用配置檔案的方式
public class Test01 {
public static void main(String[] args) {
Connection conn = null;
PreparedStatement ps = null;
try {
// 建立資料源對象
ComboPooledDataSource cpds = new ComboPooledDataSource();
// 設定資訊
cpds.setDriverClass("com.mysql.jdbc.Driver");
cpds.setJdbcUrl("jdbc:mysql://127.0.0.1:3306/study_test");
cpds.setUser("root");
cpds.setPassword("666666");
// 擷取連接配接
conn = cpds.getConnection();
// 擷取預編譯的資料庫操作對象
String sql = "update t_user set pwd = ? where id = ?";
ps = conn.prepareStatement(sql);
// 給占位符傳值
ps.setString(1, "123");
ps.setString(2, "1");
// 執行sql語句
int affectLine = ps.executeUpdate();
// 處理結果
System.out.println(affectLine == 1 ? "更新成功" : "更新失敗");
} catch (SQLException | PropertyVetoException throwables) {
throwables.printStackTrace();
} finally {
DBClose.close(conn, ps, null);
}
}
}
(2)使用配置檔案的方式
// c3p0使用配置檔案的方式
public class Test02 {
public static void main(String[] args) {
Connection conn = null;
PreparedStatement ps = null;
try {
// 建立預設的資料源對象
// 會自動綁定配置檔案c3p0-config.xml,名字必須為這個,不能寫錯
ComboPooledDataSource cpds = new ComboPooledDataSource();
// 也可以指定連接配接oracle資料庫
//ComboPooledDataSource cpds1 = new ComboPooledDataSource("oracle");
// 擷取連接配接
conn = cpds.getConnection();
// 擷取預編譯的資料庫操作對象
String sql = "update t_user set pwd = ? where id = ?";
ps = conn.prepareStatement(sql);
// 給占位符傳值
ps.setString(1, "123");
ps.setString(2, "1");
// 執行sql語句
int affectLine = ps.executeUpdate();
// 處理結果
System.out.println(affectLine == 1 ? "更新成功" : "更新失敗");
} catch (SQLException throwables) {
throwables.printStackTrace();
} finally {
DBClose.close(conn, ps, null);
}
}
}
DBUtils
1、增删改
DBUtils隻是幫我們簡化了CRUD(增删改查)操作,連接配接資料庫并不是它做的。
(1)針對增加,删除,更新操作
-
queryRunner.update(sql, params);
使用update()方法,形式參數為語句sql,變長占位符的傳值
測試代碼:
public class Test01 {
public static void main(String[] args) {
try {
// 建立連接配接池對象
ComboPooledDataSource dataSource = new ComboPooledDataSource();
// 建立DBUtil操作對象,并綁定到連接配接池
QueryRunner queryRunner = new QueryRunner(dataSource);
// 執行sql語句,增删改,用update()方法
// 增
/*String sql = "insert into t_user values(null, ?, ?)";
int affectLine = queryRunner.update(sql, "wangwu", "abc123");*/
// 删
/*String sql = "delete from t_user where id = ?";
int affectLine = queryRunner.update(sql, 6);*/
// 改
String sql = "update t_user set pwd = ? where id = ?";
int affectLine = queryRunner.update(sql, "acb", 5);
// 處理更新結果
System.out.println(affectLine == 1 ? "更新成功" : "更新失敗");
} catch (SQLException throwables) {
throwables.printStackTrace();
}
}
}
2、查詢
針對查詢操作
- queryRunner.query(sql, rsh, params);
注意:接收類User的屬性字段名字,必須對應資料庫表中的列名,這樣底層才能自動封裝,否則值會為null。
測試代碼:
public class Test02 {
public static void main(String[] args) {
// 建立資料庫連接配接池對象
ComboPooledDataSource dataSource = new ComboPooledDataSource();
// 建立DbUtil操作對象,并綁定到連接配接池
QueryRunner queryRunner = new QueryRunner(dataSource);
try {
/*// 執行sql語句
String sql = "select id, username, pwd from t_user where id = ?";
// 傳回一個使用者的資訊,手動封裝到user中
User user = queryRunner.query(sql, new ResultSetHandler<User>() {
@Override
public User handle(ResultSet rs) throws SQLException {
User u = new User();
while (rs.next()) {
int id = rs.getInt("id");
String username = rs.getString("username");
String pwd = rs.getString("pwd");
u.setId(id);
u.setUsername(username);
u.setPwd(pwd);
}
return u;
}
}, 1);
System.out.println(user);*/
// ------------------------------------------------------------------------
// 如果查詢結果集是1行資料,使用BeanHandler更簡單,不用手動去封裝
/*String sql = "select * from t_user where id = ?";
User user = queryRunner.query(sql, new BeanHandler<User>(User.class), 1);
System.out.println(user);*/
// 如果查詢結果集是多行資料,使用BeanListHandler,傳回一個list集合
// 底層:通過位元組碼擷取執行個體對象
String sql = "select * from t_user where username like ?";
List<User> users = queryRunner.query(sql, new BeanListHandler<User>(User.class), "%a%");
for (User u : users) {
System.out.println(u);
}
} catch (SQLException throwables) {
throwables.printStackTrace();
}
}
}
3、ResultSetHandler 常用的實作類
使用頻率最高的兩個:
- BeanHandler, 查詢到的單個資料封裝成一個對象
- BeanListHandler, 查詢到的多個資料封裝成一個List<對象>
以下其它的實作類也可能會用到:
- ArrayHandler,查詢到的單行資料封裝成一個數組。
- ArrayListHandler,查詢到的多行資料封裝成一個集合,集合裡面的元素是數組。
- MapHandler,查詢到的單個資料封裝成一個map。
- MapListHandler,查詢到的多個資料封裝成一個集合,集合裡面的元素是map。
- ColumnListHandler
- KeyedHandler
- ScalarHandler
總結
1、事務
使用指令行示範
使用代碼示範
讀的問題:髒讀,不可重複讀,幻讀
寫的問題:丢失更新
悲觀鎖
樂觀鎖
4個隔離級别:讀未送出,讀已送出,可重複讀,可串行化。
2、開源資料庫連接配接池
DBCP
不使用配置
使用配置.properties檔案
C3P0
不使用配置
使用配置.xml檔案,檔案名固定(必須掌握)
自定義連接配接池
單例模式
裝飾者模式
3、DBUtils
簡化了我們的CRUD,裡面定義了通用的CRUD方法。
queryRunner.update(sql, params);
queryRunner.query(sql, rsh, params);