資料庫廠商依據Java提供的介面進行實作,開發者則依照JDBC的標準進行操作
如此設計的因素是每家廠商提供用來操作的 API 並不相同
開發者透過JDBC的介面操作的好處是,若有更換資料庫的要求,可以避免大量修改程式碼
只需要替換實作廠商的資料庫驅動
也就是寫一個Java程式就能應付所有的資料庫
當然實作時,有時為了使用資料庫的特定功能,仍有修改程式碼的必要
廠商實作JDBC驅動程式的方式有四種類型
1.JDBC-ODBC Bridge Driver
ODBC(Open DataBase Connectivity)是微軟所主導的資料庫連結標準
所以也很常在 Microsoft 的系統上使用
這類的驅動程式將JDBC的呼叫轉換成對ODBC的呼叫
但因為兩者並非一對一的對應,所以在功能及存取速度上都有所限制
2.Native API Driver
載入由廠商提供的 C\C++撰寫成的原生函式庫
直接將JDBC的呼叫轉成資料庫中的相關 API 呼叫
因為直接呼叫 API 的關係,操作速度比第一種快(但比不上後兩種)
但因為驅動程式作用的對象只限制在原生資料庫
沒有做到JDBC的跨資料庫的目標
3.JDBC-Net Driver
這類型的驅動程式提供了用戶端網路API,JDBC驅動透過Socket呼叫伺服器上的中介元件
由該元件將JDBC的呼叫轉換成 API 呼叫
客戶端的驅動程式與中介元件間的協定是固定的,所以不需要載入廠商的函式庫
也因此在更換資料庫時,只需修改中介元件即可
此種方式也是相當有彈性的架構
4.Native Protocol Driver
這類的驅動程式使用Socket,直接在用戶端和資料庫間通訊
也因為這是最直接的實作方式,通常具有最快的存取速度
但針對不同的資料庫需使用不同的驅動程式
為不需要JDBC-Net Driver那般彈性時的選擇,也是最常見的驅動程式類型
接下來的操作範例以第四種為主,並以MySQL為操作範例
MySQL提供的JDBC驅動程式是 mysql-connector-java ,可在其官網中找到
首先來個範例:
SQLController.java=======
package DB;
import java.sql.*;
public class SQLController {
private Connection con = null; //Database objects
private Statement stat = null; //SQL code to execute
private ResultSet res = null; //result
private PreparedStatement pst = null;
public SQLController(){
}
public SQLController(String SQLType, String SQLDriver, String daatabase, String account, String password){
/* 範例裡傳入的參數
SQLType = "mysql"
SQLDriver = "com.mysql.jdbc.Driver"
其餘的則按資料庫的設定傳入*/
String conInfo = "jdbc:" + SQLType + "://localhost:3306/" + daatabase + "?useUnicode=true&characterEncoding=UTF-8";
try {
//register driver
Class.forName(SQLDriver);
con = DriverManager.getConnection(conInfo, account, password);
}
catch(ClassNotFoundException e){
System.out.println("DriverClassNotFound :"+e.toString());
}
//prevent SQL code error
catch(SQLException x) {
System.out.println("Exception :"+x.toString());
}
}
//clear objects in order to close database
private void Close()
{
try{
if(res!=null){ res.close();}
if(stat!=null){ stat.close();}
if(pst!=null){ pst.close(); }
if(con!=null){ con.close(); }
}
catch(SQLException e){
System.out.println("Close Exception :" + e.toString());
}
}
}
網路上可以找到寫成JavaBean格式的寫法,搭配EL也能正常地使用資料庫
範例將資料庫連線寫成Java物件,寫法依個人喜好而定
連結資料庫前必須先載入JDBC驅動程式
透過Class.forName(),範例程式動態載入 com.mysql.jdbc.Driver 類別至 DriverManager
類別會自動向 DriverManager 做註冊
生成連線時,DriverManager就會使用該驅動建立Connection實例
JDBC URL定義連線資料庫的協定、子協定、資料來源識別
型態是 協定:子協定:識別資料來源
使用MySQL的JDBC URL就會是 jdbc:mysql://localhost:3306/資料庫名稱?參數=值&...
識別資料裡的 localhost:3306 連結 MySQL的連結阜
使用中文時需附加參數 useUnicode=true&characterEncoding=UTF8,指定用UTF8編碼
建立Connection實例時我們必須提供JDBC URL
DriverManager.getConnection() method有兩種引入參數的版本
單一 參數的版本必須將資料庫的帳密資料附加在JDBC URL裡,再引入URL
另一個版本就是本例使用的 DriverManager.getConnection(conInfo, account, password);
將帳號、密碼當作呼叫時的參數傳入
SQLException是資料庫處理過程發生異常時會被丟出的例外
資料庫的使用過程中都必須做這個例外處理的準備
Connection是連接資料庫的代表物件
執行SQL還需要取得 java.sql.Statement 物件
下面的範例示範實際執行SQL碼的撰寫方式,並搭配DCBP與JNDI來進行:
DatabaseBean.java======
package tw.vencs;
import java.sql.*;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.sql.DataSource;
public class DatabaseBean {
private DataSource dataSource = null;
public DatabaseBean(){
}
public void setConSource(DataSource dataSource){
this.dataSource = dataSource;
}
public Connection getConnection() throws SQLException{
return dataSource.getConnection();
}
}
class SignInBean extends DatabaseBean{
public boolean isSignIn(String account, String password){
Connection con = null;
Statement sta = null;
ResultSet res = null;
boolean checkOK = false;
try{
con = getConnection();
sta = con.createStatement();
res = sta.executeQuery("SELECT * FROM `member` WHERE account = '"+ account
+"' AND password = '"+ password +"' ");
//如果有找到與輸入的帳密相同的資料,允許會員登入
while(res.next()){
checkOK = true;
}
}catch(SQLException ex){
Logger.getLogger(SignInBean.class.getName()).log(Level.SEVERE, null, ex);
throw new RuntimeException();
}finally{
try{
if(res != null){ res.close();}
if(sta != null){ sta.close();}
if(con != null){ con.close();}
}catch(SQLException ex){
Logger.getLogger(SignInBean.class.getName()).log(Level.SEVERE, null, ex);
throw new RuntimeException();
}
}
return checkOK;
}
}
SignInConfirmer.java======
package tw.vencs;
import java.io.IOException;
import javax.servlet.http.*;
import javax.sql.DataSource;
import com.colid.DatabaseBean;
public class SignInConfirmer extends HttpServlet{
public void doPost(HttpServletRequest request, HttpServletResponse response)throws IOException{
String account = request.getParameter("account");
String password = request.getParameter("password");
String cookieMaker = request.getParameter("cookieMaker");
//get DBCP connection resource from InitialListener
DataSource dataSource = (DataSource)getServletContext().getAttribute("conData");
SignInBean confirmer = new SignInBean();
confirmer.setConSource(dataSource);
if(confirmer.isSignIn(account, password)){
HttpSession session = request.getSession();
session.setAttribute("account", account);
if(cookieMaker.equals("yes")){
int life = 30*24*60*60; //set Life of Cookie to 30 days
Cookie cookie = new Cookie("account", account);
cookie.setMaxAge(life);
response.addCookie(cookie);
}
//encode URL if user's COOKIE function is closed
response.sendRedirect(response.encodeURL("index.jsp"));
}
else{
getServletContext().setAttribute("warning", true);
response.sendRedirect(response.encodeURL("login.jsp"));
}
}
}
範例是一隻處理登入狀態(會話管理)的 Servlet 程式
DBCP 與 JNDI 的部分可以去找那篇的筆記來看
執行SQL碼的 Statement 物件從連線物件中取得,程式碼如: con.createStatement()
Statement 物件常用的方法有 executeUpdate()、executeQuery()和execute()
executeUpdate()主要用來執行 CREATE、INSERT、 DROP等改變資料庫內容的SQL碼
執行完畢後回傳 int 數值,表示資料變動的筆數
executeQuery() 則如字面所示,用來得到 SELECT等SQL碼查詢到的結果
執行完畢後回傳 java.sql.ResultSet 物件
ResultSet 的next() method 會回傳 boolean值來表示是否有下一筆資料
如果確實有資料的話可以使用 getXXX()的方式取得該筆資料,XXX為資料型別
查詢結果會從1開始作為欄位的索引值
例如: int id = res.getINT(1)
execute() 這個method則用在無法事先得知要執行的SQL碼的場合
假使回傳的結果是 true,則可以用getResultSet() 取得 ResultSet 物件
回傳 false 則可以用 getUpdateCount() 得知更新資料筆數
最後提到上面的範例沒有展示到的 PreparedStatement
假使有動態資料的情況下
像上述範例以 + 運算子結合字串組成SQL碼會比較麻煩
字串每使用一次 + ,其實也是重新製作一個String物件
如此的字串使用也造成效能上的負擔
PreparedStatement 的使用與 Statement 很相似,如改寫範例後為:
PreparedStatement pre = con.prepareStatement("SELECT * FROM `member` WHERE
account =? AND password =?");
pre.setString(1, account); //setString() 輸入的參數可以避免 Injection Attack
pre.setString(2, password);
pre.executeUpdate();
pre.clearParameters();
將SQL碼中要動態輸入的部分以 ? 運算子代替,然後設置所要輸入的變數
con.prepareStatement() 會建立一個預先編譯的SQL實例
它也同樣可以使用executeUpdate()、executeQuery() method
當查詢完畢之後,clearParameters() 清除所輸入的參數
留下來的實例卻可以填上別的參數繼續使用,而不用重新再建立SQL實例
會被頻繁使用的SQL碼實例適合用這種方式建立
setString() 輸入的參數會被視為純粹的String物件,最後在SQL碼內的形式是包覆在引號內
若是需要填入SQL碼內的是數字,則可以用 setInt() 以免出現SQL碼錯誤的情形
PreparedStatement 會是效能與安全性考量的好選擇
沒有留言:
張貼留言