###### tags: `JDBC`
# JDBC - 課本筆記
## API 簡介
### 設計目的
#### 資料庫種類
* <font color = red>關聯式資料庫</font>(Relational DataBase Management System)
* 以表格方式儲存與呈現資料,再用數學集合論為基礎,將表格之間建立關聯以處理複雜的資料關係
* ex. Oracle-Database, MySQL, Microsoft-MS.SQL Server, IBM-DB2
* 非關聯式資料庫(NoSQL)
* 分散式進行資料儲存,可區分三大類:鍵值儲存(Key-Value)、文檔儲存與圖形關係儲存
* ex. Google-BigTable, MongoDB, Cassandra, Redis等
#### 程式與資料庫通訊
* 資料庫本身是一個獨立運行的應用程式,所以我們設計的應用程式得利用網路通訊協定對資料庫進行指令交換,以便進行資料的<font color = red>CRUD</font>(Create, Retrieve, Update, Delete)

* 但實際上我們會使用一組專門與資料庫進行通訊協定的類別庫來簡化與資料庫溝通的程式撰寫

* 問題:
1. 不同廠商資料庫的通訊協定都不一樣,需使用不同類別庫撰寫程式
2. 遇到更換資料庫,程式碼也幾乎跟著要重寫
3. 應用程式被類別庫綁死,<font color = red>跨平台議題考量</font>
* 解決:

#### 資料庫種類
* Java提供一組資料庫存取API,名為JDBC(Java DataBase Connectivity)
* JDBC是用於<font color = red>執行SQL的解決方案</font>,Java應用程式開發人員使用JDBC的標準介面,而資料庫廠商則對介面進行實作。因此應用程式開發人員就不須接觸到底層資料庫的驅動程式
* JDBC API使得應用系統開發者能使用相同的介面名稱與方法來存取資料庫的資料,讓程式設計師能專心於應用系統的開發,無需顧慮到不同廠商所提供的不同資料庫系統
### JDBC API套件
* JDBC API已包含在JDK裡
* <font color = red>java.sql</font>:JDBC的核心API,用來存取資料庫資料,基本功能
* <font color = red>javax.sql</font>:JDNC Extension API,支援進階資料庫存取功能
* JDBC標準API分為兩個部分
1. <font color = red>JDBC應用程式開發者介面</font>(Application Developer Interface)
2. JDBC驅動程式開發者介面(Drive Developer Interface)
* 註:驅動程式為資料庫廠商實作,一般開發者無須瞭解
#### java.sql

---
## JDBC Driver
### 作用

* 利用JDBC介面實現<font color = red>降低相依性設計</font>,更換資料庫只需替換底層驅動的部分即可
### 四種JDBC Driver
#### Typr 1 Driver
* JDBC-ODBC Brige Driver
* JDK安裝即附,將JDBC的運作轉成ODBC(微軟開發)的機制來連接資料庫,存取速度與功能均有受限,彈性不足,<font color = red>建議在無其他Driver可以使用時才用Type1的Driver</font>
(<font color = red>JDK 8已將ODBC相關類別庫移除</font>)

#### Type 2 Driver
* Native API Driver
* 此類型Driver會以原生(Native)方式呼叫資料庫提供的原生程式庫(通常是C/C++實作),因為採用原生方式,故存取速度較Type1快,但<font color = red>沒有達到跨平台的目標</font>,需要在各平台先行安裝資料庫所屬的原生程式庫

#### Type 3 Driver
* JDBC-Net Driver
* 此類型Driver會將JDBC呼叫轉換為特定的網路協定(Protocol),由中介伺服器或元件跟資料庫進行操作,使用此類型driver<font color = red>好處是軟體架構可獲得彈性,但速度會較慢</font>

#### Type 4 Driver
* Native Protocol Driver
* 此類型Driver通常由資料庫廠商直接提供,會將JDBC呼叫轉換為與資料庫特定的網路協定,Driver可以完全用Java技術實現,因此達到<font color = red>跨平台功能,效能也有不錯表現,為業界最常見的Driver類型</font>

### 設定
* JDBC Driver可以從各資料庫網站下載並取得,基本上都是免費取得,而Type 4 Driver通常都是以JAR檔的形式打包好提供給程式設計師們使用
* 在Eclipse的Java Project裡,需要將JAR引入給專案才能使用,但通常會再建議將下載到的JAR檔放置於我們自行設定的classpath下,這樣就能在獨立環境也能正常運作
---
## Driver 註冊
### 註冊方式
* 載入JDBC Driver有三種方式
1. Class Loader
* <font color = red>常見,主流</font>(透過Java的類別載入器,將驅動程式的class檔載入)
2. register
* 不建議
3. System Property
* 依照需求,若是要<font color = red>同時註冊多個</font>驅動資訊時可以使用
* 可參考Java-基礎 → import & classpath → 類別路徑 (classpath)
#### Class Loader
* 採用Class Loader方式產生Driver實體,並註冊到DriverManager的驅動程式註冊表中
* Class.forName(“com.mysql.jdbc.Driver”); → MySQL 8以前
* <font color = red>Class.forName(“com.mysql.cj.jdbc.Driver”); (MySQL 8以後的驅動名稱)</font>
* Class.forName(“oracle.jdbc.driver.OracleDriver”);
* Class.forName(“com.microsoft.sqlserver.jdbc.SQLServerDriver”);
* Class.forName(“com.ibm.db2.jdbc.app.DB2Driver”);
p.s. <font color = red>驅動類別的名稱為固定且公開的資訊,都可以在各家資料庫的JDBC說明文件取得</font>
#### register (<font color = red>不建議</font>)
* 採用register方式產生Driver實體,並將自己註冊到Driver Manager驅動程式註冊表單中
* DriverManager.registerDriver(new com.mysql.cj.jdbc.Driver());
* DriverManager.registerDriver(new oracle.jdbc.driver.OracleDriver());
此動作會產生<font color = red>兩個驅動物件</font>,可能會導致最後<font color = red>資源釋放的不完整</font>
#### System Property
* 採用System Property方式
* System.setProperty(“jdbc.drivers”, “com.mysql.jdbc.Driver”);
* 可以包含數個drivers,彼此之間以冒號(:)隔開
```java=
Class.forName("com.mysql.jdbc.Driver:oracle.jdbc.driver.OracleDriver");
```
### 類別載入器
* Java在執行時,需要某類別才會載入.class檔案,而非在程式啟動就載入所有類別,才能讓系統資源的運用更有效率。而載入.class檔案,就會產生一個java.lang.Class的物件實體來對應該檔案,程式設計師即可利用該class物件實體而更進一步地取得該類別的相關資訊,此機制就稱為反射或映射(Reflection)
* 需要某類別的時機
* 使用指定類別要產生物件實體時(static區塊會<font color = red>執行</font>)
* 使用Class.forName()方法
* 使用ClassLoader的loadClass方法(static區塊<font color = red>不執行</font>)
* JDK 8以前類別載入器可分為三個層級
-------->
| Bootstrap Loader | ExtClassLoader | AppClassLoader |
|:----------------------:|:---------------------------:|:----------------------------:|
| Java預設 | 延伸 | 自訂 |
| 負責載入rt.jar裡的類別 | 負責載入延伸library裡的類別 | 負責載入使用者自己設定的類別 |
### 自動載入JDBC Driver (JDBC 4.0)
* 在JDBC 4.0,也就是對應到Java SE6,增加了簡化資料庫驅動載入
* 不需用再呼叫Class.forName方法載入驅動,但在包裝JDBC驅動程式的JAR檔裡,必須要額外有一個「META-INF/services/java.sql.Driver」的檔案
* 該檔案裡要有註明驅動程式的類別名稱,才能達到簡化載入的操作
---
## 資料庫連線的建立與關閉
### 建立資料庫連線
#### 資料庫位置 (URL)
* 資料庫URL為一字串,用以說明連結某一特定資料庫所需要的資訊,包含資料庫位址(IP)、埠號(port number)、名稱等
* 資料庫URL格式(公開且固定):
```java=
jdbc: <Subprotocol> : <Subname>
"jdbc:mysql://" + serverName + ":3306/" + Schema Name
public static final String URL = "jdbc:mysql://localhost:3306/HR"
// "jdbc:mysql://localhost:3306/HR?serverTimezone=Asia/Taipei"
// 從MySQL 8開始後面要再加上時區資訊
"jdbc:oracle:thin:@" + serverName + ":1521" + SID
public static final String URL = "jdbc:oracle:thin:@localhost:1521:xe"
// xe指的是Oracle的Express版
"jdbc:sqlserver://" + serverName + ":1433:databaseName=" + DBName
public static final String URL = "jdbc:sqlserver://localhost:1433:databaseName=HR"
```
#### Connection
* Connection代表資料庫一個連線,傳遞一系列的SQL指令給資料庫,並管理這些指令的認可與中止
* Connection con = DriverManager.getConnection(String url, String userID, String password);
* 以資料庫URL作為引數產生Connection物件來連接資料庫
* DriverManager在驅動程式註冊表單搜尋driver,一旦driver認得此url,便會依據url的資訊連接資料庫
* <font color = red>Connection非常珍貴,使用完畢後務必完成歸還的動作!</font>
### 關閉連線-close()
* close():歸還資源
* Connection是極為重要的資源,<font color = red>建議寫在finally{...}裡</font>
* 以下作法將con先建立在外面成為區域變數,在finally{}時再<font color = red>對con檢查不為null</font>是為了<font color = red>避免連線尚未建立就發生例外</font>,導致例外訊息再堆疊NullPointerException訊息而增加除錯時的複雜度
```java=
Connection con = null;
try{
程式內容;
}catch(SQLException se){
se.printStrackTrace();
}finally{
if(con != null){
try{
con.close();
}catch(SQLException se){
se.printStackTrace();
}
}
}
```
### Auto-Closeable介面 ( try-with-resources )
* 以往程式設計師需自行關閉資源,Java 7開始try – with – resources可確保物件(資源)最後一定會關閉
* 實作<font color = red>java.lang.AutoCloseable</font>與<font color = red>java.io.Closeable</font>介面的物件皆可視為資源
* 若將有實作AutoCloseable介面的物件置於try{ }裡,try{ }結束時,Java即會<font color = red>自動將該物件close</font>
* JDK 7裡,Connection介面extends AutoCloseable介面
* 由於仍有可能使用JDK 7以前的版本,故還是<font color = red>寫在finally{ }較保險</font>
---
## 靜態SQL指令 ( Statement )
### 取得Statement物件
* 藉由Connection的方法:Statement createStatement()來建立與取得Statement物件
* 藉由Statement物件,才能執行靜態的SQL指令並與資料庫交談
* 因為是從Connection產生出來,所以在使用完畢時也得記得歸還資源:<font color = red>close()</font>
* 資源歸還順序:<font color = red>越晚建立,越早歸還</font>
### 執行SQL指令
* 執行SQL指令的主要兩個方法
* ResultSet excute<font color = red>Query</font>(String sql):查詢資料庫,傳回ResultSet
用於SQL的<font color = red>select</font>指令
* int excute<font color = red>Update</font>(String sql):更新資料庫,傳回成功更新的筆數
用於SQL的<font color = red>insert、update、delete</font>指令
* SQL指令在程式裡是宣告為字串型別,並在呼叫excute相關方法時做為參數傳入,即可送出給資料庫達到交談目的
補充:boolean excute()用來執行未知的SQL指令(Store Procedure預存程序)
* true:代表剛剛執行的結果是個ResultSet,也就是<font color = red>查詢</font>
* false:代表剛剛執行的結果是個更新成功的資料筆數,也就是<font color = red>更新</font>
```java=
//========準備工作========
private static final String Driver = "com.mysql.cj.jdbc.Driver";
// 註冊
private static final String URL =
"jdbc:mysql://localhost:3306/JDBCSample?serverTimzone=Asia/Taipei";
// "jdbc:mysql://" + serverName + ":3306/" + Schema Name + "時區資訊"
private static final String USER = "David";
// 帳號名稱
private static final String PASSWORD = "123456";
// 密碼
//========準備開始========
public static void main(String[] args){
Connection con = null;
// 建立連線
Statement stmt = null;
// 取得物件
try{
Class.forName(Driver);
// 載入驅動註冊
con = DriverManager.getConnection(URL, USER, PASSWORD);
// 使用URL, USER, PASSWORD 進行連線
stmt = con.createConnection();
// 取得物件
int rowCount = stmt.excuteUpdate(
"insert into DEPARTMENT(DEPTNO, DNAME, LOC) values(50, 會計部, 台北)");
// 新增一筆資料(DEPTNO:50, DNAME:會計部, LOC:台北)到DEPT
// 新增、刪除、修改皆使用此方法
}catch (SQLException se){
se.printStackTrace();
}finally{
if(stmt != null){
try{
stmt.close();
}catch(SQLException se){
se.printStackTrace();
}
}
// 判斷stmt是否建立成功,成功的話close();
if(con != null){
try{
con.close();
}catch(SQLException se){
se.printStackTrace();
}
}
// 判斷con是否連線成功,成功的話close();
}
}
```
---
## ResultSet 介面
### 游標機制
* 當執行excuteQuery()方法,該方法即回傳從資料庫查詢的結果資料列
ex. select * from EMPLOYEE

### 移動游標操作
* 移動資料列游標的相關方法
* next()、privious()、first()、last()、beforeFirst()、afterLast()
* <font color = red>next()</font>傳回 boolean → 此布林值就拿來當作迴圈操作的條件
* true → 游標停留的位置<font color = red>有指向一個資料列</font>
* false → 游標停留的位置<font color = red>沒有</font>指向一個資料列
* 測試目前資料列指標位置的方法
* isFirst()、isLast()、isBeforeFirst()、isAfterLast()
* <font color = red>部分移動游標方法為JDBC 2.0時新增,需在建立Statement時設定!</font>
### 使用游標取得欄位資料
* 取出查詢結果資料欄的方法
* 有type getType(<font color = red>int columnIndex</font>)或type getType(<font color = red>String columnName</font>)
* JDBC的<font color = red>index從1開始</font>,一般來說使用index比較有效率
* 但要小心查詢欄位的順序關係 (因為查詢結果的欄位順序會跟下SQL指令有直接關聯)
* getBoolean()、getByte()、getShort()、getInt()、getLong()、getFloat()、getDouble()、getString()、getDate()、getTime()、getTimestamp()、getBinaryStream()
* 其他方法:
* int getRow():取得目前的列編號,<font color = red>若傳回0</font>,表示游標不再任一列上,可能在<font color = red>第一列前</font>或<font color = red>最後一列後</font>
```java=
//========準備工作========
private static final String Driver = "com.mysql.cj.jdbc.Driver";
private static final String URL =
"jdbc:mysql://localhost:3306/JDBCSample?serverTimzone=Asia/Taipei";
private static final String USER = "David";
private static final String PASSWORD = "123456";
//========準備開始========
public static void main(String[] args){
Connevtion con = null;
Statement stmt = null;
ResultSet rs = null;
try{
Class.forName(Driver);
con = DriverManager.getConnection(URL, USER, PASSWORD);
stmt = con.createStatement();
rs.excuteQuery("select * from DEPATMENT");
while(rs.next()){
int deptno = rs.getInt("DEPTNO");
String dname = rs.getString("DNAME");
String loc = rs.getString('LOC');
System.out.println("DEPTNO: " + deptno);
System.out.println("DNAME: " + dname);
System.out.println("LOC: " + loc);
System.out.println("=====分隔線=====");
}catch(SQLException se){
se.printStackTrace();
}finally{
if(rs != null){
try{
rs.close();
}catch(SQLException se){
se.printStackTrace();
}
}
if(stmt != null){
try{
pstmt.close();
}catch(SQLException se){
se.printStackTrace();
}
}
if(con != null){
try{
con.close();
}catch(SQLException se){
se.printStackTrace();
}
}
}
}
}
```
---
## 動態SQL指令 ( PreparedStatement )
### 串接組成動態SQL指令
#### SQL指令動態參數改變
* 試想系統操作時,每次要新增的資料或是要查詢的條件不會永遠相同的,但Statement物件只能讓我們執行靜態(固定不變)的SQL指令
* 因為SQL指令在Java裡宣告為String,所以剛好結合變數與文字串接功能,就可以達到SQL指令參數可隨著程式執行而動態改變
ex.

### SQL Injection (SQL 注入攻擊)
* 若是使用者輸入userName與userPassword為1'or'1'='1
* 會變成(NAME = '1'or'1'='1')and(PASSWORD = '1'or'1'='1')
* '1' or true
* 以上情況,輸入文字夾帶SQL指令內容,若是未做檢查處理,很有可能被組合成一個合法且有意義的SQL指令,讓資料庫做出完全不一樣的任務,造成被有心人士入侵或是破壞
```java=
private static final String Driver = "com.mysql.cj.jdbc.Driver";
private static final String URL =
"jdbc:mysql://localhost:3306/JDBCSample?serverTimezone=Asia/Taipei";
private static final String USER = "David";
private static final String PASSWORD = "123456";
public static void main(String[] args){
Connection con = null;
Statement stmt = null;
ResultSet rs = null;
String userName = "";
String userPassword = "";
Scanner sc = new Scanner(System.in);
System.out.println("請輸入帳號");
userName = sc.nextLine;
System.out.println("請輸入密碼");
userPassword = sc.nextLine();
try{
Class.forName(Driver);
con = DriverManager.getConnetion(URL, USER, PASSWORD);
stmt = con.createStatement();
rs = stmt.excuteQuery(
"select * from MEMBER where (UserName = '" + userName "') and (UserPassword = '" + useerPassword "')");
while(rs.next()){
System.out.println("UserID: " + rs.getInt("ID"));
System.out.println("UserName: " + rs.getString("NAME"));
System.out.println("UserPassword: " + rs.getString("PASSWORD"));
}
}catch(SQLException se){
se.printStackTrace();
}finally{
if(rs != null){
try{
rs.close();
}catch(SQLException se){
se.printStackTrace();
}
}
if(stmt != null){
try{
stmt.close();
}catch(SQLException se){
se.printStackTrace();
}
}
if(con != null){
try{
con.close();
}catch(SQLException se){
se.printStackTrace();
}
}
}
}
```
### PreparedStatement介面
* PreparedStatement為Statement的子介面,因此也可以執行SQL指令
* 藉由Connection的方法:prepareStatement(String sql)
資料庫可預先編譯SQL指令,執行效能較快,常用於需變數傳遞且重複執行的SQL指令
* 因為是從Connection產生出來,所以在使用完畢時也得記得歸還資源:close()
```java=
private static final String Driver = "com.mysql.cj.jdbc.Driver";
private static final String URL =
"jdbc:mysql://localhost:3306/JDBCSample?serverTimezone=Asia/Taipei";
private static final String USER = "David";
private static final String PASSWORD = "123456";
public stativ void main(String[] args){
Connection con = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
String userName = "";
String userPassword = "";
Scanner sc = new Scanner(System.in);
System.out.println("請輸入帳號");
userName = sc.nextLine();
System.out.println("請輸入密碼");
userPassword = sc.nextLine();
try{
Class.forName(Driver);
com = DriverManager.getConnetion(URL, USER, PASSWORD);
pstmt = com.prepareStatement(
"select * from MEMBER where UserName = ? and UserPassword = ?");
// 預先處理
pstmt.setString(1, userName);
// 對索引值1進行給值 UserName
pstmt.setString(2, userPassword);
// 對索引值2進行給值 UserPassword
rs.excuteQuery();
// 執行查詢
while(rs.next()){
System.out.println("UserID: " + rs.getInt("ID"));
System.out.println("UserName: " + rs.getString("NAME"));
System.out.println("UserPassword: " + rs.getString("PASSWORD"));
}
}catch(SQLException se){
se.printStackTrace();
}finally{
if(rs != null){
try{
rs.close();
}catch(SQLException se){
se.pritStackTrace();
}
}
if(pstmt != null){
try{
pstmt.close();
}catch(SQLException se){
se.printStackTrace();
}
}
if(con != null){
try{
con.close();
}catch(SQLException se){
se.printStaceTrace();
}
}
}
}
```
---
## 動態SQL指令運用 ( PreparedStatement )
### PreparedStatement
* 資料庫會將SQL指令預先編譯,可避免資料庫重複解析同一個SQL指令,執行效能較好
<font color = red>因為可以動態處理SQL指令,可避免SQL Injection</font>
* 因為先將部分SQL指令交給資料庫做編譯處理,等拿到輸入的資料,就<font color = red>無法</font>組合成有意義的SQL指令,<font color = red>避免SQL Injection</font>
* 對SQL指令裡,未知或是需要動態改變的參數設定為" ? ",並在每次執行時,對 ? 置入不同的值,透過setType(int index, Type value)提供參數值
<font color = red>? 的索引值從 1 開始</font>
* PreparedStatement執行動態SQL指令的兩個方法
* ResultSet excuteQuery();
* int excuteUpdate();
* 使用<font color = red>PreparedStatement</font>做執行呼叫時就不必再傳參數,因為<font color = red>SQL指令</font>已經<font color = red>預先交給資料庫</font>處理了
參考 <font color = red>PreparedStatement 介面</font> 範例
---
## 預存程序 ( CallableStatement )
* 此部分內容大多由資料庫開發人員撰寫
### CallableStatement (補充資料)
* 繼承PreparedStatement介面,所以也有動態參數功能(搭配" ? ")
* CallableStatement prepareCall(String sql):使用預存程序(stored procedure),預存程序已事先內建在資料庫中,通常比預先編輯的效能加
* 預存程序(stored procedure)在應用程式執行前,已事先編譯好在資料庫裡,因此預先程序的效能一般比預先編譯的敘述快
* <font color = red>程式設計師只需知道預存程序的名稱與輸入輸出的參數,無須了解SQL指令</font>
* 不同的資料庫有不同的預存程序語法;對三階開發人員(前端)來說,意義不大
---
## ResultSet 進階操作
### 游標進階操作 (JDBC 2.0/Java 1.4)
* <font color = red>JDBC 2.0</font>對ResultSet做了許多加強,允許更彈性的移動游標與資料更新等進階操作
* 建立Statement物件時進行設定:
* Statement <font color = red>createStatement</font>(int resultSetType, int resultSetConcurrency)
* PreparedStatement <font color = red>prepareStatement</font>(String sql, int resultSetType, int resultSetConcurrency)
* 驅動程式必須能夠支援,否則會發生Runtime Exception
* <font color = red>不是所有驅動程式都支援</font>
* 相關參數(都是在ReusltSet介面已定義好的static final的整數常數):
* ResultSetType
* TYPE_FORWARD_ONLY(只能向前)
* TYPE_SCROLL_INSENSITIVE(可前後移動,不反應資料修改)
* TYPE_SCROLL_SENSITIVE(可前後移動,反應資料修改)
* ResultSetConcurrency
* CONCUR_READ_ONLY(資料為唯讀)
* CONCUR_UPDATAVLE(資料可異動,ex.新增、修改、刪除)
註:若沒設定,<font color = red>預設為TYPE_FORWARD_ONLY `&` CONCUR_READ_ONLY</font>
---
## 中繼資料 ( Metadata )
### Metadata 介紹
* 此名詞源自於1969年,由Jack E. Myers提出"關於資料的資料"(data about data),後被定義為"<font color = red>描述資料的資料</font>"而沿用至今,不過各方定義紛紛出現,現在主要分為三種Metadata:
* 描述性metadata
* 結構性metadata
* 管理姓metadata
* 已書本為例,對應上面三種metadata
* 描述型metadata:書名、作者
* 結構性metadata:目錄、章節、頁數
* 管理姓metadata:ISBN國際書碼 (書的身分證)
### JDBC Metadata
* 透過Connection的getMetadata方法取得DatabaseMetaData物件,而DatabaseMataData可取得資料庫相關資訊,為研發人員用來寫獨立於資料庫的驅動程式和開發工具
* 透過<font color = red>ResultSet的getMetaData</font>方法即可取ResultSetMetaData物件,也是<font color = red>應用程式開發人員較常用到的metadata</font>,因為可以獲取查詢資料列以外的相關訊息
* ResultSetMetaData常用方法:
* int getColumnCount():取得欄位數
* String getColumnName(int col):取得欄位名
* String getColumnTypeName(int col):取得欄位在DBMS所使用的SQL type的名稱
* String getTableName(int col):取得欄位所屬表格名稱
* int isNullable(int col):欄位能否為null
```java=
private static final String Driver = "com.mysql.cj.jdbc.Driver";
private static final String URL =
"jdbc:mysql://localhost:3306/JDBCSample?serverTimezone=Asia/Taipei";
private static final String USER = "David";
private static final String PASSWORD = "123456";
private static final String SELECT_ALL = "select * from DEPARTMENT";
public static void main(String[] args){
Connection con = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
ResultSetMetaData rsmd = null;
try{
Class.forName(Driver);
con = DriverManager(URL, USER, PASSWORD);
pstmt = con.prepareStatement(SELECT_ALL);
rs = pstmt.excuteQuery();
rsmd = re.getMetaData();
int numOfColumn = rsmd.getColumnCount();
// 取得欄位數量
// 印出個欄位資訊內容
for(int i = 1; i < numOfColumn; i++){ // 第幾個欄位
String colName = rsmd.getColumnName(i); // 欄位名稱
String table = rsmd.getTableName(i); // 表格名稱
String colType = rsmd.getColumnTypeName(i); // 欄位類型
boolean caseSen = rsmd.getCaseSensitive(i); // 內容是否區分大小寫
boolean writable = rsmd.isWritable(i); // 內容是否可以修改
boolean isNull = rsmd.isNullable(i); // 內容是否可以為空值
System.out.println("欄位資訊:" + colName);
System.out.println("屬於表格:" + table);
System.out.println("欄位內容類型:" + colType);
System.out.println("是否區分大小寫:" + caseSen);
System.out.println("內容是否可以修改:" + writable);
System.out.println("內容是否可以為空值:" + isNull);
System.out.println("========分隔線========");
}
}catch(ClassNotFoundException ce){
ce.printStackTrace();
}catch(SQLException se){
se.printStackTrace();
}finally{
if(rsmd != null){
try{
rsmd.close();
}catch(SQLException se){
se.printStackTrace();
}
}
if(rs != null){
try{
rs.close();
}catch(SQLException se){
se.printStackTrace();
}
}
if(pstmt != null){
try{
pstmt.close();
}catch(SQLException se){
se.printStackTrace();
}
}
if(con != null){
try{
con.close();
}catch(SQLException se){
se.printStackTrace();
}
}
}
}
```
---
## 批次更新 ( Batch Update )
* <font color = red>MySQL</font>要使用此功能,URL需要<font color = red>加上設定資訊</font>
### Batch Update
* 批次更新(Batch Update)適合於對資料庫進行大量更新的行為,將要執行的SQL指令一次收集完畢後再送出,效能會比一次一次送出SQL指令要來得更好
* 透過addBatch()方法收集SQL指令後,將結合成一句SQL指令,再利用excuteBatch()方法送出,其中執行SQL產生的例外為BatchUpdateException
* 只適用於更新(ex. 新增、修改、刪除),對於查詢功能無法支援
* 需注意各廠商資料庫是否對批次更新的支援,可藉由DatabaseMataData的supportBatchUpdate()得知結果
#### 迴圈式新增
```java=
private static final String Driver = "com.mysql.cj.jdbc.Driver";
private static final String URL =
"jdbc:mysql://localhost:3306/JDBCSample?serverTimezone=Asia/Taipei";
private static final String USER = "David";
private static final String PASSWORD = "123456";
public static void main(String[] args){
Connection con = null;
Statement stmt = null
try{
Class.forName(Driver);
con = DriverManager.getConnection(URL, USER, PASSWORD);
// 使用迴圈新增
for(int i = 1; i <= 5000; i++){
stmt.excuteUpdate(
"insert into EMPLOYEE(EMPNO, ENAME, JOB, HIREDATE, SAL, COMM, DEPTNO)"
+ "values(" + (i + 7014)", David, Manager, '2016-01-01, 2500, 0.0, 40')");
}
}catch(ClassNotFoundException ce){
ce.printStackTrace();
}catch(SQLException se){
se.printStackTrace();
}finally{
if(stmt != null){
try{
stmt.close();
}catch(SQLException se){
se.printStackTrace();
}
}
if(con != null){
try{
con.close();
}catch(SQLException se){
se.printStackTrace();
}
}
}
}
```
#### 批次新增1 (迴圈 + Batch)
```java=
private static final String Driver = "com.mysql.cj.jdbc.Driver";
private static final String URL =
"jdbc:nysql://localhost:3306/JDBCSample?serverTimezone=Asia/Taipei";
private static final String USER = "David";
private static final String PASSWORD = "123456";
public static void main(String[] args){
Connection con = null;
Statement stmt = null
try{
Class.forName(Driver);
con = DriverManager.getConnection(URL, USER, PASSWORD);
// 使用迴圈 + Batch新增
stat = con.createStatement();
for (int i = 1; i <= 5000; i++) {
stat.addBatch("INSERT INTO EMPLOYEE(EMPNO, ENAME, JOB, HIREDATE, SAL, COMM, DEPTNO)"
+ "VALUES(" + (i + 7014) + ", 'DAVID', 'MANAGER', '2016-01-01', 2500, 0.0, 40)");
}
stat.executeBatch();
}catch(ClassNotFoundException ce){
ce.printStackTrace();
}catch(SQLException se){
se.printStackTrace();
}finally{
if(stmt != null){
try{
stmt.close();
}catch(SQLException se){
se.printStackTrace();
}
}
if(con != null){
try{
con.close();
}catch(SQLException se){
se.printStackTrace();
}
}
}
}
```
#### 批次新增2 (PreparedStatement + Batch)
```java=
private static final String Driver = "com.mysql.cj.jdbc.Drive"
private static final String URL =
"jdbc:mysql://localhost:3306/JDBCSample?serverTimezone=Asia/Taipei";
private static final String USER = "David";
private static final String PASSWORD = "123456";
private static final String EMP_ADD =
"insert into EMPLOYEE(EMPNO, ENAME,JOB, HIREDATE, SAL, COMM, DEPTNO)"
+ "values(?, ?, ?, ?, ?, ?, ?)";
public static void main(String[] args){
Connection con = null;
PreparedStatement pstmt = null;
try{
Class.forName(Driver);
con = DriverManager.getConnection(URL, USER, PASSWORD);
pstmt = con.prepareStatement(EMP_ADD);
// 使用PreparedStatement + Batch新增
for(int i = 1; i <= 5000; i++){
pstmt.setInt(1, (i + 7014));
pstmt.setString(2, "David");
pstmt.setString(3, "Manager");
pstmt.setDate(4, java.sql.Date.valueOf("2016-01-01"));
pstmt.setDouble(5, 2500);
pstmt.setDouble(6, 0.0);
pstmt.setInt(7, 40);
}
// 批次處理
pstmt.excuteBatch();
}catch(ClassNotFoundException ce){
ce.printStackTrace();
}catch(SQLException se){
se.printStackTrace();
}finally{
if(pstmt != null){
try{
pstmt.close();
}catch(SQLException se){
se.printStackTrace();
}
}
if(con != null){
try{
con.close();
}catch(SQLException se){
se.printStackTrace();
}
}
}
}
```
---
## JDBC 例外處理
### SQLException相關
* SQLException為受檢例外(Checked Exception) → 一定要處理
* getMessage() - 足以應付大部分情況,訊息是驅動程式在執行產生例外時加入
* getSQLState() - 回傳字串
* getErrorCode() - 回傳整數
* getNextException()
<font color = red>第二、第三點不常用</font>,因為各家廠商對錯誤代碼的定義與資料庫狀態的說明都不一致
* 在JDBC 4.0時,SQLException新增了幾個以Throwable為建構子參數的建構子,就可以將任何其他類型的例外也包裝成SQLException再做後續處理
* SQLException也同時時做了Iterable`<Throwable>`介面,可以搭配for-each取出
### SQLWarning相關
* SQLWarning與DataTruncation
* SQLWarning不會終止程式執行,也不常出現
* Connection、ResultSet與Statement相關都可能會有SQLWarning
* 可透過getWarnings()取得SQLWarning物件
* 訊息取得方式跟SQLEception相同 (getMessage())
* DataTruncation is a SQLWarning,主要就是資料截斷的問題發生
ex. 對於浮點數欄位卻以整數方式取得,雖不至於導致程式發生例外,但因為資料取得的不完整,就是屬於一種警告
* <font color = red>MySQL不支援捕捉警告</font>
---
## BLOB `&` CLOB
### BLOB `&` CLOB
* <font color = red>Binary Large OBject</font> ( <font color = red>BLOB</font> ),讓我們可以將檔案(ex. 圖片、影音等資料)存入資料庫欄位裡,單筆最高可達4GB
* <font color = red>Character Larger OBject</font> ( <font color = red>CLOB</font> ),與BLOB不同的是,可以用CLOB儲存大量的文字資料,最高大小也可至4GB
* 在MySQL中以LONGTEXT表示型別
* SQL指令對以上兩種資料類型的支援度還不夠完善,因此常結合程式碼來處理與操作BLOB/CLOB
* 注意:創建表格時,欄位需先宣告為BLOB/CLOB類型
### BLOB
* JDBC API提供了Blob介面給資料庫廠商可以實作
* 相關處理做法
* getBytes/setBytes:任何Java版本都可以使用,適合位元資料量不大的情況
* getBlob/setBlob:JDBC 4.0( Java 6 )以後才能使用,以資料流的方式操作
* getBinaryStream/setBinaryStream:任何Java版本都可以使用,以資料流方式操作,<font color = red>適合資料較大的情況</font>(ex. 100MB以上)
* 結合資料流操作,取得byte[ ] 資料
* JDBC 4.0時,PreparedStatement新增了setBlob(int parameterIndex, InputStream inputStream),改進了使用方法與操作
#### 存入
```java=
private static final String Driver = "com.masql.cj.jdbc.Driver";
private static final String URL =
"jdbc:mysql://localhost:3306/JDBCSample?serverTimezone=Asia/Taipei";
private static final String USER = "David";
private static final String PASSWORD = "123456";
private static final String SQL =
"INSERT INTO CLUB(ID, NAME, PIC) VALUES (?, ?, ?)";
public static void main(String[] args){
Connection con = null;
PreparedStatement pstmt = null;
try{
Class.forName(Driver);
con = DriverManager.getConnection(URL, USER, PASSWORD);
pstmt = con.prepareStatement(SQL);
// method 1 setBlob (JDBC 4.0) Java 6
pstmt.setInt(1, 1); // ( 索引值, 資料 ) SQL索引值從 1 開始
pstmt.setString(2, "拜仁慕尼黑");
InputStream is = getPictureStream("items/FC_Bayern.png");
pstmt.setBlob(3, is); // setBlob
pstmt.executeUpdate();
is.close();
// method 2 setBytes *** 萬用 1 ***
// 動作為一次性的,適合小資料量傳輸
pstmt.setInt(1, 2);
pstmt.setString(2, "巴塞隆納");
byte[] pic = getPictureByteArray("items/FC_Barcelona.png");
pstmt.setBytes(3, pic); // setBytes
pstmt.executeUpdate();
// method 3 setBinaryStream *** 萬用 2 ***
// 1 跟 3 的差別在於版本支援度
// 使用資料流的方式,適合傳輸較大量的資料 ex. 100 MB 以上
pstmt.setInt(1, 3);
pstmt.setString(2, "皇家馬德里");
InputStream is2 = getPictureStream("items/FC_Real_Madrid.png");
pstmt.setBinaryStream(3, is2); // setBinaryStream
pstmt.executeUpdate();
is2.close();
}catch(ClassNotFoundException ce){
ce.printStackTrace();
}catch(SQLException se){
se.printStackTrace();
}catch(IPException ie){
ie.printStackTrace();
}finally{
if(pstmt != null){
try{
pstmt.close();
}catch(SQLException se){
se.printStackTrace();
}
}
if(con != null){
try{
con.close();
}catch(SQLException se){
se.printStackTrace();
}
}
}
}
// 使用InputStream資料流方式 Method 1
public static InputStream getPictureStream(String path) throws IOException {
FileInputStream fis = new FileInputStream(path);
return fis;
}
// 使用byte[]方式 Method 2
public static byte[] getPictureByteArray(String path) throws IOException {
FileInputStream fis = new FileInputStream(path);
byte[] buffer = new byte[fis.available()];
// fis.available() = length() 取得資料量大小
// byte[] 大小等於資料大小
fis.read(buffer);
fis.close();
return buffer;
}
```
#### 取出
```java=
private static final String DRIVER = "com.mysql.cj.jdbc.Driver"
private static final String URL =
"jdbc:mysql://localhost:3306/JDBCSample?serverTimezone=Asia/Taipei";
private static final String USER = "David";
private static final String PASSWORD = "123456";
private static final String SQL = "SELECT PIC FROM CLUB WHERE ID = ?";
public static void main(String[] args){
Connection con = null;
PreparedStatement pstmt = null;
ResultSet rs1 = null, rs2 = null, rs3 = null;
try{
Class.forName(DRIVER);
con = DriverManager.getConnection(URL, USER, PASSWORD);
pstmt = con.prepareStatement(SQL);
// method 1 getBlob
pstmt.setInt(1, 1);
rs1 = pstmt.executeQuery();
rs1.next(); // 移動游標
Blob blob = rs1.getBlob(1);
readPicture(blob);
// method 2 getBytes
pstmt.setInt(1, 2);
rs2 = pstmt.executeQuery();
rs2.next();
byte[] pic = rs2.getBytes(1);
readPicture(pic);
// method 3 getBinaryStream
pstmt.setInt(1, 3);
rs3 = pstmt.executeQuery();
rs3.next();
InputStream is = rs3.getBinaryStream(1);
readPicture(is);
is.close();
}catch(ClassNotFoundException ce){
ce.printStackTrace();
}catch(SQLException se){
se.printStackTrace();
}catch(IOException ie){
ie.printStackTrace();
}finally{
if(rs1 != null){
try{
rs1.close();
}catch(SQLException se){
se.printStackTrace();
}
}
if(rs2 != null){
try{
rs2.close();
}catch(SQLException se){
se.printStackTrace();
}
}
if(rs3 != null){
try{
rs3.close();
}catch(SQLException se){
se.printStackTrace();
}
}
if(pstmt != null){
try{
pstmt.close();
}catch(SQLException se){
se.printStackTrace();
}
}
if(con != null){
try{
con.close();
}catch(SQLException se){
se.printStackTrace();
}
}
}
}
// Handle with blob data
public static void readPicture(Blob blob) throws IOException, SQLException {
InputStream is = blob.getBinaryStream();
FileOutputStream fos = new FileOutputStream("Output/1.png");
int i;
while ((i = is.read()) != -1) {
fos.write(i);
}
fos.flush();
fos.close();
is.close();
}
// Handle with byte array data
public static void readPicture(byte[] bytes) throws IOException {
FileOutputStream fos = new FileOutputStream("Output/2.png");
fos.write(bytes);
fos.flush();
fos.close();
}
// Handle with stream data
public static void readPicture(InputStream is) throws IOException {
FileOutputStream fos = new FileOutputStream("Output/3.png");
int i;
while ((i = is.read()) != -1) {
fos.write(i);
}
fos.flush();
fos.close();
}
```
### CLOB
* JDBC API提供了Clob介面給資料庫廠商可以實作
* 相關處理做法
* getCharacterStream/setCharacterStream(對Unicode編碼文字處理)
* getClob/setClob
* getString/setString
* 結合資料流操作,取得字串資料
* JDBC 4.0時,PreparedStatement新增了setClob(int parameterIndex, Reader reader),改進了使用方法與操作
#### 存入
```java=
private static final String DRIVER = "com.mysql.cj.jdbc.Driver"
private static final String URL =
"jdbc:mysql://localhost:3306/JDBCSample?serverTimezone=Asia/Taipei";
private static final String USER = "David";
private static final String PASSWORD = "123456";
private static final String SQL = "UPDATE CLUB SET INTRO = ? WHERE ID = ?";
public static void main(String[] args){
Connection con = null;
PreparedStatement pstmt = null;
try{
Class.forName(Driver);
con = DriverManager.getConnection(URL, USER, PASSWORD);
pstmt = con.prepareStatement(SQL);
// method 1 setClob (JDBC 4.0)
Reader reader = getLongStringStream("items/BM.txt");
pstmt.setClob(1, reader);
pstmt.setInt(2, 1);
pstmt.executeUpdate();
reader.close();
// method 2 setCharacterStream
Reader reader2 = getLongStringStream("items/Bar.txt");
pstmt.setCharacterStream(1, reader2);
pstmt.setInt(2, 2);
pstmt.executeUpdate();
reader2.close();
// method 3 setString
String str = getLongString("items/RM.txt");
pstmt.setString(1, str);
pstmt.setInt(2, 3);
pstmt.executeUpdate();
}catch(ClassNotFoundException ce){
ce.printStackTrace();
}catch(SQLException se){
se.printStackTrace();
}catch(IOException ie){
ie.printStackTrace();
}finally{
if(pstmt != null){
try{
pstmt.close();
}catch(SQLException se){
se.printStackTrace();
}
}
if(con != null){
try{
con.close();
}catch(SQLException se){
se.printStackTrace();
}
}
}
}
// 使用String
public static String getLongString(String path) throws IOException {
BufferedReader br = new BufferedReader(new FileReader(path));
StringBuilder sb = new StringBuilder(); // StringBuffer is thread-safe!
String str;
while ((str = br.readLine()) != null) {
sb.append(str);
sb.append("\n");
}
br.close();
return sb.toString();
}
// 使用資料流
public static Reader getLongStringStream(String path) throws IOException {
return new FileReader(path);
}
```
#### 取出
```java=
private static final String DRIVER = "com.mysql.cj.jdbc.Driver"
private static final String URL =
"jdbc:mysql://localhost:3306/JDBCSample?serverTimezone=Asia/Taipei";
private static final String USER = "David";
private static final String PASSWORD = "123456";
private static final String SQL = "SELECT INTRO FROM CLUB WHERE ID = ?";
public static void main(String[] args){
Connection con = null;
PreparedStatement pstmt = null;
try{
Class.forName(Driver);
con = DriverManager.getConnection(URL, USER, PASSWORD);
pstmt = con.prepareStatement(SQL);
// method 1 getClob
pstmt.setInt(1, 1);
ResultSet rs1 = pstmt.executeQuery();
rs1.next();
Clob clob = rs1.getClob(1);
System.out.println(readString(clob));
rs1.close();
// method 2 getCharacterStream
pstmt.setInt(1, 2);
ResultSet rs2 = pstmt.executeQuery();
rs2.next();
Reader reader = rs2.getCharacterStream(1);
System.out.println(readString(reader));
reader.close();
rs2.close();
// method 3 getString
pstmt.setInt(1, 3);
ResultSet rs3 = pstmt.executeQuery();
rs3.next();
System.out.println(rs3.getString(1));
rs3.close();
}catch(ClassNotFoundException ce){
ce.printStackTrace();
}catch(SQLException se){
se.printStackTrace();
}catch(IOException ie){
ie.printStackTrace();
}finally{
if(pstmt != null){
try{
pstmt.close();
}catch(SQLException se){
se.printStackTrace();
}
}
if(con != null){
try{
con.close();
}catch(SQLException se){
se.printStackTrace();
}
}
}
}
public static String readString(Clob clob) throws IOException, SQLException {
StringBuilder sb = new StringBuilder();
BufferedReader br = new BufferedReader(clob.getCharacterStream());
String str;
while((str = br.readLine()) != null) {
sb.append(str);
sb.append("\n");
}
br.close();
return sb.toString();
}
public static String readString(Reader reader) throws IOException {
StringBuilder sb = new StringBuilder();
BufferedReader br = new BufferedReader(reader);
String str;
while((str = br.readLine()) != null) {
sb.append(str);
sb.append("\n");
}
br.close();
return sb.toString();
}
```
---
## 交易 ( Transaction )
### 交易要求原則
#### ACID
* 交易的四個基本要求是<font color = red>原子性</font> ( <font color = red>Atomicity</font> )、<font color = red>一致性</font> ( <font color = red>Consistency</font> )、<font color = red>隔離行為</font> ( <font color = red>Isolation behavior</font> )與<font color = red>持續性</font> ( <font color = red>Durability</font> ),也常簡稱為<font color = red>ACID</font>
* <font color = red>原子性</font> ( <font color = red>Atomicity</font> ):一個交易即為一個單位工作(Unit of work),裡面所有包含的步驟需全部執行成功,若有一個步驟失敗,則視此為交易失敗,須撤銷先前所有執行成功的動作,回到初始狀態
* <font color = red>一致性</font> ( <font color = red>Consistency</font> ):交易作用的資料集合在交易前後必須一致,若交易成功,則整個資料集合都必須是交易後狀態,若交易失敗,整個資料集合都必須為開始交易前的狀態
* <font color = red>隔離行為</font> ( <font color = red>Isolation behavior</font> ):多人使用環境下,每個使用者都能進行自己的交易,交易與交易之間彼此不相關,互不干擾
* <font color = red>持續性</font> ( <font color = red>Durability</font> ):交易一旦成功,所有變動都必須保存下來,即使系統出了問題,交易結果仍不會遺失或改變
### JDBC與Transaction
* 為維護資料庫的整合性,將一組SQL指令組合成一個交易,若所有敘述運作正常,則提交(commit)資料庫一次處理,若其中有錯誤發生,則回復(rollback)交易前的狀態
* 一個Connection物件<font color = red>預設是自動提交(auto-ccommit) 模式</font>,即以個別的交易為單位自動commit資料庫,完成資料庫更新
* Connection介面有關交易的三大方法
* <font color = red>setAutoCommit(boolean autoCommit):設定自動提交模式,參數false即為關閉自動提交模式,改為手動</font>
* <font color = red>commit():結束目前的交易並將所有暫存的資料永久變更至資料庫中</font>
* <font color = red>rollback():結束目前的交易並將所有暫存的資料遺棄</font>
* <font color = red>一般會在交易被中斷時呼叫,即發生SQLException時呼叫 (catch)</font>
```java=
private static final String DRIVER = "com.mysql.cj.jdbc.Driver";
private static final String URL =
"jdbc:mysql://localhost:3306/JDBCSample?serverTimezone=Asia/Taipei";
private static final String USER = "David";
private static final String PASSWORD = "123456";
public static void main(String[] args){
Connection con = null;
Statement stmt = null
try{
Class.forName(Driver);
con = DriverManager.getConnection(URL, USER, PASSWORD);
// 關閉自動 (設為手動)
con.setAutoCommit(false);
stmt = con.createStatement();
stmt.executeUpdate("INSERT INTO EMPLOYEE(EMPNO, ENAME, JOB, HIREDATE, SAL, COMM, DEPTNO)"
+ "VALUES(7015, 'DAVID', 'MANAGER', '2016-01-01', 2500, 0.0, 40)");
stmt.executeUpdate("INSERT INTO EMPLOYEE(EMPNO, ENAME, JOB, HIREDATE, SAL, COMM, DEPTNO)"
+ "VALUES(7016, 'JAMES', 'CLERK', '2016-02-02', 800, 0.0, 20)");
stmt.executeUpdate("INSERT INTO EMPLOYEE(EMPNO, ENAME, JOB, HIREDATE, SAL, COMM, DEPTNO)"
+ "VALUES(7017, 'VINCENT', 'SALESMAN', '2016-03-03', 600, 1000.0, 30)");
stmt.executeUpdate("INSERT INTO EMPLOYEE(EMPNO, ENAME, JOB, HIREDATE, SAL, COMM, DEPTNO)"
+ "VALUES(7018, 'RON', 'ANALYST', '2016-04-04', 3500, 0.0, 10)");
// 更新完成送出
con.commit();
}catch(ClassNotFoundException ce){
ce.printStackTrace();
}catch(SQLException se){
// 若發生例外則rollback
try{
con.rollback();
}catch(SQLException sqle){
sqle.printStackTrace();
}
se.printStackTrace();
}finally{
if(stmt != null){
try{
stmt.close();
}catch(SQLException se){
se.printStackTrace();
}
}
if(con != null){
try{
con.setAutoCommit(true); // 設定回自動
con.close();
}catch(SQLException se){
se.printStackTrace();
}
}
}
}
```
### 儲存點
#### 其他交易相關事項
* BatchUpdate若是不必每一筆SQL指令都要確認的話,也可以搭配交易管理進行操作,使用方式同 JDBC與Transaction
* 如果在交易管理時,僅想要撤回某個SQL執行點,可以利用設定儲存點 (Save Point) 的方式進行設計
* 上述交易相關功能,資料庫表格必須有支援交易為前提才能使用。
ex. 在MySQL裡建立為InnoDB類型的表格
* 交易進行時
1. <font color = red>同一個交易使用同一連線</font>
2. <font color = red>不同交易連線不可共用</font> (符合隔離行為)
```java=
private static final String DRIVER = "com.mysql.cj.jdbc.Driver";
private static final String URL =
"jdbc:musql://localhos:3306/JDBCSample?serverTimezone=Asia/Taipei";
private static final String USER = "David";
private static final String PASSWORD = "123456";
public static void main(Stringp[] args){
Connection con = null;
Statement stmt = null;
try{
Class.forName(Driver);
con = DriverManager.getConnection(URL, USER, PASSWORD);
// 關閉自動
con.setAutoCommit(false);
stmt = con.createStatement();
stmt.executeUpdate("INSERT INTO EMPLOYEE(EMPNO, ENAME, JOB, HIREDATE, SAL, COMM, DEPTNO)"
+ "VALUES(7015, 'DAVID', 'MANAGER', '2016-01-01', 2500, 0.0, 40)");
stmt.executeUpdate("INSERT INTO EMPLOYEE(EMPNO, ENAME, JOB, HIREDATE, SAL, COMM, DEPTNO)"
+ "VALUES(7016, 'JAMES', 'CLERK', '2016-02-02', 800, 0.0, 20)");
savePoint = con.setSavepoint(); // 設定儲存點
stmt.executeUpdate("INSERT INTO EMPLOYEE(EMPNO, ENAME, JOB, HIREDATE, SAL, COMM, DEPTNO)"
+ "VALUES(7017, 'VINCENT', 'SALESMAN', '2016-03-03', 600, 1000.0, 30)");
stmt.executeUpdate("INSERT INTO EMPLOYEE(EMPNO, ENAME, JOB, HIREDATE, SAL, COMM, DEPTNO)"
+ "VALUES(7018, 'RON', 'ANALYST', '2016-04-04', 3500, 0.0, 'xx')");
con.commit();
}catch(ClassNotFoundException ce){
ce.printStackTrace();
}catch(SQLException se){
try{
rollback();
}catch(SQLException sqle){
sqle.printStack.Trace();
}
}finally{
if(stmt != null){
try{
stmt.close();
}catch(SQLException se){
se.printStackTrace();
}
}
if(con != null){
try{
con.releaseSavepoint(savePoint); // 釋放savePoint
con.setAutoCommit(true); // 設定回自動
con.close();
}catch(SQLException se){
se.printStackTRACE();
}
}
}
}
```
### 隔離行為
* Connection物件可使用下列方法進行隔離行為等級的取得或設定
* int getTransactionIsolation():取得目前的Transaction-isolation-level值
* Connection介面定義了五個常數
* int TRANSACTION_NONE
* int TRANSACTION_READ_UNCOMMITTED
* int TRANSACTION_READ_COMMITTED
* int TRANSACTION_REPETABLE_READ
* int TRANSACTION_SERIALIZABLE
* void setTransactionIsolation(int level)
* 此方法不得於執行交易區間內呼叫
* 從setAutoCommit(false)到commit()之間
* <font color = red>不要亂用</font>
### 隔離行為與同步 ( 本段引用馨華筆記 )
<style>
table, tr, td {
text-align: center;
}
</style>
<table>
<tr>
<td>隔離行為條件/預防問題</td>
<td><a href="#產生的問題:Lost-Update(更新遺失)">Lost Update</a></td>
<td><a href="#產生的問題:Dirty-Read(髒讀)">Dirty Read</a></td>
<td><a href="#產生的問題:Unrepeatale-Read(無法重複讀取)">Unrepeatable Read</a></td>
<td><a href="#產生的問題:Phantom-Read(幻讀)">Phantom Read</a></td>
</tr>
<tr>
<td><a href="#TRANSACTION_NONE-沒有交易(隔離)功能支援">TRANSACTION_NONE</a></td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td><a href="#READ_UNCOMMITTED-讀取未提交資料">READ_UNCOMMITTED</a></td>
<td>V</td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td><a href="#READ_COMMITTED-讀取提交資料">READ_COMMITTED</a></td>
<td>V</td>
<td>V</td>
<td></td>
<td></td>
</tr>
<tr>
<td><a href="#REPEATABLE_READ(可重複讀取)">REPEATABLE_READ</a></td>
<td>V</td>
<td>V</td>
<td>V</td>
<td></td>
</tr>
<tr>
<td><a href="#SERIALIZABLE-序列交易">SERIALIZABLE</a></td>
<td>V</td>
<td>V</td>
<td>V</td>
<td>V</td>
</tr>
</table>
### TRANSACTION_NONE 沒有交易(隔離)功能支援
老師的表上寫的是**沒有交易功能支援**,但這邊一定要非常注意,並<font color="red">**不是**</font>真的沒有交易行為,**而是沒有交易隔離的行為**。
#### 產生的問題:Lost Update(更新遺失)
<table>
<tr>
<td colspan="2">ENAME = David</td>
</tr>
<tr>
<td>A:update ENAME = James</td>
<td></td>
</tr>
<tr>
<td></td>
<td>B:update ENAME = Vincent</td>
</tr>
<tr>
<td>A:commit</td>
<td></td>
</tr>
<tr>
<td></td>
<td>B:rollback</td>
</tr>
<tr>
<td colspan="2">ENAME = David</td>
</tr>
</table>
A交易的更新被B交易的更新洗掉,最後結果仍然為 David。
在沒有交易隔離的情況,每個交易的更新都會互相干擾,**會以「最後對資源的異動行為」為最後結果**。
### READ_UNCOMMITTED 讀取未提交資料
可解決 [Lost Update](#產生的問題:Lost-Update(更新遺失)) 的問題:當 A 交易取得此欄位的編輯權時,其他交易只能讀取(可讀取尚未**確認**[包含 commit、rollback]的資料)。
READ_UNCOMMITTED 雖然可以解決更新遺失的問題,卻也容易發生髒讀問題。
#### 產生的問題:Dirty Read(髒讀)
<table>
<tr>
<td colspan="2">ENAME = David</td>
</tr>
<tr>
<td>A:update ENAME = James</td>
<td></td>
</tr>
<tr>
<td></td>
<td>B:read ENAME(= James)</td>
</tr>
<tr>
<td>A:rollback</td>
<td></td>
</tr>
<tr>
<td colspan="2">ENAME = David</td>
</tr>
<tr>
<td></td>
<td>B:read ENAME<span style="background: yellow;">(= James)</span></td>
</tr>
</table>
假設 A 在其他交易讀取後,對此修改做了 rollback,其他交易的讀取卻還是停留在 A 交易確認前,此情況稱為髒讀。
因為擁有編輯權的交易 rollback,而導致其他交易讀到的資料並非最後確認的資料。
### READ_COMMITTED 讀取提交資料
可解決 [Dirty Read](#產生的問題:Dirty-Read(髒讀)) 問題:在交易期間,其他交易都得等到此交易結束後才能做讀取的動作。
仍然有新的問題。
#### 產生的問題:Unrepeatale Read(無法重複讀取)
<table>
<tr>
<td colspan="2">ENAME = David</td>
</tr>
<tr>
<td>A:read ENAME(= David)</td>
<td></td>
</tr>
<tr>
<td></td>
<td>B:update ENAME = James</td>
</tr>
<tr>
<td></td>
<td>B:commit</td>
</tr>
<tr>
<td colspan="2">ENAME = James</td>
</tr>
<tr>
<td>A:read ENAME<span style="background: yellow;">(= James)</span></td>
<td></td>
</tr>
</table>
A 交易首先執行,但因為只進行了讀取,並沒有取得編輯權,因此 B 交易可以進行編輯(取得編輯權)。
B 編輯並確認,姓名資料由 David 改為 James。
此時 A 再次讀取,會得到姓名為 James 的結果。
乍看之下並沒有問題,但其實這和 ACID 的 **Consistency(一致性)原則已相違背:同個交易的前後兩次讀取,資料並不一致**。
所以 Unrepeatale Read 也不單純只是字面上的「無法重複讀取」,而是指「資料禁不起重複讀取的考驗」,因為重複讀取會得到不一致的結果。
### REPEATABLE_READ 可重複讀取
解決 [Unrepeatable Read](#產生的問題:Unrepeatale-Read(無法重複讀取)) 的問題。
REPEATABLE READ 在該欄位被讀取時,就會先將編輯權鎖起來,不讓其他交易可以編輯此欄位,造成讀取中的交易讀到的資料前後不一致的狀況。
#### 產生的問題:Phantom Read(幻讀)
<table>
<tr>
<td colspan="2">TABLE 中共有 5 筆資料(COUNT(*) = 5)</td>
</tr>
<tr>
<td>A:select * from TABLE(COUNT(*) = 5)</td>
<td></td>
</tr>
<tr>
<td></td>
<td>B:insert into TABLE values (...)(COUNT(*) = 6)</td>
</tr>
<tr>
<td></td>
<td>B:commit</td>
</tr>
<tr>
<td colspan="2">TABLE 中共有 6 筆資料(COUNT(*) = 6)</td>
</tr>
<tr>
<td>A:select * from TABLE<span style="background: yellow;">(COUNT(*) = 6)</span></td>
<td></td>
</tr>
</table>
和無法重複讀取的狀況有點類似,但擴大到對整個表格的資料。
A 交易在第 2 次查詢(讀取)時,資料(整個表格的總筆數)已經前後不一致了,少讀幾筆、多讀幾筆的這種狀況就稱之為幻讀。
### SERIALIZABLE 序列交易
和 [REPEATABLE READ](#REPEATABLE_READ(可重複讀取)) 相似,差別在於 REPEATABLE READ 是對單欄位做鎖定,SERIALIZABLE 對整個表格做鎖定,因此可以解決 [Phantom Read](#產生的問題:Phantom-Read(幻讀)) 的問題。
SERIALIZABLE 是最安全、一致的,但相對而言效能也是最差。
---
## 自增主鍵值綁定
### 目的
* 關聯式資料庫裡對於表格之間關係,建議會是一對多或式多對一的關係維護
ex. 以商品跟訂單為例,在表格設計上,就會採取以下的作法:

### 多人環境與主鍵值取得問題
* 編號在設計上常見使用流水號,也就是SEQUEMCE或是AUTO INCREMENT,如ORD001、ORD002、ORD003...依此類推,但若是在多人環境,<font color = red>該如何拿到屬於該訂單的正確編號並新增至訂單明細表格?</font>
#### 單人使用正常

#### 多人取值導致結果不正確

### Statement 綁定主鍵值
* <font color = red>JDBC 3.0</font>規範裡,當新增資料時,允許<font color = red>資料庫自動產生的主鍵值綁訂</font>到Statement或PreparedStatement中 (也處理了同步議題)
* 使用方式 (Statement)
* int excuteUpdate(String sql, int autoGeneratedKeys)
* int excuteUpdate(String sql, int[] columnIndexes)
* int excuteUpdate(String sql, String[] columnNames)
* 使用方式 (PreparedStatement)
* PreparedStatement preparesStatement(String sql, int autoGeneratedKeys)
* PreparedStatement preparesStatement(String sql, int[] columnIndexes)
* PreparedStatement preparesStatement(String sql, String[] columnNames)
* 以Statement的ResultSet <font color = red>getGeneratedKeys()</font>方法可以取出綁定的資料庫自增主鍵值
#### Statement
```java=
private static final String DRIVER = "com.mysql.cj.jdbc.Driver";
private static final String URL =
"jdbc:mysql://localhost:3306/JDBCSample?serverTimezone=Asia/Taipei";
private static final String USER = "David";
private static final String PASSWORD = "123456";
private static final String SQL = "INSERT INTO EMPLOYEE2(ENAME, JOB, HIREDATE, SAL, COMM, DEPTNO)"
+ "VALUES('DAVID', 'MANAGER', '2016-01-01', 2500, 0.0, 40)";
public static void main(String[] args){
Connection con = null;
Statement stmt = null;
ResultSet rs = null;
try{
Class.forName(Driver);
con = DriverManager.getConnection(URL, USER, PASSWORD);
con.setAutoCommit(false);
stmt = con.createStatement();
String[] cols = { "EMPNO" }; // 或是 int[] cols = {1};
stmt.excuteUpdate(SQL, cols);
// (執行指令, 取得同一資料的EMPNO流水編號(綁定欄位資訊))
rs = stmt.getGeneratedKeys();
// 取得流水編號
if (rs.next()) {
String key = rs.getString(1); // 只支援欄位"索引值"取得自增主鍵值
System.out.println("自增主鍵值 = " + key + "(剛新增成功的員工編號)");
} else {
System.out.println("NO KEYS WERE GENERATED.");
}
con.commit();
}catch(ClassNotFoundException ce){
ce.printStackTrace();
}catch(SQLException se){
try{
con.rollback();
}catch(SQLException sqle){
sqle.printStackTrace();
}
se.printStackTrace();
}finally{
if(rs != null){
try{
rs.close();
}catch(SQLException se){
se.printStackTrace();
}
}
if(stmt != null){
try{
stmt.close();
}catch(SQLException se){
se.printStackTrace();
}
}
if(con != null){
try{
con.setAutoCommit(true);
con.close();
}catch(SQLException se){
se.printStackTrace();
}
}
}
}
```
#### PreparedStatement
```java=
private static final String DRIVER = "com.mysql.cj.jdbc.Driver";
private static final String URL =
"jdbc:mysql://localhost:3306/JDBCSample?serverTimezone=Asia/Taipei";
private static final String USER = "David";
private static final String PASSWORD = "123456";
private static final String SQL = "INSERT INTO EMPLOYEE2(ENAME, JOB, HIREDATE, SAL, COMM, DEPTNO)"
+ "VALUES(?, ?, ?, ?, ?, ?)";
public static void main(String[] args){
Connection con = null;
PreparedStatement pstmt = null;
try{
Class.forName(DRIVER);
con = DriverManager.getConnection(URL, USER, PASSWORD);
con.setAutoCommit(false);
String[] cols = { "EMPNO" }; // 或 int cols[] = {1};
pstmt = con.prepareStatement(SQL, cols); // (SQL指令, 綁定欄位資訊)
pstmt.setString(1, "DAVID");
pstmt.setString(2, "MANAGER");
pstmt.setDate(3, java.sql.Date.valueOf("2016-01-01"));
pstmt.setInt(4, 2500);
pstmt.setDouble(5, 0.0);
pstmt.setInt(6, 40);
pstmt.executeUpdate();
ResultSet rs = pstmt.getGeneratedKeys();
if (rs.next()) {
String key = rs.getString(1); // 只支援欄位"索引值"取得自增主鍵值
System.out.println("自增主鍵值 = " + key + "(剛新增成功的員工編號)");
} else {
System.out.println("NO KEYS WERE GENERATED.");
}
rs.close();
commit();
}catch(ClassNotFoundException ce){
ce.printStackTrace();
}catch(SQLException se){
try{
con.rollback();
}catch(SQLException sqle){
sqle.printStackTrace();
}
se.printStackTrace();
}finally{
if(pstmt != null){
try{
pstmt.close();
}catch(SQLException se){
se.printStackTrace();
}
}
if(con != null){
try{
con.serAutoCommit(true);
con.close();
}catch(SQLException se){
se.printStackTrace();
}
}
}
}
```
---
## ORM 設計模型
### 介紹
#### 關聯式資料庫對映
* 物件 -- 關聯式資料庫對映 (Object-Relational Mapping)
* 簡稱ORM或式O/R Mapping
* 是一種常見的軟體設計模型
* 一個資料庫表格對應一個Java類別,表格欄位對應到此類別的實體變數
* 此Java類別稱為Value Object(VO)或Data Transfer Object(DTO),用在client端與server端間傳遞資料
* 亦即應用程式的Domain Object對應到資料庫的business entity
* 本講義以Java Bean來實作business entity
* Java Bean (包裝資料,重複使用)
* <font color = red>對每一個private欄位分別設計一組getter/setter</font>
* <font color = red>此類別實作Serializable介面</font>
* <font color = red>此類別擁有一個不帶參數的建構子</font>
### Java型別與SQL型別
| Java型態 | SQL型態 |
| :------------------- | :---------------------------------- |
| boolean | BIT |
| byte | TINYINT |
| short | SMALLINT |
| int | INTEGER |
| long | BIGINT |
| float | FLOAT |
| double | DOUBLE |
| byte[] | BINARY, VARBINARY, LONGBINARY, BLOB |
| java.lang.String | CHAR, VARCHAR, LONGVARCHAR, CLOB |
| java.math.BigDecimal | DECIMAL, NUMBERIC |
| java.sql.Date | DATE |
| java.sql.Time | TIME |
| java.sql.Timestamp | TIMESTAMP |
---
## DAO 設計模型
### DAO 設計
* 針對資料庫表格存取會設計一個Data Access Object (DAO)類別來對應
* 此DAO類別封裝JDBC的資料庫存取程式碼的實作,應用程式若需要存取資料庫時,一律透過此DAO來處理
* 通常不同廠商提供的資料庫,除了標準的SQL指另外,有些存取會依資料庫的不同而有所不同
* <font color = red>設計目的是若更換資料庫時,只需更換DAO即可,而應用程式無須變更</font>
* <font color = red>針對每一個DAO類別設計一個DAO介面來對應 (降低相依性)</font>
* 此DAO介面負責定義方法,就是對該表格資料的各種操作
### 流程
* 建立類別並實作Serializable → 建立DAO介面 → 建立實作DAO介面的類別 → 使用類別方法
#### 建立Table類別
* 建立Table類別並實作Serializable介面
* 宣告private欄位的實體變數
* 擁有一個不帶參數的建構子
* 對每一個private欄位分別設計一組getter/setter
```java=
public class Dept implements Serializable {
// 實作 Serializable 介面
private Integer deptno; // 對應到SQL,所以用Integer
private String dname;
private String loc;
// 擁有一個不代參數的建構子
public Dept() {
super();
}
public Dept(Integer deptno, String dname, String loc) {
super();
this.deptno = deptno;
this.dname = dname;
this.loc = loc;
}
// 對每一個private欄位"分別"設計一組getter/setter
public Integer getDeptno() {
return deptno;
}
public void setDeptno(Integer deptno) {
this.deptno = deptno;
}
public String getDname() {
return dname;
}
public void setDname(String dname) {
this.dname = dname;
}
public String getLoc() {
return loc;
}
public void setLoc(String loc) {
this.loc = loc;
}
}
```
#### 建立DAO介面
```java=
public interface DeptDAO {
// Data Access Object
// 此介面定義對資料庫的相關存取抽象方法
void add(Dept dept);
void update(Dept dept);
void delete(Dept dept);
Dept getOneByDeptno(int deptno);
// 輸入參數deptno 回傳Dept資料
List<Dept> getAll();
// 回傳List的方法
// 可以再使用SQL排序方法 (order by)
}
```
#### 建立實作DAO介面的類別
```java=
public class DeptDAOImpl implements DeptDAO{
// step.1
public static final String DRIVER = "com.mysql.cj.jdbc.Driver";
public static final String URL = "jdbc:mysql://localhost:3306/jdbcsample?serverTimezone=Asia/Taipei";
public static final String USER = "David";
public static final String PASSWORD = "123456";
// step.3
public static final String INSERT_STMT = "INSERT INTO DEPARTMENT VALUES (?, ?, ?)";
public static final String UPDATE_STMT = "UPDATE DEPT SET DEPTNO = ?, DNAME = ?, LOC = ?";
public static final String DELETE_STMT = "DELETE FROM DEPT WHERE DEPTNO = ?";
public static final String GET_ONE_BY_DEPTNO = "SELECT * FROM DEPARTMENT WHERE DEPTNO = ?";
public static final String GET_ALL = "SELECT * FROM DEPARTMENT";
// step.2
static {
try {
Class.forName(DRIVER);
}catch(ClassNotFoundException ce) {
ce.printStackTrace();
}
}
@Override
public void add(Dept dept) {
Connection con =null;
PreparedStatement pstmt = null;
try {
con = DriverManager.getConnection(URL, USER, PASSWORD);
pstmt = con.prepareStatement(INSERT_STMT);
pstmt.setInt(1, dept.getDeptno());
pstmt.setString(2, dept.getDname());
pstmt.setString(3, dept.getLoc());
pstmt.executeUpdate(); // 送出
}catch (SQLException se) {
se.printStackTrace();
}finally {
if (pstmt != null) {
try {
pstmt.close();
} catch (SQLException se) {
se.printStackTrace(System.err);
}
}
if (con != null) {
try {
con.close();
} catch (Exception e) {
e.printStackTrace(System.err);
}
}
}
}
@Override
public void update(Dept dept) {
// TODO Auto-generated method stub
}
@Override
public void delete(Dept dept) {
// TODO Auto-generated method stub
}
@Override
public Dept getOneByDeptno(int deptno) {
// TODO Auto-generated method stub
Dept dept = null;
Connection con = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
try {
con = DriverManager.getConnection(URL, USER, PASSWORD);
pstmt = con.prepareStatement(GET_ONE_BY_DEPTNO);
pstmt.setInt(1, deptno);
rs = pstmt.executeQuery(); // 送出
while(rs.next()) {
dept = new Dept();
String dname = rs.getString("DNAME");
String loc = rs.getString("LOC");
// 使用Dept Bean包裝著查詢出來的資料,方便一次回傳的動作
dept.setDeptno(deptno);
dept.setDname(dname);
dept.setLoc(loc);
}
}catch (SQLException se) {
se.printStackTrace();
}finally {
if (rs != null) {
try {
rs.close();
} catch (SQLException se) {
se.printStackTrace(System.err);
}
}
if (pstmt != null) {
try {
pstmt.close();
} catch (SQLException se) {
se.printStackTrace(System.err);
}
}
if (con != null) {
try {
con.close();
} catch (Exception e) {
e.printStackTrace(System.err);
}
}
}
return dept;
}
@Override
public List<Dept> getAll() {
// TODO Auto-generated method stub
List<Dept> deptList = new ArrayList<>();
Dept dept = null;
Connection con = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
try {
con = DriverManager.getConnection(URL, USER, PASSWORD);
pstmt = con.prepareStatement(GET_ALL);
rs = pstmt.executeQuery(); // 送出
while(rs.next()) {
dept = new Dept();
int deptno = rs.getInt("DEPTNO");
String dname = rs.getString("DNAME");
String loc = rs.getString("LOC");
// 使用Dept bean包裝著查詢出來的資料,方便一次回傳的動作
dept.setDeptno(deptno);
dept.setDname(dname);
dept.setLoc(loc);
// 可簡化為
dept.setDeptno(rs.getInt("DEPTNO"));
dept.setDname(rs.getString("DNAME"));
dept.setLoc(rs.getString("LOC"));
// 用集合收集各個包裝好的Dept Bean,最後回傳此集合
deptList.add(dept);
}
}catch (SQLException se) {
se.printStackTrace();
}finally {
if (rs != null) {
try {
rs.close();
} catch (SQLException se) {
se.printStackTrace(System.err);
}
}
if (pstmt != null) {
try {
pstmt.close();
} catch (SQLException se) {
se.printStackTrace(System.err);
}
}
if (con != null) {
try {
con.close();
} catch (Exception e) {
e.printStackTrace(System.err);
}
}
}
return deptList;
}
}
```
#### 使用類別方法
```java=
public class DAOTest {
public static void main(String[] args) {
// 使用輸入新增資料
Scanner sc = new Scanner(System.in);
System.out.println("輸入要新增的部門編號");
int deptno = sc.nextInt();
System.out.println("輸入要新增的部門名稱");
String dname = sc.next();
System.out.println("輸入要新增的部門所在地");
String loc = sc.next();
// 包裝資料,準備轉交給DAO物件完成新增
Dept dept = new Dept(deptno, dname, loc);
// 產生DAO物件,準備資料庫進行操作
DeptDAO dao = new DeptDAOImpl();
// 將dept資料丟給dao.add方法執行
dao.add(dept);
DeptDAO dao = new DeptDAOImpl();
Dept dept = dao.getOneByDeptno(deptno);
System.out.println("DEPTNO:" + dept.getDeptno());
System.out.println("DNAME:" + dept.getDname());
System.out.println("LOC:" + dept.getLoc());
DeptDAO dao = new DeptDAOImpl();
List<Dept> deptList = dao.getAll();
for (Dept dept : deptList) {
System.out.println("DEPTNO:" + dept.getDeptno());
System.out.println("DNAME:" + dept.getDname());
System.out.println("LOC:" + dept.getLoc());
System.out.println("======================");
}
}
}
```
---
## 連線管理
### 連線管理議題
* 對於一個Web應用系統來說,連線有可能產生以下問題
* 資料庫連結過於頻繁,造成整個系統執行效率低落
* 占用連線後,卻又很少執行程式造成浪費 (ex. servlet init()方法使用)
* 使用一個連線後,執行程式頻率極高,可能造成多個執行緒同時共用一個連線的不正常情況 (資料異動時不能共用同一個連線)
* <font color = red>連線池 ( Connection Pool )</font> -- (重複使用)
* 是一種對Conection物件的管理機制,為解決上述問題的方案之一
* 系統啟動初期,Connection Pool就會預先建立幾個連線物件,並開始執掌分配連線物件的工作
### 連線池套件
* Connection Pool實作
* 可由Application Server提供 (ex. Tomcat, WebSphere, WebLogic)
* 驅動程式內也可以處理Connection Pool → <font color = red>相依性較高,不建議使用</font>
* 或是處理Connection Pool的商業產品套件
* 免費的連線池:Apache DBCP, C3P0, Proxool等
* 連線管理程式
* 範例:<font color = red>ConnPool.java + SQLAgent.java + TestThread.java</font>
### 連線池管理
* JDBC連線池模型

---
## 連線池 ( Connection Pool )
### DataSource介面
* 使用<font color = red>javax.sql.DataSource</font> (資料來源介面)
* JDBC 2.0標準延伸API (Standard Extension API)裡引入的DataSource介面是目前連上資料庫的DriverManager類別的最佳替代方案
```java=
public interface DataSource{
Connection getConnection throws SQLException;
// 此方法會由連線池套件實作
}
```
* <font color = red>一個DataSource物件就代表一個資料庫,應用程式可以間接透過這個物件以取得資料庫Connection</font>
* 要配置DataSource物件來產生連線池,需先有ConnectionPoolDataSource介面的物件
* 於JDBC 2.0提出加入,JDBC 3.0正式使用
### JNDI簡介
* 實際上對於DataSource的取得,我們的應用程式更常藉由JNDI (Java Naming and Directory Interface),從Servlet Container取得已設定好的DataSource,再從DataSource取得資料庫連線物件 ( 在SE沒辦法跑 )
ex. ( 課本內容 )
```java=
javax.naming.Context ctx = new javax.naming.InitialContext();
DataSource ds = (DataSource)ctx.lookup("jdbc/xxx");
Connection con = ds.getConnection();
```
* Java程式內無需再註冊驅動程式,也不必透過DriverManager提供URL, USER, PASSWORD,JDBC已經發現了命名與目錄服務
### 連線池優勢
* <font color = red>Java應用程式只要向DataSource交談即可</font>,DataSource會自行和ConnectionPoolDataSource交談
* ConnectionPoolDataSource儲存的就是放在連線池裡的資料庫連線。當應用程式呼叫DataSource實體的getConnection()方法時,<font color = red>getConnection()方法即會從連線池裡取出一個連線物件</font>,應用程式就能利用此物件一直到工作完成
* 工作完成之後,應用程式正常關閉連線,但應用程式並不會知道實際上此連線與資料庫沒有中斷。<font color = red>close()方法將連線歸還到連線池裡</font>,若是要再使用連線,需要再次取出
* 採用連線池的好處:
* <font color = red>開發快速、好維護、系統效能提升與解決交易連線共用問題</font>
* 補充:搭配連線池機制,<font color = red>程式碼一樣還是得對連線做close()的呼叫</font>,不過這close()已經被連線池套件override為歸還連線的行為了
---
## JDBC 各版本比較
* JDBC 1.0 vs JDBC 2.0
* ResultSet可以指定游標筆數
* 可以使用Java指令更新資料,替代SQL指令,也可一次下達多個SQL指令
* 支援SQL3的資料型態
* JDBC 3.0 (J2SE 1.4/1.5)
* 當新增資料時,允許將資料庫自動產生的<font color =red>主鍵值</font>進行綁定到Statement或是PreparedStatement
* 與各種Java應用程式結合,讓開發速度與應用程式效能都有所提升
* 規範與規定了下述類別與介面支援資料庫的連線池實作
* javax.sql.ConnectionEvent
* javax.sql.ConnectionPoolDataSource
* javax.sql.PooledConnection
* java.sql.ConnectionEventListener
* JDBC 4.0 (J2SE 6)
1. <font color = red>簡化資料庫驅動程式載入</font>
JDBC 4.0後,可以不用再呼叫Class.forName(DRIVER)方法載入驅動,但在包裝JDBC驅動程式的JAR檔裡必須有一個"META-INF/services/java.sql.Driver"的檔案,並在該檔案裡註明驅動程式的類別名稱
2. <font color = red>SQLException功能增強</font>
SQLException新增了幾個以Throwable為建構子參數的建構子,就可以將任何其他類型的例外也包裝成SQLException再做後續處理。SQLException也同時實作了Iterable`<Throwable>`介面,可以搭配for-each取出
3. <font color = red>BLOB與CLOB的支援</font>
PreparedStatement新增了setBlob()與setClob()方法,使用InputStream將位元資料送入資料庫,使用Reader將文字資料送入資料庫