day16 事務 - 數據庫鏈接池 - 編寫本身的jdbc框架

Author:相忠良
Email: ugoood@163.com
起始於:June 8, 2018
最後更新日期:June 11, 2018java

聲明:本筆記依據傳智播客方立勳老師 Java Web 的授課視頻內容記錄而成,中間加入了本身的理解。本筆記目的是強化本身學習所用。如有疏漏或不當之處,請在評論區指出。謝謝。
涉及的圖片,文檔寫完後,一次性更新。mysql

1. 事務

1

發sql時,把多個sql放在Start transactioncommit之間便可。spring

試驗準備:sql

create table account(
    id int primary key auto_increment,
    name varchar(40),
    money float
)character set utf8 collate utf8_general_ci;

insert into account(name,money) values('aaa',1000);
insert into account(name,money) values('bbb',1000);
insert into account(name,money) values('ccc',1000);

如今,a向b轉帳100元,操做以下:數據庫

start transaction;
update account set money=money-100 where name='aaa';

關掉鏈接,從新登陸數據庫查看,aaa 帳戶的 money 仍是 1000。
只有下面這樣才行:apache

start transaction;
update account set money=money-100 where name='aaa';
update account set money=money+100 where name='bbb';
commit;

執行到 commit,上面2條sql纔算真正執行,而不是回滾,這就是事務(控制多條sql做爲總體執行)。編程

rollback 能夠手動回滾,而不是異常時,事務在數據庫中自動回滾。設計模式

當Jdbc程序向數據庫得到一個Connection對象時,默認狀況下這個Connection對象會自動向數據庫提交在它上面發送的SQL語句。若想關閉這種默認提交方式,讓多條SQL在一個事務中執行,可以使用下列語句:
JDBC控制事務語句:服務器

  • Connection.setAutoCommit(false); 至關於 start transaction
  • Connection.rollback(); rollback
  • Connection.commit(); commit

程序中控制事務的例子以下:併發

public class Demo1 {
    /**  
     a--->b 100
     */
    public static void main(String[] args) throws SQLException {

        Connection conn = null;
        PreparedStatement st = null;
        ResultSet rs = null;
        try{
            conn = JdbcUtils.getConnection();
            conn.setAutoCommit(false);    //start transaction;

            String sql1 = "update account set money=money-100 where name='aaa'";
            String sql2 = "update account set money=money+100 where name='bbb'";

            st = conn.prepareStatement(sql1);
            st.executeUpdate();

            int x = 1/0; // <-- 產生異常

            st = conn.prepareStatement(sql2);
            st.executeUpdate();

            conn.commit(); // commit
        }finally{
            JdbcUtils.release(conn, st, rs);
        }
    }
}

1.1 事務回滾點

手動回滾,按下面例子,只想從第二條sql開始回滾,方法就是:

  1. 設置回滾點Savepoint
  2. 手動設置 commit。

例子以下:

public class Demo2 {
    public static void main(String[] args) throws SQLException {
        Connection conn = null;
        PreparedStatement st = null;
        ResultSet rs = null;
        Savepoint sp = null; // 回滾點對象
        try {
            conn = JdbcUtils.getConnection();
            conn.setAutoCommit(false); // start transaction;

            String sql1 = "update account set money=money-100 where name='aaa'";
            String sql2 = "update account set money=money+100 where name='bbb'";
            String sql3 = "update account set money=money+100 where name='ccc'";

            st = conn.prepareStatement(sql1);
            st.executeUpdate();

            sp = conn.setSavepoint(); // <-- 2. 設置回滾點

            st = conn.prepareStatement(sql2);
            st.executeUpdate();

            int x = 1 / 0;      // <-- 1. 產生異常

            st = conn.prepareStatement(sql3);
            st.executeUpdate();

            conn.commit(); // commit
        } catch (Exception e) {
            e.printStackTrace();
            conn.rollback(sp);  // <-- 3. 回滾
            conn.commit();      // <-- 4. 手動回滾後,必定要記得提交事務
        } finally {
            JdbcUtils.release(conn, st, rs);
        }
    }
}

1.2 事務四大特性 ACID

若一個數據庫號稱支持事務,那它必然支持 ACID;反過來講,若某數據庫支持 ACID,那這個數據庫也是支持事務的。

  • 原子性(Atomicity) 原子性是指事務是一個不可分割的工做單位,事務中的操做要麼都發生,要麼都不發生;
  • 一致性(Consistency) 事務先後數據的完整性必須保持一致;
  • 隔離性(Isolation) 事務的隔離性是多個用戶 併發訪問 數據庫時,數據庫爲每個用戶開啓的事務,不能被其餘事務的操做數據所幹擾,多個併發事務之間要相互隔離;
  • 持久性(Durability) 持久性是指一個事務一旦被提交,它對數據庫中數據的改變就是永久性的,接下來即便數據庫發生故障也不該該對其有任何影響。

1.2.1 隔離性 - 髒讀 - 不可重複讀 - 虛讀(幻讀)

髒讀:指一個事務讀取了另一個事務未提交的數據。(最危險)

故事:這是很是危險的,假設 A 向 B 轉賬 100 元,對應 sql 語句以下所示:

  1. update account set money=money+100 while name='b';
  2. update account set money=money-100 while name='a';

當第 1 條 sql 執行完,第 2 條還沒執行(A 未提交時),若是此時 B 查詢本身的賬戶,就會發現本身多了 100 元錢。若是 A 等 B 走後再回滾,B 就會損失 100 元。

下面介紹的不可重複讀和幻讀,有些狀況下是沒問題的,但有時會有問題。

不可重複讀:在一個事務內讀取表中的某一行數據,屢次讀取結果不一樣。 也指讀表中同一條數據,結果不一樣。

故事:中國人民銀行生成開啓生成報表這個事務,報送克強總理1000億RMB,在報送近平主席前,生成報表這個事務未結束期間,有客戶存了200億RMB並該客戶完成了他的事務,如今又生成近平主席的報表顯示爲1200億。問題出現了:兩位領導要打架的。困惑就是:哪次查詢時是準確的呢? 這就是不可重複讀所產生的問題。

虛讀(幻讀):是指在一個事務內讀取到了別的事務插入的數據,致使先後讀取不一致。 也指所讀的表的記錄數在變化。

故事:人口普查系統正生成報表,開啓了一個事務。該系統在這個事務中需生成多個報表。可能發生這樣的事:生成第一個報表,顯示中國有10億人,但生成第二個報表期間,有人往數據庫中插入了數據,統計結果顯示有11億人。困惑來了:到底以哪一個爲準呢?這就是幻讀產生的問題。

1.3 事務的隔離級別

根據上節介紹的,若無隔離性,數據庫可能出現的三種問題,針對問題的解決,提出了事務隔離級別。隔離級別的提出,主要在解決問題的基礎上,儘量的不過多損失數據庫性能。

數據庫共定義了四種隔離級別:

  • Serializable:可避免髒讀、不可重複讀、虛讀狀況的發生。(串行化)
  • Repeatable read:可避免髒讀、不可重複讀狀況的發生。(可重複讀)
  • Read committed:可避免髒讀狀況發生(讀已提交)。
  • Read uncommitted:最低級別,以上狀況均沒法保證。(讀未提交)

事務隔離性的設置語句:

  • set transaction isolation level 設置事務隔離級別
  • select @@tx_isolation 查詢當前事務隔離級別

方立勳老師開啓了2個mysql客戶端,進行了模擬。模擬過程這裏不表述了。

編程序時,得到的 connection:

  • 如果 mysql 的連接,默認隔離級別是repeatable read,mysql徹底支持上述4種隔離級別;
  • 如果 oracle 的連接,默認隔離級別是 read committed,且oracle只支持Serializable和Read committed這兩種隔離級別。

編程中,用JDBC設置隔離級別: conn.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE);

示例代碼:

public class Demo3 {
    public static void main(String[] args) throws SQLException, InterruptedException {
        Connection conn = null;
        PreparedStatement st = null;
        ResultSet rs = null;
        Savepoint sp = null;
        try{
            conn = JdbcUtils.getConnection();   //mysql repeatable read
            conn.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE);
            conn.setAutoCommit(false);    //start transaction;

            String sql = "select * from account";
            conn.prepareStatement(sql).executeQuery();

            Thread.sleep(1000*20);

            conn.commit();
        }finally{
            JdbcUtils_DBCP.release(conn, st, rs);
        }
    }
}

2. 數據庫鏈接池

下圖展現了無數據庫鏈接池時的缺點:
2

下圖是有鏈接池的情形:
3

有鏈接池後,數據庫就沒必要爲每一個用戶建立鏈接,而僅僅在一開始生成一些鏈接(假如20個),並將這些鏈接放入鏈接池,其餘用戶只從池中拿鏈接,用完後還到池中。(這個故事主要考慮,數據庫本身建立1個鏈接需消耗不少資源,10萬用戶申請,就建立10萬次鏈接,數據庫自己作本職工做就很繁忙,再去頻繁地建立若此多的連接,數據庫極有可能被累死!咱們要作的是儘可能減輕數據庫服務器的負擔。)


故事:
咱們但願執行conn.close();時,鏈接還回鏈接池,但事實是conn是mysql提供的連接,執行close方法時,那個鏈接將還給mysql,而不是鏈接池。
當發現對象的方法不夠咱們用時,咱們需加強那個方法。辦法有:

  1. 寫一個Connection子類,覆蓋close方法,加強close方法;
  2. 用包裝設計模式;
  3. 用動態代理。

一般子類的方式不可行,緣由是很難將父類對象信息導入子類對象中,除非父類對象封裝的信息極少。

包裝設計模式步驟(我本身的經驗,想象一下BufferedReader的用法,就是用構造函數接收被包裝對象):

  1. 定義一個類,實現與被加強對象相同的接口;
  2. 在類中定義一個變量,記住被加強對象;
  3. 定義一個構造函數,接收被加強對象;
  4. 覆蓋想加強的方法;
  5. 對於不想加強的方法,直接調用目標對象(被加強對象)的方法。

包裝模式例子:

class MyConnection implements Connection{ // step 1
  private Connection conn; // step 2
  public MyConnection(Connection conn){ // step 3
    this.conn = conn;
  }
  public void close(){ // step 4
    list.add(this.conn);
  }

  // step 5
  @Override
  public void commit() throws SQLException{
    this.conn.commit(); // 調用的是 mysql 提供的 commit 方法
  }

  @Override
  public void clearWarnings() throws SQLException{
    this.conn.clearWarnings(); // 調用的是 mysql 提供的 clearWarnings 方法
  }

  /*
    ...
    ...
    後面不想加強的方法均照 step 5 處理,極有可能代碼量超大,這也是包裝模式處理此類問題的缺點
  */
}

使用經包裝(裝飾)後的conn對象:

MyConnection my = new MyConnection(conn);

當咱們用my這個連接對象時,它的close方法就是咱們本身寫的方法了。

下面代碼時動態代理方式(這裏僅作個記錄):

proxyConn = (Connection) Proxy.newProxyInstance(this.getClass()
    .getClassLoader(), conn.getClass().getInterfaces(),
    new InvocationHandler() {
      // 此處爲內部類,當close方法被調用時將conn還回池中,其它方法直接執行
      public Object invoke(Object proxy, Method method,
          Object[] args) throws Throwable {
        if (method.getName().equals("close")) {
          pool.addLast(conn);
          return null;
        }
        return method.invoke(conn, args);
      }
    });

3. 經常使用開源數據庫鏈接池(DataSource 接口的開源實現)

數據源 = 數據庫鏈接池

常見開源數據庫鏈接池有:

  • DBCP 數據庫鏈接池
  • C3P0 數據庫鏈接池
  • Apache Tomcat 內置的鏈接池(用的是 apache DBCP)

3.1 Apache DBCP 數據源

若想用 Apache DBCP,應用程序應增長以下 2 個 jar 文件:

  • Commons-dbcp.jar:鏈接池的實現
  • Commons-pool.jar:鏈接池實現的依賴庫

下面是 dbcp-1.2.2 開發包中的 dbcpconfig.properties文件(實驗時,需將該文件 copy 到 src 目錄下),其做用同之前咱們本身寫的 db.properties 同樣,是存放配置 dbcp 鏈接哪一種數據庫、url、用戶名、密碼等信息的一種配置文件。以下:

#鏈接設置
driverClassName=com.mysql.jdbc.Driver
url=jdbc:mysql://localhost:3306/jdbc
username=root
password=

#<!-- 初始化鏈接 -->
initialSize=10

#最大鏈接數量
maxActive=50

#<!-- 最大空閒鏈接 -->
maxIdle=20

#<!-- 最小空閒鏈接 -->
minIdle=5

#<!-- 超時等待時間以毫秒爲單位 6000毫秒/1000等於60秒 -->
maxWait=60000


#JDBC驅動創建鏈接時附帶的鏈接屬性屬性的格式必須爲這樣:[屬性名=property;]
#注意:"user" 與 "password" 兩個屬性會被明確地傳遞,所以這裏不須要包含他們。
connectionProperties=useUnicode=true;characterEncoding=utf8

#指定由鏈接池所建立的鏈接的自動提交(auto-commit)狀態。
defaultAutoCommit=true

#driver default 指定由鏈接池所建立的鏈接的只讀(read-only)狀態。
#若是沒有設置該值,則「setReadOnly」方法將不被調用。(某些驅動並不支持只讀模式,如:Informix)
defaultReadOnly=

#driver default 指定由鏈接池所建立的鏈接的事務級別(TransactionIsolation)。
#可用值爲下列之一:(詳情可見javadoc。)NONE,READ_UNCOMMITTED, READ_COMMITTED, REPEATABLE_READ, SERIALIZABLE
defaultTransactionIsolation=READ_COMMITTED

從新設置 JdbcUtils.java,用鏈接池的方式:

package cn.wk.utils;

import java.io.InputStream;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Properties;

import javax.sql.DataSource;

import org.apache.commons.dbcp.BasicDataSourceFactory;

public class JdbcUtils_DBCP {

    private static DataSource ds = null;

    static {
        try {
            // 讀配置文件 dbcpconfig.properties
            InputStream in = JdbcUtils_DBCP.class.getClassLoader()
                    .getResourceAsStream("dbcpconfig.properties");
            Properties prop = new Properties();
            prop.load(in);

            BasicDataSourceFactory factory = new BasicDataSourceFactory();
            ds = factory.createDataSource(prop);
        } catch (Exception e) {
            throw new ExceptionInInitializerError(e); // 異常轉換成錯誤
        }
    }

    public static Connection getConnection() throws SQLException {
        return ds.getConnection(); // dbcp conn.close() commit()
    }

    public static void release(Connection conn, Statement st, ResultSet rs) {
        // 模板代碼
        if (rs != null) {
            try {
                rs.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
            rs = null;
        }
        if (st != null) {
            try {
                st.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
            st = null;
        }
        if (conn != null) {
            try {
                conn.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

3.2 C3P0 數據源 (Spring 內置數據源)

C3P0 的jar包在c3p0-0.9.2-pre1中,導入以下2個jar包:

  • c3p0-0.9.2-pre1.jar
  • mchange-commons-0.2.jar

C3P0數據源配置文件名爲c3p0-config.xml,可放在src目錄下,C3P0本身會找到它。

c3p0-config.xml例子以下:

<c3p0-config>
    <default-config>
        <property name="driverClass">com.mysql.jdbc.Driver</property>
        <property name="jdbcUrl">jdbc:mysql://localhost:3306/day16</property>
        <property name="user">root</property>
        <property name="password">root</property>       
        <property name="initialPoolSize">10</property>
        <property name="maxIdleTime">30</property>
        <property name="maxPoolSize">20</property>
        <property name="minPoolSize">5</property>
        <property name="maxStatements">200</property>
    </default-config>

    <named-config name="mysql">
        <property name="acquireIncrement">50</property>
        <property name="initialPoolSize">100</property>
        <property name="minPoolSize">50</property>
        <property name="maxPoolSize">1000</property><!-- intergalactoApp adopts a different approach to configuring statement caching -->
        <property name="maxStatements">0</property>
        <property name="maxStatementsPerConnection">5</property>
    </named-config>

    <named-config name="oracle">
        <property name="acquireIncrement">50</property>
        <property name="initialPoolSize">100</property>
        <property name="minPoolSize">50</property>
        <property name="maxPoolSize">1000</property><!-- intergalactoApp adopts a different approach to configuring statement caching -->
        <property name="maxStatements">0</property>
        <property name="maxStatementsPerConnection">5</property>
    </named-config>
</c3p0-config>

最上面的<default-config>是默認配置,使用方法以下:

ComboPooledDataSource ds =  new ComboPooledDataSource();

若想用<named-config name="oracle">的配置,使用方法以下:

ComboPooledDataSource ds =  new ComboPooledDataSource("oracle");

看起來很是方便。

完整的 C3P0 鏈接創建代碼JdbcUtils_C3P0以下:

package cn.wk.utils;

import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;

import com.mchange.v2.c3p0.ComboPooledDataSource;

public class JdbcUtils_C3P0 {

    private static ComboPooledDataSource ds = null;
    static {
        try {
            ds = new ComboPooledDataSource();
        } catch (Exception e) {
            throw new ExceptionInInitializerError(e); // 異常轉換成錯誤
        }
    }

    public static Connection getConnection() throws SQLException {
        return ds.getConnection();
    }

    public static void release(Connection conn, Statement st, ResultSet rs) {
        // 模板代碼
        if (rs != null) {
            try {
                rs.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
            rs = null;
        }
        if (st != null) {
            try {
                st.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
            st = null;
        }
        if (conn != null) {
            try {
                conn.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

測試代碼:

public class Demo4 {
    public static void main(String[] args) throws SQLException,
            InterruptedException {
        Connection conn = null;
        PreparedStatement st = null;
        ResultSet rs = null;
        try {
            conn = JdbcUtils_C3P0.getConnection();
            System.out.println(conn.getClass().getName());
        } finally {
            JdbcUtils_C3P0.release(conn, st, rs);
        }
    }
}

4. 編寫本身的 JDBC 框架

4.1 元數據 - DataBaseMetaData

元數據:數據庫、表、列的定義信息。
Connection.getDatabaseMetaData()

DataBaseMetaData對象

  • getURL():返回一個String類對象,表明數據庫的URL。
  • getUserName():返回鏈接當前數據庫管理系統的用戶名。
  • getDatabaseProductName():返回數據庫的產品名稱。
  • getDatabaseProductVersion():返回數據庫的版本號。
  • getDriverName():返回驅動驅動程序的名稱。
  • getDriverVersion():返回驅動程序的版本號。
  • isReadOnly():返回一個boolean值,指示數據庫是否只容許讀操做。

ParameterMetaData對象,獲取 sql 語句參數的元數據。

以上2個元數據對象例子以下:

public class Demo5 {
    public static void main(String[] args) throws SQLException {

        Connection conn = JdbcUtils_C3P0.getConnection();

        // 獲取數據庫的元數據
        DatabaseMetaData meta = conn.getMetaData();     
        System.out.println(meta.getDatabaseProductName());

        // 獲取參數元數據
        String sql = "insert into user(id,name) values(?,?)";
        PreparedStatement st = conn.prepareStatement(sql);
        ParameterMetaData para_meta = st.getParameterMetaData();        
        System.out.println(para_meta.getParameterCount());
        System.out.println(para_meta.getParameterType(1)); // mysql不支持得到類型,拋異常
    }
}

ResultSetMetaData對象(重要,後面案例用到),結果集元數據:

  • getColumnCount() 返回resultset對象的列數
  • getColumnName(int column) 得到指定列的名稱
  • getColumnTypeName(int column) 得到指定列的類型

4.2 作本身的 jdbc 框架

準備:
模擬環境,先弄一個cn.wk.domain.Account的javabean:

package cn.wk.domain;

public class Account {
    private int id;
    private String name;
    private double money;
    public int getId() {return id;}
    public void setId(int id) {this.id = id;}
    public String getName() {return name;}
    public void setName(String name) {this.name = name;}
    public double getMoney() {return money;}
    public void setMoney(double money) {this.money = money;}
}

dao 層方法大體代碼:
注意到:crud 變化的是 sql 和 st.set 其他代碼均相同

public void add(Account a) throws SQLException{
  Connection conn = null;
  PreparedStatement st = null;
  ResultSet rs = null;
  try {
    conn = JdbcUtils_DBCP.getConnection();
    String sql = "(?,?,?)";
    st.setInt(1, a.getId());
    st.setString(2, a.getName());
    st.setDouble(3, a.getMoney());

    st.executeUpdate();
  } finally {
    JdbcUtils_DBCP.release(conn, st, rs);
  }
}

public void delete(int id) throws SQLException{
  Connection conn = null;
  PreparedStatement st = null;
  ResultSet rs = null;
  try {
    conn = JdbcUtils_DBCP.getConnection();
    String sql = "delete from where id=?";
    st.setInt(1, id);           

    st.executeUpdate();
  } finally {
    JdbcUtils_DBCP.release(conn, st, rs);
  }
}

如今要作優化,抽出相同的部分。

重寫了cn.wk.utils.JdbcUtils,重點在該工具類的release方法的後面, 涉及到如下知識點:
本身 = 框架編寫者

  1. 本身的框架提供個接口(處理器),供用戶填寫;
  2. 本身寫好了經常使用的處理器實現;
  3. 使用元數據;
  4. 使用反射技術,根據結果集字段名向對應的bean的域中寫入數據。
package cn.wk.utils;

import java.io.InputStream;
import java.lang.reflect.Field;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Properties;

import javax.sql.DataSource;

import org.apache.commons.dbcp.BasicDataSourceFactory;

public class JdbcUtils {

    private static DataSource ds = null;

    static {
        try {
            // 讀配置文件 dbcpconfig.properties
            InputStream in = JdbcUtils.class.getClassLoader()
                    .getResourceAsStream("dbcpconfig.properties");
            Properties prop = new Properties();
            prop.load(in);

            BasicDataSourceFactory factory = new BasicDataSourceFactory();
            ds = factory.createDataSource(prop);
        } catch (Exception e) {
            throw new ExceptionInInitializerError(e); // 異常轉換成錯誤
        }
    }

    public static Connection getConnection() throws SQLException {
        return ds.getConnection(); // dbcp conn.close() commit()
    }

    public static void release(Connection conn, Statement st, ResultSet rs) {
        // 模板代碼
        if (rs != null) {
            try {
                rs.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
            rs = null;
        }
        if (st != null) {
            try {
                st.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
            st = null;
        }
        if (conn != null) {
            try {
                conn.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    /* 抽取 增刪改 的公共代碼 */

    // add delete update 都調用下面方法,變化的部分 sql , params

    // String sql="insert into account(id,name,money) values(?,?,?)";
    // object[]{1,"aaa","1000"}
    public static void update(String sql, Object params[]) throws SQLException {
        Connection conn = null;
        PreparedStatement st = null;
        ResultSet rs = null;
        try {
            conn = getConnection();
            st = conn.prepareStatement(sql);
            for (int i = 0; i < params.length; i++)
                st.setObject(i + 1, params[i]);
            st.executeUpdate();
        } finally {
            release(conn, st, rs);
        }
    }

    // 想替換掉全部 查詢
    public static Object query(String sql, Object params[],
            ResultSetHandler handler) throws SQLException {

        Connection conn = null;
        PreparedStatement st = null;
        ResultSet rs = null;
        try {
            conn = getConnection();
            st = conn.prepareStatement(sql);
            for (int i = 0; i < params.length; i++)
                st.setObject(i + 1, params[i]);
            rs = st.executeQuery(); // 接下來, 框架製做者不知道該怎樣處理 rs
            // 方法: 對外暴露個接口,讓調用者實現那個接口(handler),咱們用客戶所實現的接口處理 rs
            // 調用用戶傳來的 handler
            return handler.handler(rs);
        } finally {
            release(conn, st, rs);
        }
    }
}

// 設計一個接口,對外暴露
interface ResultSetHandler {
    public Object handler(ResultSet rs); // 讓用戶實現這個方法
}

// 框架做者根據現實狀況,提早寫好一些處理器
class BeanHandler implements ResultSetHandler {
    // 不知道 bean 是啥, 就定義一個變量接收,且用構造函數提供對外訪問方式
    private Class clazz;

    public BeanHandler(Class clazz) {
        this.clazz = clazz;
    }

    @Override
    public Object handler(ResultSet rs) {
        try {
            if (!rs.next())
                return null;

            // 建立出要封裝結果集的 bean
            Object bean = this.clazz.newInstance();

            // 經過元數據技術獲知 rs 裏有啥
            ResultSetMetaData meta = rs.getMetaData();
            int colNum = meta.getColumnCount();
            for (int i = 0; i < colNum; i++) {
                String name = meta.getColumnName(i + 1); // 結果集每列列名 id
                Object value = rs.getObject(name);       // 1

                // 經過 name,反射出 bean 上與 name對應的屬性
                Field f = bean.getClass().getDeclaredField(name);
                f.setAccessible(true); // 強制訪問私有元素
                f.set(bean, value);
            }
            return bean;
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

// 返回包含 bean 的 list 集合
class BeanListHandler implements ResultSetHandler {
    private Class clazz;

    public BeanListHandler(Class clazz) {
        this.clazz = clazz;
    }

    @Override
    public Object handler(ResultSet rs) {
        List list = new ArrayList();
        try {
            ResultSetMetaData meta = rs.getMetaData();
            int count = meta.getColumnCount();
            while (rs.next()) {
                Object bean = this.clazz.newInstance();
                for (int i = 0; i < count; i++) {
                    String name = meta.getColumnName(i + 1);
                    Object value = rs.getObject(name);

                    Field f = bean.getClass().getDeclaredField(name); // 反射獲取域
                    f.setAccessible(true);
                    f.set(bean, value);
                }
                list.add(bean);
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        return list;
    }
}

模擬使用該框架的 dao 代碼:

package cn.wk.utils;
import java.sql.SQLException;
import org.junit.Test;
import cn.wk.domain.Account;

// 假設這是 Dao
// 注意到:crud 變化的是 sql 和 st.set 其他代碼均相同
public class Demo7 {
    @Test
    public void test() throws SQLException {
    List<?> list = getAll();
        System.out.println(list.size());
    }
    public void add(Account a) throws SQLException {
        String sql = "insert into account(name,money) values(?,?)";
        Object params[] = { a.getName(), a.getMoney() };
        JdbcUtils.update(sql, params);
    }
    public void delete(int id) throws SQLException {
        String sql = "delete from account where id=?";
        Object params[] = { id };
        JdbcUtils.update(sql, params);
    }
    public void update(Account a) throws SQLException {
        String sql = "update account set name=?, money=? where id=?";
        Object params[] = { a.getName(), a.getMoney(), a.getId() };
        JdbcUtils.update(sql, params);
    }
    public Account find(int id) throws SQLException {
        String sql = "select * from account where id=?";
        Object params[] = { id };
        return (Account) JdbcUtils.query(sql, params, new BeanHandler(
                Account.class));
    }
  public List getAll() throws SQLException {
  String sql = "select * from account";
  Object params[] = {};
  return (List) JdbcUtils.query(sql, params, new BeanListHandler(
      Account.class));
  }
}
相關文章
相關標籤/搜索