模板方法设计模式在JDBC中的应用

设计模式是在特定场景下对特定问题的解决方案,这些解决方案是经过反复论证和测试总结出来的。实际上,除了软件设计,设计模式也被广泛应用于其他领域,比如UI设计和建筑设计等。Java软件设计模式大都来源于GoF[1]的23种设计模式。

这段时间一直在录制Java EE视频课程,其中在JDBC(Java数据库连接)中使用了模板方法设计(Template Method),下面给大家分享一下。

1. 什么是模板方法设计模式?

在生活中完成一些“任务”有着固定的步骤,例如,我要完成“喝茶”任务,需要的步骤如下:
①烧水→②沏茶→③喝茶
而很多任务也有类似的步骤,例如,我要完成“喝咖啡”任务,当然是速溶咖啡那种。需要的步骤如下:
①烧水→②冲咖啡→③喝咖啡
对应两个任务他们有类似的3个步骤,步骤①和③是相同的,而步骤②是不同的。这样可以设计一个父类TaskTemplate代码如下:

public abstract class TaskTemplate {

    public final void 任务() {
        // 步骤①
        烧水();
        // 步骤②
        冲泡();
        // 步骤③
        喝();
    }

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

    protected abstract void 冲泡();


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

}

TaskTemplate是一个抽象类,其中“任务()”方法中定义了执行“任务”的流程,其中“烧水()”和“喝()”是两个具体方法,由于父类中无法确定冲泡什么,因此“冲泡()”方法是抽象方法,留给子类实现。“任务()”就是模板方法。
“喝茶”任务实现类TeaTask代码如下:

public class TeaTask extends TaskTemplate {

    @Override
    protected void 冲泡() {
        System.out.println("来壶铁观音。");
    }
}

“喝咖啡”任务实现类CoffeeTask代码如下:

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代码:

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所示,查询过程中1、2、5和7步是不可变的所有查询都是一样的,而3、4和6步不同,第3步在“创建语句对象”时需要指定SQL语句,这是“此查询”与“彼查询”的不同之处;由于SQL语句的不同绑定参数也可能不同,所以第4步也是不同的;另外,第6步是“遍历结果集”也会根据查询的不同字段,以及字段提取后处理的方式不同而有所不同。


图3 查询过程

使用代码模板方法模式,可以将1、2、5和7步定义在父类在,将3、4和6步定义在子类中。代码如下:

public abstract class JdbcTemplate {

  public final void query() {

    // 1、载数据库驱动
    loadDBDriver();

    Connection connection = null;
    PreparedStatement ps = null;
    try {
        // 2、创建数据库连接
        connection = getConnection();

        // 3、创建语句对象 4、绑定参数
        ps = createPreparedStatement(connection);

        // 5、执行查询
        ResultSet rs = ps.executeQuery();

        // 6、遍历结果集
        while (rs.next()) {
            processRow(rs);
        }

    } catch (SQLException e) {
        e.printStackTrace();
    } finally {
        // 7、释放资源
        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 processRow(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)过程中实现“3、创建语句对象”和“4、绑定参数”。
代码第②行是在遍历结果集过程中调用抽象方法processRow(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 processRow(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所示是修改过程,其中1、2、5和6步是不可变的所有修改(插入、删除和更新)都是一样的,而3和4步是不同的。


图4 JDBC修改过程

使用代码模板方法模式,可以将1、2、5和7步定义在父类在,将3、4和6步定义在子类中。代码如下:

public abstract class JdbcTemplate {

    public final void update() {

        // 1、载数据库驱动
        loadDBDriver();

        Connection connection = null;
        PreparedStatement ps = null;
        try {
            // 2、创建数据库连接
            connection = getConnection();

            // 3、创建语句对象 4、绑定参数
            ps = createPreparedStatement(connection); ①

            // 5、执行SQL语句
            int count = ps.executeUpdate();

            System.out.printf("成功修改%d条数据.\n", count);

        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            // 6、释放资源
            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 processRow(ResultSet rs) throws SQLException {}  ①

    };

    template.update();
}

插入数据的模板也是采用匿名内部类子类化JdbcTemplate,由于插入过程不需要遍历结果集,所以抽象方法processRow()采用空实现,见代码第①行。另外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 processRow(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 processRow(ResultSet rs) throws SQLException {
        }

    };

    template.update();
}

读者可以比较一下,采用了模板设计方法后是不是代码变得很简单了呢!

4. 后记

JDBC模板子类不要采用有名子类化JDBC模板父类,这会使我们为每一个查询和修改操作而编写一个子类,这个数量会很多。
再有,从上面的代码可见,模板设计方法还是可以进行优化的。事实上还可以更加抽象一下,即采用接口替代两个抽象方法,这样会更加灵活,而且可以使用Lambda表达式替代内部类。这种方式就Spring框架的实现Jdbc模板的实现方法,感兴趣的同学可以看看Spring的源代码。另外,可以通过关东升老师《Java Web从入门到实战》视频课程第5章JDBC技术了解具体细节。

代码下载地址:https://github.com/tonyguan/JdbcTemplate

《Java Web从入门到实战》视频课程:
1、进入51CTO学院该课程
2、进入网易云课堂该课程


  1. Design Patterns: Elements of Reusable Object-Oriented
    Software
    (中文版《设计模式》)一书由Erich Gamma、Richard Helm、Ralph Johnson 和
    John Vlissides 合著(Addison-Wesley,1995),这四位作者常被称为“四人组”(Gang of
    Four,GoF)。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,772评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,458评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,610评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,640评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,657评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,590评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,962评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,631评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,870评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,611评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,704评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,386评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,969评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,944评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,179评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,742评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,440评论 2 342

推荐阅读更多精彩内容

  • 第一次感受到听到某首音乐就要爆炸的节奏!就是那种才听到前奏就想说“你给我闭嘴!”的那种感觉,真的是够了,啊啊啊啊啊...
    胡萝卜猫阅读 214评论 0 0
  • 一起工作这么久第一次听到被直呼我的名字。虽然我的名字是念起来有些不顺口,但怎么说呢,虽然今天有点迫不得已的意思,但...
    Joshua_05d6阅读 309评论 0 0
  • procrastinate v.拖延 耽搁 procrastinator n.拖延症人 catastrophic ...
    XINRRong阅读 217评论 0 0
  • “老婆,我们养个孩子吧,不管是男孩还是女孩,我们都要好好爱她/他哦。” 新婚的第一年,老公就特别想要一个宝宝,每天...
    素丹v阅读 404评论 0 2