2010年9月26日 星期日

JDBC 簡介與基礎使用

JDBC(Java Database Connectivity)是Java規範資料庫存取的API
資料庫廠商依據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 會是效能與安全性考量的好選擇

沒有留言:

張貼留言