JDBC之事務、連接池、dbutils

事務


在實際程序開發中,因業務的不同,總是會涉及到對數據庫的多次操作。例如:銀行轉賬!

張三轉10000塊到李四的賬戶,這其實需要兩條SQL語句:

l  給張三的賬戶減去10000元;

l  給李四的賬戶加上10000元。

如果在第一條SQL語句執行成功後,在執行第二條SQL語句之前,程序被中斷了(可能是拋出了某個異常,也可能是其他什麼原因),那麼李四的賬戶沒有加上10000元,而張三卻減去了10000元。這肯定是不行的!

所以事務就是保證對數據庫的多個操作,要麼完全成功,要麼完全失敗。

總結一句話不成功便成仁


事務的四大特性(ACID)

l  原子性(Atomicity):事務中所有操作是不可再分割的原子單位。事務中所有操作要麼全部執行成功,要麼全部執行失敗。

l  一致性(Consistency):事務執行後,數據庫狀態與其它業務規則保持一致。如轉賬業務,無論事務執行成功與否,參與轉賬的兩個賬號餘額之和應該是不變的。

l  隔離性(Isolation):隔離性是指在併發操作中,不同事務之間應該隔離開來,使每個併發中的事務不會相互干擾。

l  持久性(Durability):一旦事務提交成功,事務中所有的數據操作都必須被持久化到數據庫中,即使提交事務後,數據庫馬上崩潰,在數據庫重啓時,也必須能保證通過某種機制恢復數據。


Mysql中的事務:

在默認情況下,MySQL每執行一條SQL語句,都是一個單獨的事務。如果需要在一個事務中包含多條SQL語句,那麼需要開啓事務和結束事務。

l  開啓事務:starttransaction

l  結束事務:commit(提交)rollback(回滾)

下面演示zs給li轉賬10000元的示例:

START TRANSACTION;

UPDATE account SET balance=balance-10000WHERE id=1;

UPDATE account SET balance=balance+10000WHERE id=2;

ROLLBACK

執行結果:回滾結束,事務執行失敗。

------------------------------------------------------------------------------------

START TRANSACTION;

UPDATE account SET balance=balance-10000WHERE id=1;

UPDATE account SET balance=balance+10000WHERE id=2;

COMMIT

執行結果:提交結束,事務執行成功

------------------------------------------------------------------------------------

START TRANSACTION;

UPDATE account SET balance=balance-10000WHERE id=1;

UPDATE account SET balance=balance+10000WHERE id=2;

quit

執行結果:退出,mysql會自動回滾。

----------------------------------------------------------


jdbc中的事務

Connection有三個與事務相關的方法

  1. setAutoCommit(bookean) :設置是否爲自動提交事務,如果true(默認值就是true)表示自動提交,也就是每條執行的SQL語句都是一個單獨的事務,如果設置false,那麼就相當於開啓了事務了;
  2. commit() :提交結束事務
  3. rollback() : 回滾結束事務

	public void transfer(boolean b) {
		Connection con = null;
		PreparedStatement pstmt = null;
		
		try {
			con = JdbcUtils.getConnection();
			//手動提交,開啓事務
			con.setAutoCommit(false);
			
			String sql = "update account set balance=balance+? where id=?";
			pstmt = con.prepareStatement(sql);
			
			//操作
			pstmt.setDouble(1, -10000);
			pstmt.setInt(2, 1);
			pstmt.executeUpdate();
			
			// 在兩個操作中拋出異常
			if(b) {
				throw new Exception();
			}
			
			pstmt.setDouble(1, 10000);
			pstmt.setInt(2, 2);
			pstmt.executeUpdate();
			
			//提交事務
			con.commit();
		} catch(Exception e) {
			//回滾事務
			if(con != null) {
				try {
					con.rollback();
				} catch(SQLException ex) {}
			}
			throw new RuntimeException(e);
		} finally {
			//關閉
			JdbcUtils.close(con, pstmt);
		}
	}



jdbc中的保存點

保存點是JDBC3.0的東西!當要求數據庫服務器支持保存點方式的回滾。可以通過boolean b = con.getMetaData().supportsSavepoints(); 這個方法進行效驗。

保存點的作用是允許事務回滾到指定的保存點位置。在事務中設置好保存點,然後回滾時可以選擇回滾到指定的保存點,而不是回滾整個事務!

注意,回滾到指定保存點並沒有結束事務!!!只有回滾了整個事務纔算是結束事務了!

Connection 類設置保存點,以及回滾到指定保存點的方法:

  1. 設置保存點 :Savepoint.setSavepoint();//使用到Savepoint
  2. 回滾到指定保存點 :voidrollback(Savepoint);

	/*
	 * 李四對張三說,如果你給我轉1W,我就給你轉100W。
	 * ==========================================
	 * 
	 * 張三給李四轉1W(張三減去1W,李四加上1W)
	 * 設置保存點!
	 * 李四給張三轉100W(李四減去100W,張三加上100W)
	 * 查看李四餘額爲負數,那麼回滾到保存點。
	 * 提交事務
	 */
	@Test
	public void fun() {
		Connection con = null;
		PreparedStatement pstmt = null;
		
		try {
			con = JdbcUtils.getConnection();
			//手動提交
			con.setAutoCommit(false);
			
			String sql = "update account set balance=balance+? where name=?";
			pstmt = con.prepareStatement(sql);
			
			//操作1(張三減去1W)
			pstmt.setDouble(1, -10000);
			pstmt.setString(2, "zs");
			pstmt.executeUpdate();
			
			//操作2(李四加上1W)
			pstmt.setDouble(1, 10000);
			pstmt.setString(2, "ls");
			pstmt.executeUpdate();
			
			// 設置保存點
			Savepoint sp = con.setSavepoint();
			
			//操作3(李四減去100W)
			pstmt.setDouble(1, -1000000);
			pstmt.setString(2, "ls");
			pstmt.executeUpdate();		
			
			//操作4(張三加上100W)
			pstmt.setDouble(1, 1000000);
			pstmt.setString(2, "zs");
			pstmt.executeUpdate();
			
			//操作5(查看李四餘額)
			sql = "select balance from account where name=?";
			pstmt = con.prepareStatement(sql);
			pstmt.setString(1, "ls");
			ResultSet rs = pstmt.executeQuery();
			rs.next();
			double balance = rs.getDouble(1);
      <span style="white-space:pre">		</span>//如果李四餘額爲負數,那麼回滾到指定保存點
			if(balance < 0) {
				con.rollback(sp);<span style="white-space:pre">			</span>//發現李四餘額小於0,回滾到指定還原點!即撤銷了李四給張三轉賬100萬的操作
				System.out.println("張三,你上當了!");
			}
			
			//提交事務
			con.commit();<span style="white-space:pre">					</span>//注意,一定要提交事務,因爲回滾到指定保存點不會結束事務!保存點之前的操作沒有被回滾,只能提交了才能真正把沒有回滾的操作執行了。
		} catch(Exception e) {
			//回滾事務
			if(con != null) {
				try {
					con.rollback();
				} catch(SQLException ex) {}
			}
			throw new RuntimeException(e);
		} finally {
			//關閉
			JdbcUtils.close(con, pstmt);
		}
	}


事務的隔離級別

我也沒搞清楚。我覺得沒有必要死記。在這就說說我的理解:我認爲就是控制併發事務問題的東東。

五大併發事務問題:兩類更新,三類讀

 1   髒讀:讀取到另一個事務未提交數據;

 2   不可重複讀:兩次讀取不一致;

 3   幻讀(虛讀):讀到另一事務已提交數據。

例子:

1  髒讀(dirty read):讀到未提交更新數據

 

時間

轉賬事務A

取款事務B

T1

 

開始事務

T2

開始事務

 

T3

 

查詢賬戶餘額爲1000元

T4

 

取出500元把餘額改爲500元

T5

查看賬戶餘額爲500元(髒讀)

 

T6

 

撤銷事務,餘額恢復爲1000元

T7

匯入100元把餘額改爲600元

 

T8

提交事務

 

  

    A事務查詢到了B事務未提交的更新數據,A事務依據這個查詢結果繼續執行相關操作。但是接着B事務撤銷了所做的更新,這會導致A事務操作的是髒數據。(這是絕對不允許出現的事情)


2  虛讀(幻讀)(phantom read):讀到已提交插入數據

 

時間

統計金額事務A

轉賬事務B

T1

 

開始事務

T2

開始事務

 

T3

統計總存款數爲10000元

 

T4

 

新增一個存款賬戶,存款爲100元

T5

 

提交事務

T6

再次統計總存款數爲10100元

 

 

  A事務第一次查詢時,沒有問題,第二次查詢時查到了B事務已提交的新插入數據,這導致兩次查詢結果不同。(在實際開發中,很少會對相同數據進行兩次查詢,所以可以考慮是否允許虛讀)


3  不可重複讀(unrepeatable read):讀到已提交更新數據

 

時間

取款事務A

轉賬事務B

T1

 

開始事務

T2

開始事務

 

T3

 

查詢賬戶餘額爲1000元

T4

查詢賬戶餘額爲1000元

 

T5

 

取出100元,把餘額改爲900元

T6

 

提交事務

T7

查詢賬戶餘額爲900元(與T4讀取的一不一致)

 

 

  不可重複讀與虛讀有些相似,都是兩次查詢的結果不同。後者是查詢到了另一個事務已提交的新插入數據,而前者是查詢到了另一個事務已提交的更新數據。


四大隔離級別

隔離級別

髒讀

不可重複讀

虛讀

第一類丟失更新

第二類丟失更新

READ UNCOMMITTED (讀未提交數據)

允許

允許

允許

不允許

允許

READ COMMITTED (讀已提交數據)

不允許

允許

允許

不允許

允許

REPEATABLE READ (可重複讀)

不允許

不允許

允許

不允許

不允許

SERIALIZABLE (串行化)

不允許

不允許

不允許

不允許

不允許


1 SERIALIZABLE(串行化)

當數據庫系統使用SERIALIZABLE隔離級別時,一個事務在執行過程中完全看不到其他事務對數據庫所做的更新。當兩個事務同時操作數據庫中相同數據時,如果第一個事務已經在訪問該數據,第二個事務只能停下來等待,必須等到第一個事務結束後才能恢復運行。因此這兩個事務實際上是串行化方式運行。

 

2 REPEATABLE READ(可重複讀)

當數據庫系統使用REPEATABLE READ隔離級別時,一個事務在執行過程中可以看到其他事務已經提交的新插入的記錄,但是不能看到其他事務對已有記錄的更新。

 

3 READ COMMITTED(讀已提交數據)

  當數據庫系統使用READ COMMITTED隔離級別時,一個事務在執行過程中可以看到其他事務已經提交的新插入的記錄,而且還能看到其他事務已經提交的對已有記錄的更新。

 

4 READ UNCOMMITTED(讀未提交數據)

  當數據庫系統使用READUNCOMMITTED隔離級別時,一個事務在執行過程中可以看到其他事務沒有提交的新插入的記錄,而且還能看到其他事務沒有提交的對已有記錄的更新。


MySQL的默認隔離級別爲REPEATABLE READ


連接池


可以理解爲用來管理connection的容器。將connection交予池來管理,這可以重複使用Connection。有了池,所以我們就不用自己來創建Connection,而是通過池來獲取Connection對象。

當使用完Connection後,調用Connection的close()方法也不會真的關閉Connection,而是把Connection“歸還”給池。池就可以再利用這個Connection對象了。


JDBC的數據庫連接池接口:DateSource

Java爲數據庫連接池提供了公共的接口:javax.sql.DataSource,各個廠商可以讓自己的連接池實現這個接口。這樣應用程序可以方便的切換不同廠商的連接池!


DBCP

DBCP是Apache提供的一款開源免費的數據庫連接池!

連接池類  BaseicDataSource

如何使用:

1.導包:commons-dbcp-1.4.jar 依賴 commons-pool-1.6.jar 別忘了驅動包(3個)

2.代碼:

import java.sql.Connection;
import java.sql.SQLException;

import org.apache.commons.dbcp.BasicDataSource;
import org.junit.Test;

public void fun1() throws SQLException {
		BasicDataSource ds = new BasicDataSource();
		ds.setUsername("root");
		ds.setPassword("123");
		ds.setUrl("jdbc:mysql://localhost:3306/mydb1");
		ds.setDriverClassName("com.mysql.jdbc.Driver");
		
		ds.setMaxActive(20);   <span style="white-space:pre">					</span>//最大連接數
		ds.setMaxIdle(10);<span style="white-space:pre">					</span>//最大空閒數
		ds.setInitialSize(10);<span style="white-space:pre">					</span>//初始化連接數
		ds.setMinIdle(2);<span style="white-space:pre">					</span>//最小空閒數
		ds.setMaxWait(1000);<span style="white-space:pre">					</span>//最大等待毫秒數
		
		Connection con = ds.getConnection();
		System.out.println(con.getClass().getName());
		con.close();<span style="white-space:pre">						</span>//關閉連接只是把連接歸還給池,更多詳細設置請參考幫助文檔的BasicDateSorice類。
}


C3P0

C3P0也是開源免費的連接池!C3P0被很多人看好!

連接池類:ComboPooledDataSource

如何使用:

1.導包:c3p0-0.9.2-pre1.jar 、 c3p0-oracle-thin-extras-0.9.2-pre1.jar 、mchange-commons-0.2.jar 和驅動

2.可以加入配置文件,也可以不加。如果使用配置文件,配置文件必須命名爲c3p0-config.xml  放在src下。

3.代碼

	public void fun1() throws PropertyVetoException, SQLException {
		ComboPooledDataSource ds = new ComboPooledDataSource();
		ds.setJdbcUrl("jdbc:mysql://localhost:3306/mydb1");
		ds.setUser("root");
		ds.setPassword("123");
		ds.setDriverClass("com.mysql.jdbc.Driver");
		
		ds.setAcquireIncrement(5);<span style="white-space:pre">			</span>//每次增量
		ds.setInitialPoolSize(20);<span style="white-space:pre">			</span>//初始化連接數
		ds.setMinPoolSize(2);<span style="white-space:pre">				</span>//最少連接數
		ds.setMaxPoolSize(50);<span style="white-space:pre">				</span>//最多連接數
		
		Connection con = ds.getConnection();
		System.out.println(con);
		con.close();
	}
使用配置文件。

配置文件c3p0-config.xml

<?xml version="1.0" encoding="UTF-8"?>
<c3p0-config>
	<default-config><span style="white-space:pre">										</span><!-- 默認配置 -->
		<property name="jdbcUrl">jdbc:mysql://localhost:3306/mydb1</property>
		<property name="driverClass">com.mysql.jdbc.Driver</property>
		<property name="user">root</property>
		<property name="password">123</property>
		<property name="acquireIncrement">3</property>
		<property name="initialPoolSize">10</property>
		<property name="minPoolSize">2</property>
		<property name="maxPoolSize">10</property>
	</default-config>
	<named-config name="oracle-config"><span style="white-space:pre">								</span><!-- 命名配置  -->
		<property name="jdbcUrl">jdbc:mysql://localhost:3306/mydb1</property>
		<property name="driverClass">com.mysql.jdbc.Driver</property>
		<property name="user">root</property>
		<property name="password">123</property>
		<property name="acquireIncrement">3</property>
		<property name="initialPoolSize">10</property>
		<property name="minPoolSize">2</property>
		<property name="maxPoolSize">10</property>
	</named-config>
</c3p0-config>

代碼

	public void fun2() throws PropertyVetoException, SQLException {
		ComboPooledDataSource ds = new ComboPooledDataSource(); <span style="white-space:pre">			</span>//不用定配置文件名稱,因爲配置文件名必須是c3p0-config.xml,這裏使用的是默認配置。
		Connection con = ds.getConnection();
		System.out.println(con);
		con.close();
	}
	public void fun2() throws PropertyVetoException, SQLException {
		ComboPooledDataSource ds = new ComboPooledDataSource("orcale-config") ;<span style="white-space:pre">		</span>//使用命名爲orcale-config的配置
		Connection con = ds.getConnection();
		System.out.println(con);
		con.close();
	}


DBUtils

是Apache Commons組件的項目,開源免費。對jdbc進行簡單的封裝


主要類:

DbUitls   ---- 都是靜態方法,一系列的close()。

QueryRunner  ---- 增刪改查都靠它 

update() : 執行insert  update  delete 

query() :執行select 語句

batch() :執行批處理

ResultSetHandler  ---- 結果集處理接口,其提供的實現類可以把結果集轉換成不同類型。

  • MapHandler  ----  單行處理器!把結果集轉換成Map<String,Object>,其中列名爲鍵
  • MapListHandler   ----  多行處理器!把結果集轉換成List<Map<String,Object>>
  • BeanHandler   ----  單行處理器!把結果集轉換成Bean,該處理器需要Class參數,即Bean的類型
  • BeanListHandler   ---- 多行處理器!把結果集轉換成List<Bean>  
  • ColumnListHandler   ----  多行單列處理器!把結果集轉換成List<Object>,使用ColumnListHandler時需要指定某一列的名稱或編號,例如:new ColumListHandler(“name”)表示把name列的數據放到List中。
  • ScalarHandler   ----    單行單列處理器!把結果集轉換成Object。一般用於聚集查詢,例如select count(*) from tab_student。

詳細請看DBUtils的API,在DBUtils包的apidocs文件夾下!!!!

如何使用:

1.導包:commons-dbutils-1.6.jar  依賴  commons-logging-1.2.jar  照舊驅動包

2.代碼


基本使用:

<span style="white-space:pre">	</span>@Test
	public void fun1() throws SQLException {
		QueryRunner qr = new QueryRunner();
		String sql = "insert into user values(?,?,?)";
		qr.update(JdbcUtils.getConnection(), sql, "u1", "zhangSan", "123");
	}
	@Test
	public void fun2() throws SQLException {
		QueryRunner qr = new QueryRunner(JdbcUtils.getDataSource());//這種方式在創建QueryRunner時傳遞池對象,那麼在調用update()時不用在傳遞connection了。
		String sql = "insert into user values(?,?,?)";
		qr.update(sql, "u1", "zhangSan", "123");
	}


</pre><pre>

QueryRunner之查詢

import java.sql.Connection;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.commons.dbutils.QueryRunner;
import org.apache.commons.dbutils.handlers.MapHandler;
import org.apache.commons.dbutils.handlers.MapListHandler;
import org.junit.Test;

public class JdbcDemo {
	@Test
	public void fun1() throws SQLException {
		DataSource ds = JdbcUtils.getDataSource();
		QueryRunner qr = new QueryRunner(ds);
		String sql = "select * from tab_student where number=?";
		Map<String,Object> map = qr.query(sql, new MapHandler(), "S_2000");<span style="white-space:pre">		</span>//把一行記錄轉換成一個Map,其中鍵爲列名稱,值爲列值
		System.out.println(map);
	}
	
	@Test
	public void fun2() throws SQLException {
		DataSource ds = JdbcUtils.getDataSource();
		QueryRunner qr = new QueryRunner(ds);
		String sql = "select * from tab_student";
		List<Map<String,Object>> list = qr.query(sql, new MapListHandler());<span style="white-space:pre">		</span>//把轉換集轉換成List<Map>,其中每個Map對應一行記錄
		for(Map<String,Object> map : list) {
			System.out.println(map);
		}
	}
	
	@Test
	public void fun3() throws SQLException {
		DataSource ds = JdbcUtils.getDataSource();
		QueryRunner qr = new QueryRunner(ds);
		String sql = "select * from tab_student where number=?";
		Student stu = qr.query(sql, new BeanHandler<Student>(Student.class), "S_2000");<span style="white-space:pre">		</span>//把結果集轉換成一個Bean對象,在使用BeanHandler時需要指定Class,即Bean的類型
		System.out.println(stu);
	}
	
	@Test
	public void fun4() throws SQLException {
		DataSource ds = JdbcUtils.getDataSource();
		QueryRunner qr = new QueryRunner(ds);
		String sql = "select * from tab_student";
		List<Student> list = qr.query(sql, new BeanListHandler<Student>(Student.class));<span style="white-space:pre">	</span>//把結果集轉換成List<Bean>,其中每個Bean對應一行記錄
		for(Student stu : list) {
			System.out.println(stu);
		}
	}
	
	@Test
	public void fun5() throws SQLException {
		DataSource ds = JdbcUtils.getDataSource();
		QueryRunner qr = new QueryRunner(ds);
		String sql = "select * from tab_student";
		List<Object> list = qr.query(sql, new ColumnListHandler("name"));<span style="white-space:pre">			</span>//多行單例處理器,即獲取name列數據
		for(Object s : list) {
			System.out.println(s);
		}
	}
	
	@Test
	public void fun6() throws SQLException {
		DataSource ds = JdbcUtils.getDataSource();
		QueryRunner qr = new QueryRunner(ds);
		String sql = "select count(*) from tab_student";
		Number number = (Number)qr.query(sql, new ScalarHandler());<span style="white-space:pre">				</span>//單行單列處理器,一般用於聚合查詢,在使用ScalarHandler時可以指定列名,如果不指定,默認爲第1列。

<span style="white-space:pre">		</span>//對聚合函數的查詢結果,有的驅動返回的是Long,有的返回的是BigInteger,所以這裏我們把它轉換成Number,Number是Long和BigInteger的父類!然後我們再調用Number的intValue()或longValue()方法就OK了。
		int cnt = number.intValue();<span style="white-space:pre">								</span>
		System.out.println(cnt);
	}

}


QueryRunner之批處理

@Test
public void fun10() throws SQLException {
	DataSource ds = JdbcUtils.getDataSource();
	QueryRunner qr = new QueryRunner(ds);
	String sql = "insert into tab_student values(?,?,?,?)";
	Object[][] params = new Object[10][];<span style="white-space:pre">								</span>//表示 要插入10行記錄,注意,這裏是二維數組,這個二維數組有10個一維數組。
	for(int i = 0; i < params.length; i++) {
		params[i] = new Object[]{"S_300" + i, "name" + i, 30 + i, i%2==0?"男":"女"};
	}
	qr.batch(sql, params);<span style="white-space:pre">										</span>//執行批處理










發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章