模板方法設計模式在JDBC中的應用

設計模式是在特定場景下對特定問題的解決方案,這些解決方案是通過反覆論證和測試總結出來的。實際上,除了軟件設計,設計模式也被普遍應用於其餘領域,好比UI設計和建築設計等。Java軟件設計模式大都來源於GoF的23種設計模式。java

這段時間一直在錄製Java EE視頻課程,其中在JDBC(Java數據庫鏈接)中使用了模板方法設計(Template Method),下面給你們分享一下。mysql

1. 什麼是模板方法設計模式?

在生活中完成一些「任務」有着固定的步驟,例如,我要完成「喝茶」任務,須要的步驟以下:
①燒水→②沏茶→③喝茶
而不少任務也有相似的步驟,例如,我要完成「喝咖啡」任務,固然是速溶咖啡那種。須要的步驟以下:
①燒水→②衝咖啡→③喝咖啡
對應兩個任務他們有相似的3個步驟,步驟①和③是相同的,而步驟②是不一樣的。這樣能夠設計一個父類TaskTemplate代碼以下:git

public abstract class TaskTemplate {

    public final void 任務() {
        // 步驟①
        燒水();
        // 步驟②
        沖泡();
        // 步驟③
        喝();
    }

    private void 燒水() {
        System.out.println("燒水...");
    }

    protected abstract void 沖泡();

    private void 喝() {
        System.out.println("喝...");
    }

}

TaskTemplate是一個抽象類,其中「任務()」方法中定義了執行「任務」的流程,其中「燒水()」和「喝()」是兩個具體方法,因爲父類中沒法肯定沖泡什麼,所以「沖泡()」方法是抽象方法,留給子類實現。「任務()」就是模板方法。
「喝茶」任務實現類TeaTask代碼以下:github

public class TeaTask extends TaskTemplate {

    @Override
    protected void 沖泡() {
        System.out.println("來壺鐵觀音。");
    }
}

「喝咖啡」任務實現類CoffeeTask代碼以下:sql

public class CoffeeTask extends TaskTemplate {

    @Override
    protected void 沖泡() {
        System.out.println("衝卡布奇諾咖啡+糖+奶。");
    }
}

他們的類圖如圖1所示。
圖1 類圖
這就是模板方法設計模式了,那麼如何使用呢?示例代碼以下:數據庫

public class Main {

    public static void main(String[] args) {

        System.out.println("------喝茶任務------");
        TaskTemplate template = new TeaTask();
        template.任務();

        System.out.println("------喝咖啡任務------");
        template = new CoffeeTask();
        template.任務();
    }
}

輸出結果以下:編程

------喝茶任務------
燒水...
來壺鐵觀音。
喝...
------喝咖啡任務------
燒水...
衝卡布奇諾咖啡+糖+奶。
喝...

上述代碼模板子類是有名類,而有時候子類個數太多,也能夠採用匿名內部類做爲模板子類。修改Main調用代碼以下:設計模式

public class Main {

    public static void main(String[] args) {

        System.out.println("------喝茶任務------");
        TaskTemplate template = new TaskTemplate() { ①
            @Override
            protected void 沖泡() {
                System.out.println("來壺鐵觀音。");
            }
        };
        template.任務();

        System.out.println("------喝咖啡任務------");
        template = new TaskTemplate() {  ②
            @Override
            protected void 沖泡() {
                System.out.println("衝卡布奇諾咖啡+糖+奶。");
            }
        };
        template.任務();

    }
}

上述代碼第①行是實現了喝茶任務子類功能,代碼第②行是實現了喝咖啡任務子類功能。框架

2. 糟糕的JDBC代碼

上面的介紹的設計模式或許很容易理解,可是又有什麼用途呢?使用設計模式是學習的難點。下面先來看看糟糕的JDBC代碼:ide

public class Main {

    public static void main(String[] args) {

        //查詢數據
        read();
        //數據插入
        create();
        //數據更新
        update();
        //刪除數據
        delete();

    }

    /**
     * 查數據
     */
    private static void read() {

        // 載數據庫驅動
        loadDBDriver();

        String sql = "select name, userid from user where userid > ? order by userid";

        Connection connection = null;
        PreparedStatement ps = null;
        try {
            // 建立數據庫鏈接
            connection = getConnection();

            // 建立語句對象
            ps = connection.prepareStatement(sql);

            // 綁定參數
            ps.setInt(1, 0);
            ResultSet rs = ps.executeQuery();

            //遍歷結果集
            while (rs.next()) {
                System.out.printf("name: %s     id: %d \n",
                        rs.getString("name"),
                        rs.getInt("userid"));
            }

        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            if (connection != null) {
                try {
                    connection.close();
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }
            if (ps != null) {
                try {
                    ps.close();
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    /**
     * 插入數據
     */
    private static void create() {

        // 載數據庫驅動
        loadDBDriver();

        String sql = "insert into user (userid, name) values (?, ?)";

        Connection connection = null;
        PreparedStatement ps = null;

        try {
            // 建數據庫鏈接
            connection = getConnection();

            // 建立語句對象
            ps = connection.prepareStatement(sql);

            // 綁定參數
            ps.setInt(1, 999);
            ps.setString(2, "Tony999");
            // 執行SQL語句
            int count = ps.executeUpdate();

            System.out.printf("成功插入%d條數據.\n", count);

        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            if (connection != null) {
                try {
                    connection.close();
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }
            if (ps != null) {
                try {
                    ps.close();
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    /**
     * 更新數據
     */
    private static void update() {

        // 載數據庫驅動
        loadDBDriver();

        String sql = "update user set name=? where userid =?";

        Connection connection = null;
        PreparedStatement ps = null;

        try {
            // 建立數據庫鏈接
            connection = getConnection();

            // 建立語句對象
            ps = connection.prepareStatement(sql);

            // 綁定參數
            ps.setString(1, "Tom999");
            ps.setInt(2, 999);
            // 執行SQL語句
            int count = ps.executeUpdate();

            System.out.printf("成功更新%d條數據.\n", count);

        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            if (connection != null) {
                try {
                    connection.close();
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }
            if (ps != null) {
                try {
                    ps.close();
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }
        }

    }

    /**
     * 刪除數據
     */
    private static void delete() {

        // 載數據庫驅動
        loadDBDriver();

        String sql = "delete from user where userid = ?";

        Connection connection = null;
        PreparedStatement ps = null;

        try {
            // 建立數據庫鏈接
            connection = getConnection();

            // 建立語句對象
            ps = connection.prepareStatement(sql);

            // 綁定參數
            ps.setInt(1, 999);
            // 執行SQL語句
            int count = ps.executeUpdate();

            System.out.printf("成功刪除%d條數據.\n", count);

        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            if (connection != null) {
                try {
                    connection.close();
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }
            if (ps != null) {
                try {
                    ps.close();
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    /**
     * 創建數據庫鏈接
     *
     * @return 返回數據庫鏈接對象
     * @throws SQLException
     */
    private static Connection getConnection() throws SQLException {

        String url = "jdbc:mysql://localhost:3306/mydb?verifyServerCertificate=false&useSSL=false";
        String user = "root";
        String password = "12345";

        Connection connection = DriverManager.getConnection(url, user, password);
        return connection;
    }

    /**
     * 加載數據庫驅動
     */
    private static void loadDBDriver() {
        // 1.
        try {
            Class.forName("com.mysql.jdbc.Driver");
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }

}

上述代碼中訪問數據的方法有4個read()、create()、update()和delete()。其中create()、update()和delete()三個方法代碼很是類似,只是SQL語句和綁定參數不一樣而已。雖然read()方法與create()、update()和delete()方法不一樣,可是差異也不大。
JDBC代碼主要的問題是:大量的重複代碼!!!

3. 在JDBC中使用模板設計方法模式

從上一節代碼總結數據庫編程通常過程,如圖2所示。
圖2 數據庫編程通常過程
從圖3中可見查詢(Read)過程最多須要7個步驟。修改(C插入、U更新、D刪除)過程最多須要6個步驟。其中有些步驟是不變的,而有些步驟是可變的。如圖3所示,查詢過程當中一、二、5和7步是不可變的全部查詢都是同樣的,而三、4和6步不一樣,第3步在「建立語句對象」時須要指定SQL語句,這是「此查詢」與「彼查詢」的不一樣之處;因爲SQL語句的不一樣綁定參數也可能不一樣,因此第4步也是不一樣的;另外,第6步是「遍歷結果集」也會根據查詢的不一樣字段,以及字段提取後處理的方式不一樣而有所不一樣。
圖3  查詢過程

使用代碼模板方法模式,能夠將一、二、5和7步定義在父類在,將三、4和6步定義在子類中。代碼以下:

public abstract class JdbcTemplate {

  public final void query() {

    // 一、載數據庫驅動
    loadDBDriver();

    Connection connection = null;
    PreparedStatement ps = null;
    try {
        // 二、建立數據庫鏈接
        connection = getConnection();

        // 三、建立語句對象 四、綁定參數
        ps = createPreparedStatement(connection);

        // 五、執行查詢
        ResultSet rs = ps.executeQuery();

        // 六、遍歷結果集
        while (rs.next()) {
            proce***ow(rs);
        }

    } catch (SQLException e) {
        e.printStackTrace();
    } finally {
        // 七、釋放資源
        if (connection != null) {
            try {
                connection.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
        if (ps != null) {
            try {
                ps.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
    }
  }

    /**
     * 遍歷結果集時,處理結果集
     * @param rs 結果集
     * @throws SQLException
     */
    public abstract void proce***ow(ResultSet rs) throws SQLException; ③

    /**
     * 建立語句對象,其中包括指定SQL語句,綁定參數。
     * @param conn 鏈接對象
     * @return 語句對象
     * @throws SQLException
     */
    public abstract PreparedStatement 
            createPreparedStatement(Connection conn)  throws SQLException; ④

    /**
     * 創建數據庫鏈接
     *
     * @return 返回數據庫鏈接對象
     * @throws SQLException
     */
    private static Connection getConnection() throws SQLException {

        String url = "jdbc:mysql://localhost:3306/mydb?verifyServerCertificate=false&useSSL=false";
        String user = "root";
        String password = "12345";

        Connection connection = DriverManager.getConnection(url, user, password);
        return connection;
    }

    /**
     * 加載數據庫驅動
     */
    private static void loadDBDriver() {
        try {
            Class.forName("com.mysql.jdbc.Driver");
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

在查詢方法中代碼第①行調用抽象方法createPreparedStatement(connection)建立預處理的語句對象,事實上在建立語句對象時,還能夠爲其綁定參數,因此代碼第①行調用createPreparedStatement(connection)過程當中實現「三、建立語句對象」和「四、綁定參數」。
代碼第②行是在遍歷結果集過程當中調用抽象方法proce***ow(rs)處理結果集。通常而言全部遍歷結果集都是while (rs.next()) {…}循環語句實現的,只是提取的字段不一樣,提取以後的處理過程不一樣。
那麼調用read()方法代碼以下:

/**
 * 查數據
 */
private static void read() {

    String sql = "select name, userid from user where userid > ? order by userid";

    JdbcTemplate template = new JdbcTemplate() {  ①

        @Override
        public PreparedStatement createPreparedStatement(Connection conn) throws SQLException {
            // 綁定參數
            PreparedStatement ps = conn.prepareStatement(sql);
            // 綁定參數
            ps.setInt(1, 0);

            return ps;
        }

        @Override
        public void proce***ow(ResultSet rs) throws SQLException {
            System.out.printf("name: %s     id: %d \n",
                    rs.getString("name"),
                    rs.getInt("userid"));
        }

    }; ②

    template.query(); ③

}

上述代碼第①行~第②行採用匿名內部類子類化JdbcTemplate類,而且實例化它,而沒有采用有名類子類化JdbcTemplate類,這是由於每一次查詢都須要一個JdbcTemplate子類,以及該子類的實例。這樣會須要建立不少個JdbcTemplate子類。代碼第③行調用模板方法query()執行查詢。

圖4所示是修改過程,其中一、二、5和6步是不可變的全部修改(插入、刪除和更新)都是同樣的,而3和4步是不一樣的。
圖4  JDBC修改過程

使用代碼模板方法模式,能夠將一、二、5和7步定義在父類在,將三、4和6步定義在子類中。代碼以下:

public abstract class JdbcTemplate {

    public final void update() {

        // 一、載數據庫驅動
        loadDBDriver();

        Connection connection = null;
        PreparedStatement ps = null;
        try {
            // 二、建立數據庫鏈接
            connection = getConnection();

            // 三、建立語句對象 四、綁定參數
            ps = createPreparedStatement(connection); ①

            // 五、執行SQL語句
            int count = ps.executeUpdate();

            System.out.printf("成功修改%d條數據.\n", count);

        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            // 六、釋放資源
            if (connection != null) {
                try {
                    connection.close();
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }
            if (ps != null) {
                try {
                    ps.close();
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

代碼第①行的createPreparedStatement(connection)方法與查詢時共用該方法,當子類實現該方法時建立預編譯語句對象和綁定參數。

那麼調用create()方法的代碼以下:

/**
 * 插入數據
 */
private static void create() {

    String sql = "insert into user (userid, name) values (?, ?)";
    JdbcTemplate template = new JdbcTemplate() {

        @Override
        public PreparedStatement createPreparedStatement(Connection conn) throws SQLException {
            // 綁定參數
            PreparedStatement ps = conn.prepareStatement(sql);
            // 綁定參數
            ps.setInt(1, 999);
            ps.setString(2, "Tony999");

            return ps;
        }

        @Override
        public void proce***ow(ResultSet rs) throws SQLException {}  ①

    };

    template.update();
}

插入數據的模板也是採用匿名內部類子類化JdbcTemplate,因爲插入過程不須要遍歷結果集,因此抽象方法proce***ow()採用空實現,見代碼第①行。另外update()也是模板方法。
更新數據和刪除數據方法與插入數據方法是相似的,代碼以下:

/**
 * 更新數據
 */
private static void update() {

    String sql = "update user set name=? where userid =?";
    JdbcTemplate template = new JdbcTemplate() {

        @Override
        public PreparedStatement createPreparedStatement(Connection conn) throws SQLException {
            // 綁定參數
            PreparedStatement ps = conn.prepareStatement(sql);
            // 綁定參數
            ps.setString(1, "Tom999");
            ps.setInt(2, 999);

            return ps;
        }

        @Override
        public void proce***ow(ResultSet rs) throws SQLException {
        }

    };

    template.update();

}

/**
 * 刪除數據
 */
private static void delete() {

    String sql = "delete from user where userid = ?";
    JdbcTemplate template = new JdbcTemplate() {

        @Override
        public PreparedStatement createPreparedStatement(Connection conn) throws SQLException {
            // 綁定參數
            PreparedStatement ps = conn.prepareStatement(sql);
            // 綁定參數
            ps.setInt(1, 999);

            return ps;
        }

        @Override
        public void proce***ow(ResultSet rs) throws SQLException {
        }

    };

    template.update();
}

讀者能夠比較一下,採用了模板設計方法後是否是代碼變得很簡單了呢!

4. 後記

JDBC模板子類不要採用有名子類化JDBC模板父類,這會使咱們爲每個查詢和修改操做而編寫一個子類,這個數量會不少。
再有,從上面的代碼可見,模板設計方法仍是能夠進行優化的。事實上還能夠更加抽象一下,即採用接口替代兩個抽象方法,這樣會更加靈活,並且可使用Lambda表達式替代內部類。這種方式就Spring框架的實現Jdbc模板的實現方法,感興趣的同窗能夠看看Spring的源代碼。另外,能夠經過關東昇老師《Java Web從入門到實戰》視頻課程第5章JDBC技術瞭解具體細節。

代碼下載地址:https://github.com/tonyguan/JdbcTemplate

《Java Web從入門到實戰》視頻課程:
一、進入51CTO學院該課程

相關文章
相關標籤/搜索