编写自己的JDBC框架

Posted 李阿昀

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了编写自己的JDBC框架相关的知识,希望对你有一定的参考价值。

元数据介绍

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

DatabaseMetaData元数据

Connection.getMetaData()获得代表DatabaseMetaData元数据的DatabaseMetaData对象。DatabaseMetaData对象的常用方法有:
在这里插入图片描述
例,

package cn.liayun.demo;

import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.SQLException;

import org.junit.Test;

import cn.liayun.utils.JdbcUtils_C3P0;

public class Demo5 {
	
	/**
	 * 获取数据库的元数据
	 * @param args
	 * @throws SQLException 
	 */
	@Test
	public void test1() throws SQLException {
		Connection conn = JdbcUtils_C3P0.getConnection();
		DatabaseMetaData meta = conn.getMetaData();
		System.out.println(meta.getDatabaseProductName());//mysql
		System.out.println(meta.getDatabaseMajorVersion());//5
		System.out.println(meta.getDatabaseMinorVersion());//7
	}

}

JdbcUtils_C3P0这个类的代码怎么写,可以参考我的笔记《Java Web基础入门第五十八讲 开源数据库连接池》。test1方法运行结果如下:
在这里插入图片描述

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

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

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

我们要想知道参数的个数和每一个参数的类型,就得用到ParameterMetaData元数据。ParameterMetaData对象的常用方法有:
在这里插入图片描述
例,

package cn.liayun.demo;

import java.sql.Connection;
import java.sql.ParameterMetaData;
import java.sql.PreparedStatement;
import java.sql.SQLException;

import org.junit.Test;

import cn.liayun.utils.JdbcUtils_C3P0;

public class Demo5 {
	
	/**
	 * 获取参数的元数据
	 * @param args
	 * @throws SQLException 
	 */
	@Test
	public void test2() throws SQLException {
		Connection conn = JdbcUtils_C3P0.getConnection();
		String sql = "insert into user(id,name) values(?,?)";
		
		PreparedStatement st = conn.prepareStatement(sql);
		ParameterMetaData meta = st.getParameterMetaData();
		System.out.println(meta.getParameterCount());//sql语句需要几个参数?
		System.out.println(meta.getParameterType(1));//想要知道sql语句的第1个参数的类型,但MySQL驱动不支持获取参数的类型
	}

}

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

ResultSet.getMetaData()获得代表ResultSet对象元数据的ResultSetMetaData对象。ResultSetMetaData对象的常用方法有:
在这里插入图片描述
例,

package cn.liayun.demo;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;

import org.junit.Test;

import cn.liayun.utils.JdbcUtils_C3P0;

public class Demo5 {
	/**
	 * 获取结果集的元数据
	 * @param args
	 * @throws SQLException 
	 */
	@Test
	public void test3() 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));
	}
}

使用元数据封装简单的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数据库打交道的代码都是这样写的:

package cn.liayun.utils;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.List;

import cn.liayun.domain.Account;

// 模拟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这个类的代码怎么写,也可以参考我的笔记《Java Web基础入门第五十八讲 开源数据库连接池》。这样写代码你是不是要疯了啊!我们可以明显看到:所有实体的增删改操作代码基本相同,仅仅是发送给数据库的SQL语句不同而已,因此可以把增删改操作的所有相同代码抽取到工具类的一个update方法中,并定义参数接收变化的SQL语句。

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

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

package cn.liayun.utils;

import java.io.InputStream;
import java.sql.Connection;
import java.sql.PreparedStatement;
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 {
	
	private static DataSource ds = null;
	
	//在静态代码块里面初始化DBCP链接池
	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();
			} 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();
			}
			conn = null;
		}
	}
	
	//以下方法用来抽取增删改的公共代码
	/*
	 * 别人调用以下方法时,会传进来如下参数:
	 * String sql = "insert into account(id,name,money) values(?,?,?)";
	 * Object[]{4,"liayun",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);
		}
	}
	
}

从上面代码可以看到抽取增删改的公共代码还是比较容易的,但最麻烦的就是抽取查询的公共代码,现在来攻克它。
下面,我们来抽取查询的公共代码,优化查询,替换掉所有的查询。在JdbcUtils工具类中编写执行SQL语句的通用的query操作。萌新们初次编写时,代码一般都会是这样:
在这里插入图片描述
框架的设计者是不知道要执行的sql语句的,执行该sql语句,拿到结果集,如何对结果集进行处理,框架的设计者也是不知道的,那该怎么办呢?框架的设计者不知道没关系,但使用该框架的人是知道怎么对结果集进行处理的,那就让他去做这种事情。所以,代码该这样写:框架的设计者(我)对外暴露一个接口,使用该框架的人去实现该接口做这种事情,框架的设计者(我)针对接口进行调用。这即是一种设计模式,叫策略模式。
首先,框架的设计者(我)对外暴露一个接口(该接口存放在JdbcUtils工具类中):
在这里插入图片描述
然后,框架的设计者(我)针对接口进行调用,那么在JdbcUtils工具类中的query方法就变为:
在这里插入图片描述
使用该框架的人去实现ResultSetHandler接口,并对结果集进行处理。那么别人写的TestJdbcFramework类中的find方法就可以简化为:
在这里插入图片描述
明显发现这样写与以前写毫无区别,也要对结果集进行遍历啊……等等操作!设计出来的框架就是废物!

编写常用的结果集处理器

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

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

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

/*
 * 框架设计者在编写这个处理器的时候,并不知道把结果集处理到哪个对象里面去,
 * 框架的设计者不知道没关系,但使用该框架的人总该知道吧,到时候他在使用这个结果集处理器时传递给框架设计者即可。
 */
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);//获取到结果集每列的列名
				Object value = rs.getObject(name);//获取到结果集每列的值(id-1)
				
				//反射出bean上与列名相应的属性
				Field f = bean.getClass().getDeclaredField(name);
				f.setAccessible(true);
				f.set(bean, value);//取到每列的数据,并把每列的数据全部整到bean上与列名相应的属性上面去了
			}
			return bean;
		} catch (Exception e) {
			throw new RuntimeException(e);
		}
	}
	
}

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

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

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

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

package cn.liayun.utils;

import java.io.InputStream;
以上是关于编写自己的JDBC框架的主要内容,如果未能解决你的问题,请参考以下文章

编写自己的JDBC框架

day16 事务 - 数据库连接池 - 编写自己的jdbc框架

简单的 jdbc 包装器

JDBC-06-笔记

创建自己的代码片段(CodeSnippet)

SSH框架的基本整合