优雅的JDBC写法

介绍下在数据库的相关操作中什么是元数据,以及如果自己写工具类的时候应该如何写,或者可以使用别人写好的工具类,比如 Apache 的 dbutils 工具类,就是不知道现在是不是还在用,也当作是为以后学习框架做准备吧!

元数据

首先,来了解下什么是元数据,当然主要说的是数据库相关的,简单说它就是:数据库、表、列的定义信息
所以,指的基本就是数据库一些“对象”的信息,基本可以分为三类

  • DatabaseMetaData
    数据库的元数据,封装了数据库的相关信息,比如版本、名字、驱动、URL、用户名等等
  • ParameterMetaData
    参数元数据,简单说就是:获得预编译 SQL 语句中 “?” 信息。比如:参数的个数、类型等,但是并不是所有的数据库(驱动)都支持
  • ResultSetMetaData
    结果集的元数据,封装了结果集中的列数、列名称、列类型等信息

工具类写法

下面就来优雅的写 JDBC 的工具类,嗯….起码算比较优雅

以前我们在工具类就写了两个方法,一个是获取连接,一个是释放相关资源;下面就来扩展一下,让其变得更加通用一些
增加两个方法,一个用于增删改(update),一个用于查询(query)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public static void update(String sql, Object[] params) throws SQLException {
Connection conn = null;
PreparedStatement st = 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 {
JdbcUtils.release(conn, st, null);
}
}

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 {
JdbcUtils.release(conn, st, rs);
}

}

基本思路应该看出来了,增删改的时候统一调用 update 方法,传入相应的 SQL 语句和参数数组就可以了;
然后查询的时候单独一个方法 query ,除了需要 SQL 和参数数组还要传入一个 ResultSetHandler 对象,这个是个接口,是自己定义的,主要用于回调来处理结果集,因为 SQL 语句不确定,结果集也不确定如何处理,最妥的方式就是交给用户处理,既然是你写的 SQL 所以你肯定知道怎么处理结果集嘛!

1
2
3
public interface ResultSetHandler {
Object handler(ResultSet rs);
}

当然,如果让用户去实现接口多少还是有些麻烦的,为了更好的用户体验我们可以给用户准备几个常用的,比如把查询出来的数据封装到一个 Javabean 中去:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public class BeanHandler implements ResultSetHandler {
private Class<SimpleTab> clazz;

public BeanHandler(Class<SimpleTab> clazz) {
this.clazz = clazz;
}

@Override
public Object handler(ResultSet rs) {
try {
if (rs == null || !rs.next()) {
return null;
}
Object bean = clazz.newInstance();
// 得到结果集的元数据
ResultSetMetaData metaData = rs.getMetaData();
int count = metaData.getColumnCount();
for (int i = 0; i < count; i++) {
// 获取每一列的名字
String name = metaData.getColumnName(i + 1);
// 获取每一列的值
Object value = rs.getObject(name);

// 利用反射技术得到相应的属性,使数据拷贝到 javabean 里
Field field = bean.getClass().getDeclaredField(name);
field.setAccessible(true);
field.set(bean, value);
}
return bean;
} catch (InstantiationException | IllegalAccessException | SQLException | NoSuchFieldException e) {
throw new RuntimeException(e);
}
}
}

这里就用到了之前说的元数据,还使用到了反射技术,调用的时候直接 new 这个实现类就可以了,传入一个 class 用来指定 bean 的类型;
当然如果查询返回了多条记录,一般是封装到 List 集合中去,所以再写一个实现类吧:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class BeanListHandler implements ResultSetHandler {
private Class clazz;

public BeanListHandler(Class clazz) {
this.clazz = clazz;
}

@Override
public Object handler(ResultSet rs) {
List<Object> mList = new ArrayList<>();
try {
while (rs.next()) {
Object bean = clazz.newInstance();
ResultSetMetaData metaData = rs.getMetaData();
for (int i = 0; i < metaData.getColumnCount(); i++) {
String name = metaData.getColumnName(i + 1);
Object value = rs.getObject(name);

// 反射,使用 Declared 说明获取的是私有变量
Field field = bean.getClass().getDeclaredField(name);
field.setAccessible(true);
field.set(bean, value);
}
mList.add(bean);
}
return mList;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}

然后调用的时候就很简单了:

1
2
3
4
5
6
7
8
9
10
private static void extendedTest2() throws SQLException {
String sql = "SELECT * FROM simpletab";
Object[] params = {};
List list = (List) JdbcUtils.query(sql, params, new BeanListHandler(SimpleTab.class));
System.out.println(list.size());

// String sql = "insert INTO simpletab VALUES (?,?)";
// Object[] params = {2, "测试"};
// JdbcUtils.update(sql,params);
}

使用DBUtils

上面的工具类是我们自己定义的,然后 Apache 其实也写了一个,就叫 dbutils 不过我看了下最新的也已经是几年前了,不知道在实际开发中还用不用,不过嘛,这种工具类应该一般也不需要更新
dbutils 的使用和自定义的工具类很类似,实现原理可能也差不多
它只是对 JDBC 进行简单封装,学习成本低,并且不会影响程序的性能。它不是一个框架

简单使用

使用很简单,毕竟就是为了简化开发,导入相应的包后直接用就行了,主要用到的是 QueryRunner 对象,具体的方法可以去看 API,说点常用的,比如执行增删改操作:

1
2
3
4
5
6
private static void test1() throws SQLException {
QueryRunner qr = new QueryRunner(JdbcUtilsC3P0.getDataSource());
String sql = "INSERT INTO simpletab(id,name) VALUES (?,?)";
Object[] param = {3, "hello"};
qr.update(sql, param);
}

和我们自己写的非常相似,new 的时候接收一个 conn 对象,直接从连接池里取了
再说查询,也是支持封装到 bean 和 List 中去,就举个封装到 List 中的栗子:

1
2
3
4
5
6
private static void test2() throws SQLException {
QueryRunner qr = new QueryRunner(JdbcUtilsC3P0.getDataSource());
String sql = "SELECT * FROM simpletab";
List<SimpleTab> list = qr.query(sql, new BeanListHandler<>(SimpleTab.class));
System.out.println(list);
}

dbutils 还给了一些实现了 ResultSetHandler 的类,这里不多说了 API 写的很明白,比如可以封装到数组中去。
如果返回的是单条数据可以使用 ScalarHandler 对象,用于获取结果集中第一行某列的数据并转换成 T (指定的泛型)表示的实际对象,更多的返回类型可以参考:http://www.cnblogs.com/myit/p/4272824.html

批量操作

就是批量执行 SQL 了,当然是相同的 sql 类型,比如可以批量删除、添加

1
2
3
4
5
6
7
8
9
private static void test3() throws SQLException {
QueryRunner qr = new QueryRunner(JdbcUtilsC3P0.getDataSource());
String sql = "INSERT INTO simpletab(id,name) VALUES (?,?)";
Object[][] params = new Object[5][];
for (int i = 0; i < 5; i++) {
params[i] = new Object[]{i + 4, "test" + i};
}
qr.batch(sql, params);
}

主要区别就是参数的一维数组换成了二维数组,也就是说数组中的每一个数组中封装了批量操作中的一条数据,然后执行 batch 方法执行

事务操作

事务这是一个重要的操作,就当是操作吧,既然要使用事务,那么就不能让它执行完一条 SQL 就关闭连接了,所以只能我们自己传入 conn 了,并且是关闭了自动提交的 conn

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
private static void test4() throws SQLException {
QueryRunner qr = new QueryRunner();
Connection conn = null;

try {
conn = JdbcUtilsC3P0.getConnection();
conn.setAutoCommit(false);

String sql1 = "UPDATE simpletab SET name='loli' WHERE id=1";
qr.update(conn, sql1);

String sql2 = "UPDATE simpletab SET name='loli' WHERE id=2";
qr.update(conn, sql2);

conn.commit();
} finally {
if (conn != null) {
conn.close();
}
}
}

当然这样的写法是有问题的,如果是按照三层架构开发的话,dao 层是不允许有业务逻辑的,只能有执行 sql 的相关代码,类似这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 由构造方法获得
private static Connection conn = null;

private static int updateExample(SimpleTab bean) throws SQLException {
QueryRunner qr = new QueryRunner();
String sql = "UPDATE simpletab SET name=? WHERE id=?";
Object[] param = {bean.getName(), bean.getId()};
return qr.update(conn, sql, param);
}

private static SimpleTab findExample(int id) {
try {
QueryRunner qr = new QueryRunner();
String sql = "SELECT * FROM simpletab WHERE id=?";
Object[] param = {id};
return qr.query(conn, sql, new BeanHandler<>(SimpleTab.class), param);
} catch (SQLException e) {
throw new RuntimeException(e);
}
}

这样写的话,如果使用到事务,在 Server 层的时候获取连接后关闭自动提交,然后再传进来,最后还要记得手动 commit 和 close

但是这样写还是不太好,更优雅的写法可以考虑使用 ThreadLocal ,将 conn 绑定到线程上,这样也不用考虑高并发的问题了

使用ThreadLocal优化工具类

ThreadLocal 可以简单理解为线程管理数据的类,他有两个经常用的方法,get、set 都是静态的,分别表示着往当前线程获取、保存数据
下面就来改造下工具类,主要是添加几个管理 conn 的方法,其他方法不变,就省略了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
public class JdbcUtilsC3P0 {
private static ComboPooledDataSource ds = null;
// 设置为静态,随类加载而创建,内部其实是一个 Map 集合
private static ThreadLocal<Connection> tl = new ThreadLocal<>();

......

public static Connection getConnectionThread() {
try {
// 便于 Dao 层的获取,一般来说获取的到的是开启事务的连接,获取不到就从连接池拿一个普通的
// 得到当前线程上绑定的连接
Connection conn = tl.get();
// 判断是否为空,如果为空就从连接池获取一个,绑定到当前线程
if (conn == null) {
conn = getConnection();
tl.set(conn);
}
return conn;
} catch (SQLException e) {
throw new RuntimeException(e);
}
}

// 便于 Server 层获取
public static void startTransaction() {
try {
// 得到当前线程上绑定的连接
Connection conn = tl.get();
// 判断是否为空,如果为空就从连接池获取一个,绑定到当前线程
if (conn == null) {
conn = getConnection();
tl.set(conn);
}
conn.setAutoCommit(false);
} catch (SQLException e) {
throw new RuntimeException(e);
}
}

public static void commintTransaction() {
try {
Connection conn = tl.get();
if (conn != null) {
conn.commit();
}
} catch (Exception e) {
e.printStackTrace();
}
}

public static void closeConnection() {
try {
Connection conn = tl.get();
if (conn != null) {
conn.close();
}
} catch (SQLException e) {
e.printStackTrace();
}finally {
// 千万记得释放资源,否则 Map 集合会越来越大;只会移除当前线程的
tl.remove();
}
}
}

这样改造后基本就比较优雅了,效率也比较好了

多表操作

这个也不是太难,大体思路是:接受一个 bean 根据里面的具体数据来构造相应的 sql ,比如如果传入的 bean 包含有多个子 bean ;其实对应的就是一对多的关系,先把主 bean 中的基本数据存进去,再把子 bean 拆出来一个个的存,最后加上外键连接起来就 OK 了
当然每一个 bean 都应该对应一个具体的 dao 层操作的方法

PS:set 集合的 addAll() 方法可以把传入的集合拆了,然后再存进自己的 set

代码就不写了….

喜欢就请我吃包辣条吧!

评论框加载失败,无法访问 Disqus

你可能需要魔法上网~~