首页 > 代码库 > 编写自己的JDBC框架

编写自己的JDBC框架

元数据介绍

元数据指的是”数据库”、”表”、”列”的定义信息。

DataBaseMetaData元数据

Connection.getDatabaseMetaData()获得代表DatabaseMetaData元数据的DatabaseMetaData对象。
DataBaseMetaData对象的常用方法:

  • getURL():返回一个String类对象,代表数据库的URL。
  • getUserName():返回连接当前数据库管理系统的用户名。
  • getDatabaseProductName():返回数据库的产品名称。
  • getDatabaseProductVersion():返回数据库的版本号。
  • getDriverName():返回驱动程序的名称。
  • getDriverVersion():返回驱动程序的版本号。
  • isReadOnly():返回一个boolean值,指示数据库是否只允许读操作。

例,

public class Demo1 {

    /*
     * 获取数据库的元数据
     */
    public static void main(String[] args) throws SQLException {

        Connection conn = JdbcUtils_C3P0.getConnection();
        DatabaseMetaData meta = conn.getMetaData();
        System.out.println(meta.getDatabaseProductName());
        System.out.println(meta.getDatabaseMajorVersion());
        System.out.println(meta.getDatabaseMinorVersion()); // 5.X

    }

}

JdbcUtils_C3P0这个类的代码怎么写,可以参考我的笔记开源数据库连接池——C3P0数据源(Spring内置)。
运行结果如下:

MySQL
5
7

ParameterMetaData元数据——参数的元数据

PreparedStatement.getParameterMetaData()获得代表PreparedStatement元数据的ParameterMetaData对象。
例如,有这样的一条SQL语句:

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

我们要想知道参数的个数和每一个参数的类型,就得用到ParameterMetaData元数据。
ParameterMetaData对象的常用方法:

  • getParameterCount(): 获得指定参数的个数。
  • getParameterType(int param):获得指定参数的sql类型。MySQL数据库驱动不支持

例,

public class Demo2 {

    /*
     * 获取参数的元数据
     */
    public static void main(String[] args) throws SQLException {

        Connection conn = JdbcUtils_C3P0.getConnection();
        String sql = "insert into user(name,password) values(?,?)";

        PreparedStatement st = conn.prepareStatement(sql);
        ParameterMetaData meta = st.getParameterMetaData();

        System.out.println(meta.getParameterCount()); // 获取参数个数
        /*
         * 以下语句运行会报异常:
         * java.sql.SQLException: Parameter metadata not available for the given statement
         */
        System.out.println(meta.getParameterType(1)); // MySQL驱动不支持获取参数类型,即第一个参数需要什么类型的数据
    }

}

ResultSetMetaData元数据——结果集的元数据

ResultSet. getMetaData()获得代表ResultSet对象元数据的ResultSetMetaData对象。
ResultSetMetaData对象的常用方法:

  • getColumnCount():返回resultset对象的列数。
  • getColumnName(int column):获得指定列的名称。
  • getColumnTypeName(int column):获得指定列的类型。

例,

public class Demo9 {

    /*
     * 获取结果集的元数据
     */
    public static void main(String[] args) throws SQLException {

        Connection conn = JdbcUtils_C3P0.getConnection();
        String sql = "select * from account";
        PreparedStatement st = conn.prepareStatement(sql);
        ResultSet rs = st.executeQuery();

        ResultSetMetaData meta = rs.getMetaData();
        System.out.println(meta.getColumnCount());
        System.out.println(meta.getColumnName(1));
        System.out.println(meta.getColumnName(2));
        System.out.println(meta.getColumnName(3));

        // rs.getObject("XXX");
    }

}

使用元数据封装简单的JDBC框架

业务背景:系统中所有实体对象都涉及到基本的CRUD操作。
所有实体的CUD操作代码基本相同,仅仅发送给数据库的SQL语句不同而已,因此可以把CUD操作的所有相同代码抽取到工具类的一个update方法中,并定义参数接收变化的SQL语句。
实体的R操作,除SQL语句不同之外,根据操作的实体不同,对ResultSet的映射也各不相同,因此可义一个query方法,除以参数形式接收变化的SQL语句外,可以使用策略模式由qurey方法的调用者决定如何把ResultSet中的数据映射到实体对象中。
在设计使用元数据封装简单的JDBC框架之前,我们先编写测试用的SQL脚本:

/* 创建数据库 */
create database day16;

use day16;

/* 创建账户表 */
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); 

以前我们与MySQL数据库打交道的代码是这样写的:

// 模拟Dao,公共的代码都应该提取出来
public class TestJdbcFramework {

    public void add(Account a) throws SQLException {
        Connection conn = null;
        PreparedStatement st = null;
        ResultSet rs = null;

        try {
            conn = JdbcUtils_DBCP.getConnection();
            String sql = "insert into account(id,name,money) values(?,?,?)";
            st = conn.prepareStatement(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 account where id=?";
            st = conn.prepareStatement(sql);
            st.setInt(1, id);

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

    public void update(Account a) throws SQLException {
        Connection conn = null;
        PreparedStatement st = null;
        ResultSet rs = null;

        try {
            conn = JdbcUtils_DBCP.getConnection();
            String sql = "update account set name=? where id=?";
            st = conn.prepareStatement(sql);
            st.setString(1, a.getName());
            st.setInt(2, a.getId());

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

    public Account find(int id) throws SQLException {
        Connection conn = null;
        PreparedStatement st = null;
        ResultSet rs = null;

        try {
            conn = JdbcUtils.getConnection();
            String sql = "select * from account where id=?";
            st = conn.prepareStatement(sql);
            st.setInt(1, id);

            rs = st.executeQuery();
            if (rs.next()) {
                Account a = new Account();
                // blablabla......

                return a;
            }
            return null;

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

    public List getAll() throws SQLException {

        // blablabla......
        return null;

    }

}

JdbcUtils_DBCP这个类的代码怎么写,可以参考我的笔记开源数据库连接池——DBCP数据源。
这样写代码你是不是要疯了啊!我们可以明显看到:所有实体的增删改操作代码基本相同,仅仅发送给数据库的SQL语句不同而已,因此可以把增删改操作的所有相同代码抽取到工具类的一个update方法中,并定义参数接收变化的SQL语句。

封装通用的update方法和qurey方法

现在我们先来抽取增删改的公共代码 。定义一个JdbcUtils工具类,工具类负责获取数据库连接,释放资源,执行SQL的update操作,代码如下:

public class JdbcUtils {

    private static DataSource ds = null;

    // 静态代码块只执行一次,因为静态代码块在类加载时执行,类永远只加载一次
    static {
        // 初始化连接池

        try {
            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(); // 不会将真正的MySQL驱动返回的Connection返回给你
    }

    public static void release(Connection conn, Statement st, ResultSet rs) {

        if (rs!=null) {
            try {
                rs.close(); // 假设throw异常
            } catch (Exception e) {
                e.printStackTrace(); // 只需在后台记录异常
            }
            rs = null; // 假设rs对象没有释放,将其置为null,该对象就变成垃圾,由Java垃圾回收器回收
        }
        if (st!=null) {
            try {
                st.close(); // 假设throw异常
            } catch (Exception e) {
                e.printStackTrace(); // 只需在后台记录异常
            }
            st = null;
        }
        if (conn!=null) {
            try {
                conn.close();
            } catch (Exception e) {
                e.printStackTrace(); // 只需在后台记录异常
            }
        }

    }

    // 抽取增删改的公共代码  
    /*
     * String sql = "insert into account(id,name,money) values(?,?,?)";
     * Object[]{1, "aaa", 10000}
     */
    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);
        }
    }

}

从上面代码可以看到抽取增删改的公共代码还是比较容易的,但最麻烦的就是抽取查询的公共代码,现在来攻克它。
现在我们来抽取查询的公共代码,优化查询,替换掉所有的查询。在JdbcUtils工具类中编写执行SQL语句的通用的query操作。
我们初次编写时,代码一般都会是这样:

// 抽取查询的公共代码,优化查询,替换掉所有的查询
public static Object query(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]);
        }

        rs = st.executeQuery();

        // 写到这儿卡壳了......

    } finally {
        release(conn, st, rs);
    }

}

框架的设计者是不知道要执行的sql语句的,执行该sql语句,拿到结果集,如何对结果集进行处理,框架的设计者也是不知道的。那该怎么办呢?
答:框架的设计者不知道没关系,但使用该框架的人是知道怎么对结果集进行处理的,那就让他去做这种事情。
所以,代码该这样写:框架的设计者(我)对外暴露一个接口,使用该框架的人去实现该接口做这种事情,框架的设计者(我)针对接口进行调用。这是一种设计模式——策略模式
框架的设计者(我)对外暴露一个接口(该接口存放在JdbcUtils.java中):

interface ResultSetHandler {

    public Object handler(ResultSet rs);

}

框架的设计者(我)针对接口进行调用,那么在JdbcUtils工具类中的query方法就变为:

// 抽取查询的公共代码,优化查询,替换掉所有的查询
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();

        return handler.handler(rs);

    } finally {
        release(conn, st, rs);
    }

}

使用该框架的人去实现该接口,对结果集进行处理。那么别人写的TestJdbcFramework类中的find方法就可以简化为:

public Account find(int id) throws SQLException {
    String sql = "select * from account where id=?";
    Object[] params = {id};
    return (Account) JdbcUtils.query(sql, params, new ResultSetHandler() {

        @Override
        public Object handler(ResultSet rs) {
            // blablabla......
            return null;
        }
    });
}

明显发现这样写与以前写毫无区别,也要对结果集进行遍历啊……等等操作!设计出来的框架就是废物!

编写常用的结果集处理器

为了提高框架的易用性,我们可以事先就针对结果集写好一些常用的处理器,比如将结果集转换成bean对象的处理器,将结果集转换成bean对象的list集合的处理器。

BeanHandler——将结果集转换成bean对象的处理器

框架的设计者(我)针对结果集写一个将结果集转换成bean对象的处理器(该类实现了ResultSetHandler接口,同样在JdbcUtils.java内)。

// 框架设计者在编写这个处理器的时候,并不知道把结果集处理到哪个对象里面去,
// 框架的设计者不知道没关系,但使用该框架的人总该知道,到时候在使用这个结果集处理器时传给框架设计者
class BeanHandler implements ResultSetHandler {

    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 = clazz.newInstance();

            // 得到结果集的元数据,以获取结果集的信息
            ResultSetMetaData meta = rs.getMetaData();
            int count = meta.getColumnCount();
            for (int i = 0; i < count; i++) {
                String name = meta.getColumnName(i+1); // 获取到结果集每列的列名     id
                Object value = http://www.mamicode.com/rs.getObject(name);     // 1

                // 反射出bean上与列名相应的属性
                Field f = bean.getClass().getDeclaredField(name);
                f.setAccessible(true);
                f.set(bean, value);
            }
            return bean;
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

}

BeanListHandler——将结果集转换成bean对象的list集合的处理器

框架的设计者(我)针对结果集写一个将结果集转换成bean对象的list集合的处理器(该类实现了ResultSetHandler接口,同样在JdbcUtils.java内)。

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 {
            while (rs.next()) {
                Object bean = clazz.newInstance();

                ResultSetMetaData meta = rs.getMetaData();
                int count = meta.getColumnCount();
                for (int i = 0; i < count; i++) {
                    String name = meta.getColumnName(i+1);
                    Object value = http://www.mamicode.com/rs.getObject(name);"hljs-keyword">true);
                    f.set(bean, value);
                }
                list.add(bean);
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        return list;
    }

}

至此,我设计出来的简单的JDBC框架的完整代码为:

public class JdbcUtils {

    private static DataSource ds = null;

    // 静态代码块只执行一次,因为静态代码块在类加载时执行,类永远只加载一次
    static {
        // 初始化连接池

        try {
            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(); // 不会将真正的MySQL驱动返回的Connection返回给你
    }

    public static void release(Connection conn, Statement st, ResultSet rs) {

        if (rs!=null) {
            try {
                rs.close(); // 假设throw异常
            } catch (Exception e) {
                e.printStackTrace(); // 只需在后台记录异常
            }
            rs = null; // 假设rs对象没有释放,将其置为null,该对象就变成垃圾,由Java垃圾回收器回收
        }
        if (st!=null) {
            try {
                st.close(); // 假设throw异常
            } catch (Exception e) {
                e.printStackTrace(); // 只需在后台记录异常
            }
            st = null;
        }
        if (conn!=null) {
            try {
                conn.close();
            } catch (Exception e) {
                e.printStackTrace(); // 只需在后台记录异常
            }
        }

    }

    // 抽取增删改的公共代码  
    /*
     * String sql = "insert into account(id,name,money) values(?,?,?)";
     * Object[]{1, "aaa", 10000}
     */
    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]);
            }

            /*
             * 框架的设计者是不知道要执行的sql语句的,
             * 执行该sql语句,拿到的结果集,如何对结果集进行处理框架的设计者也是不知道的
             * 那该怎么办呢?框架的设计者不知道没关系,但使用该框架的人是知道怎么对结果集进行处理的,
             * 那就让他去做这种事情。
             * 代码该这样写:我对外暴露一个接口,使用该框架的人去实现该接口做这种事情,我针对接口进行调用。这是一种设计模式——策略模式
             */
            rs = st.executeQuery();

            return handler.handler(rs);

        } finally {
            release(conn, st, rs);
        }

    }

}

interface ResultSetHandler {

    public Object handler(ResultSet rs);

}

// 框架设计者在编写这个处理器的时候,并不知道把结果集处理到哪个对象里面去,
// 框架的设计者不知道没关系,但使用该框架的人总该知道,到时候在使用这个结果集处理器时传给框架设计者
class BeanHandler implements ResultSetHandler {

    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 = clazz.newInstance();

            // 得到结果集的元数据,以获取结果集的信息
            ResultSetMetaData meta = rs.getMetaData();
            int count = meta.getColumnCount();
            for (int i = 0; i < count; i++) {
                String name = meta.getColumnName(i+1); // 获取到结果集每列的列名     id
                Object value = rs.getObject(name);     // 1

                // 反射出bean上与列名相应的属性
                Field f = bean.getClass().getDeclaredField(name);
                f.setAccessible(true);
                f.set(bean, value);
            }
            return bean;
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

}

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 {
            while (rs.next()) {
                Object bean = clazz.newInstance();

                ResultSetMetaData meta = rs.getMetaData();
                int count = meta.getColumnCount();
                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;
    }

}

当框架自身提供的结果集处理器不满足用户的要求时,那么用户就可以自己去实现ResultSetHandler接口,编写满足自己业务要求的结果集处理器。
有了上述的JdbcUtils框架之后,针对单个实体对象CRUD操作就非常方便了,TestJdbcFramework类的代码就变为:

// 模拟Dao,公共的代码都应该提取出来
public class TestJdbcFramework {

    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));
    }

}

编写的这个JDBC框架就是模拟Apache的DBUtils框架的实现,下一篇将具体介绍Apache的DBUtils框架。

O-R Mapping简介

什么是O-R Mapping

O-R Mapping即对象关系映射。

常用O-R Mapping映射工具

常用O-R Mapping映射工具有:

  • Hibernate
  • Ibatis
  • Commons DbUtils(只是对JDBC简单封装)
<script type="text/javascript"> $(function () { $(‘pre.prettyprint code‘).each(function () { var lines = $(this).text().split(‘\n‘).length; var $numbering = $(‘
    ‘).addClass(‘pre-numbering‘).hide(); $(this).addClass(‘has-numbering‘).parent().append($numbering); for (i = 1; i <= lines; i++) { $numbering.append($(‘
  • ‘).text(i)); }; $numbering.fadeIn(1700); }); }); </script>

    编写自己的JDBC框架