首页 > 代码库 > 编写自己的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简单封装)
编写自己的JDBC框架