使用JDBC主要有三步:
连接数据库 => 执行SQL语句 => 获得结果
这一篇聊聊如何利用Connection进行CRUD即第二和第三步。期间会引入部分设计模式不断重构代码,最终实现类似JDBCTemplate的模板类,方便其他类继承或组合以提高开发效率。
主要内容:
- 基础示例
- 抽取重复代码(JdbcUtils工具类)
- CRUD
- 异常处理
- 模板方法模式重构
- 策略模式重构
基础示例
整个过程:通过DriverManager得到Connection,由Connection得到PreparedStatement,preparedStatement执行sql返回结果集。
image.png数据库表结构
image.pngpublic class Basic {
@Test
public void testJdbc() throws SQLException, ClassNotFoundException {
// 1.注册驱动
Class.forName("com.mysql.jdbc.Driver");
// 2.建立连接
String url = "jdbc:mysql://192.168.136.128:3306/test";
String user = "root";
String password = "root";
Connection conn = DriverManager.getConnection(url, user, password);
// 3.创建sql模板
String sql = "select * from t_user where id = ?";
PreparedStatement preparedStatement = conn.prepareStatement(sql);
// 4.设置模板参数
preparedStatement.setInt(1, 5);
// 4.执行语句
ResultSet rs = preparedStatement.executeQuery();
// 5.处理结果
while (rs.next()) {
System.out.println(rs.getObject(1) + "\t" + rs.getObject(2) + "\t"
+ rs.getObject(3) + "\t" + rs.getObject(4));
}
// 6.释放资源
rs.close();
preparedStatement.close();
conn.close();
}
}
image.png
抽取重复代码
上面示例代码,有两个问题:
- 获取Connection的步骤太复杂,需要封装
- 资源释放太随意,不够规范
尤其第二点,一定要重视。数据库的连接数是有限的,如果不及时释放,会导致其他请求无法访问。应该把释放资源的操作放在finally中,保证资源一定会被关闭。
Basic
public class Basic {
//请注意,这里抛了异常
@Test
public void testJdbc() throws SQLException {
Connection conn = null;
PreparedStatement ps = null;
ResultSet rs = null;
try {
// 1.获取连接
conn = JdbcUtils.getConnection();
// 2.创建sql模板
String sql = "select * from t_user where id = ?";
ps = conn.prepareStatement(sql);
// 3.设置模板参数
ps.setInt(1, 5);
// 4.执行语句
rs = ps.executeQuery();
// 5.处理结果
while (rs.next()) {
System.out.println(rs.getObject(1) + "\t" + rs.getObject(2) + "\t"
+ rs.getObject(3) + "\t" + rs.getObject(4));
}
} finally {
// 6.释放资源
JdbcUtils.free(rs, ps, conn);
}
}
}
JdbcUtils 1.0版
public class JdbcUtils {
private static Properties props = null;
// 只在JdbcUtils类被加载时执行一次!
static {
// 给props进行初始化,即加载jdbc.properties文件到props对象中
try {
InputStream in = JdbcUtils.class.getClassLoader()
.getResourceAsStream("jdbc.properties");
props = new Properties();
props.load(in);
} catch (IOException e) {
throw new RuntimeException(e);
}
// 加载驱动类
try {
Class.forName(props.getProperty("driver"));
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
}
// 获取连接
public static Connection getConnection() throws SQLException {
// 得到Connection
return DriverManager.getConnection(props.getProperty("url"),
props.getProperty("username"),
props.getProperty("password"));
}
// 释放连接
public static void free(ResultSet rs, Statement st, Connection conn) {
try {
if (rs != null)
rs.close();
} catch (SQLException e) {
e.printStackTrace();
} finally {
try {
if (st != null)
st.close();
} catch (SQLException e) {
e.printStackTrace();
} finally {
if (conn != null)
try {
conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}
}
CRUD
目录结构
image.pngUserDao
public interface UserDao {
int addUser(User user) throws SQLException;
int update(User user) throws SQLException;
int delete(User user) throws SQLException;
User getUser(int Id) throws SQLException;
User findUser(String name, int age) throws SQLException;
}
UserDaoJdbcImpl
public class UserDaoJdbcImpl implements UserDao {
public int addUser(User user) throws SQLException {
Connection conn = null;
PreparedStatement ps = null;
ResultSet rs = null;
try {
conn = JdbcUtils.getConnection();
String sql = "insert into t_user(name,age, birthday) values (?,?,?) ";
ps = conn.prepareStatement(sql);
ps.setString(1, user.getName());
ps.setInt(2, user.getAge());
ps.setDate(3, new java.sql.Date(user.getBirthday().getTime()));
return ps.executeUpdate();
} finally {
JdbcUtils.free(rs, ps, conn);
}
}
public int delete(User user) throws SQLException {
Connection conn = null;
PreparedStatement ps = null;
ResultSet rs = null;
try {
conn = JdbcUtils.getConnection();
String sql = "delete from t_user where id=?";
ps = conn.prepareStatement(sql);
ps.setInt(1, user.getId());
System.out.println(sql);
return ps.executeUpdate(sql);
} finally {
JdbcUtils.free(rs, ps, conn);
}
}
public int update(User user) throws SQLException {
Connection conn = null;
PreparedStatement ps = null;
ResultSet rs = null;
try {
conn = JdbcUtils.getConnection();
String sql = "update t_user set name=?, age=?, birthday=? where id=? ";
ps = conn.prepareStatement(sql);
ps.setString(1, user.getName());
ps.setInt(2, user.getAge());
ps.setDate(3, new java.sql.Date(user.getBirthday().getTime()));
ps.setInt(4, user.getId());
return ps.executeUpdate();
} finally {
JdbcUtils.free(rs, ps, conn);
}
}
public User findUser(String name, int age) throws SQLException {
Connection conn = null;
PreparedStatement ps = null;
ResultSet rs = null;
User user = null;
try {
conn = JdbcUtils.getConnection();
String sql = "select id, name, birthday from t_user where name=? and age=?";
ps = conn.prepareStatement(sql);
ps.setString(1, name);
ps.setInt(2, age);
rs = ps.executeQuery();
while (rs.next()) {
user = new User();
user.setId(rs.getInt("id"));
user.setAge(rs.getInt("age"));
user.setName(rs.getString("name"));
user.setBirthday(rs.getDate("birthday"));
}
} finally {
JdbcUtils.free(rs, ps, conn);
}
return user;
}
public User getUser(int userId) throws SQLException {
Connection conn = null;
PreparedStatement ps = null;
ResultSet rs = null;
User user = null;
try {
conn = JdbcUtils.getConnection();
String sql = "select id, name, age, birthday from t_user where id=?";
ps = conn.prepareStatement(sql);
ps.setInt(1, userId);
rs = ps.executeQuery();
while (rs.next()) {
user = new User();
user.setId(rs.getInt("id"));
user.setAge(rs.getInt("age"));
user.setName(rs.getString("name"));
user.setBirthday(rs.getDate("birthday"));
}
} finally {
JdbcUtils.free(rs, ps, conn);
}
return user;
}
}
DAOTest
public class DAOTest {
public static void main(String[] args) throws SQLException {
UserDao userDao = new UserDaoJdbcImpl();
User user = new User();
user.setAge(19);
user.setName("little ming");
user.setBirthday(new Date());
userDao.addUser(user);
}
}
image.png
异常处理
上面的CRUD并没有捕获异常,而是直接往外抛。这会带来两个后果:
-
SQLException是编译时异常,Service在调用DAO时必须处理异常,否则编译不通过。如何处理?要么继续抛,交给Controller处理(意义不大),要么try catch(Service层代码很臃肿,不美观)。
-
DAO接口有声明异常SQLException,这等于向外界暴露DAO层是JDBC实现。而且针对该接口只能用关系型数据库,耦合度太高了。后期无法切换DAO实现。
比较好的做法是,将SQLException转为运行时异常抛出,Service层可处理也可不处理。
我们需要自定义一个DaoException:
public class DaoException extends RuntimeException {
public DaoException() {
}
public DaoException(String message) {
super(message);
}
public DaoException(Throwable cause) {
super(cause);
}
public DaoException(String message, Throwable cause) {
super(message, cause);
}
}
UserDaoJdbcImpl(捕获编译时异常,转为运行时异常)
public class UserDaoJdbcImpl implements UserDao {
public int addUser(User user) {
Connection conn = null;
PreparedStatement ps = null;
ResultSet rs = null;
try {
conn = JdbcUtils.getConnection();
String sql = "insert into t_user(name,age, birthday) values (?,?,?) ";
ps = conn.prepareStatement(sql);
ps.setString(1, user.getName());
ps.setInt(2, user.getAge());
ps.setDate(3, new java.sql.Date(user.getBirthday().getTime()));
return ps.executeUpdate();
} catch (SQLException e) {
//转为DaoException(运行时异常)抛出,Service层可以不处理
throw new DaoException(e.getMessage(), e);
} finally {
JdbcUtils.free(rs, ps, conn);
}
}
public int delete(User user) {
Connection conn = null;
PreparedStatement ps = null;
ResultSet rs = null;
try {
conn = JdbcUtils.getConnection();
String sql = "delete from t_user where id=?";
ps = conn.prepareStatement(sql);
ps.setInt(1, user.getId());
System.out.println(sql);
return ps.executeUpdate(sql);
} catch (SQLException e) {
throw new DaoException(e.getMessage(), e);
} finally {
JdbcUtils.free(rs, ps, conn);
}
}
public int update(User user) {
Connection conn = null;
PreparedStatement ps = null;
ResultSet rs = null;
try {
conn = JdbcUtils.getConnection();
String sql = "update t_user set name=?, age=?, birthday=? where id=? ";
ps = conn.prepareStatement(sql);
ps.setString(1, user.getName());
ps.setInt(2, user.getAge());
ps.setDate(3, new java.sql.Date(user.getBirthday().getTime()));
ps.setInt(4, user.getId());
return ps.executeUpdate();
} catch (SQLException e) {
throw new DaoException(e.getMessage(), e);
} finally {
JdbcUtils.free(rs, ps, conn);
}
}
public User findUser(String name, int age) {
Connection conn = null;
PreparedStatement ps = null;
ResultSet rs = null;
User user = new User();
try {
conn = JdbcUtils.getConnection();
String sql = "select id, name, birthday from t_user where name=? and age=?";
ps = conn.prepareStatement(sql);
ps.setString(1, name);
ps.setInt(2, age);
rs = ps.executeQuery();
while (rs.next()) {
user.setName(rs.getString("name"));
user.setBirthday(rs.getDate("birthday"));
}
} catch (SQLException e) {
throw new DaoException(e.getMessage(), e);
} finally {
JdbcUtils.free(rs, ps, conn);
}
return user;
}
public User getUser(int userId) {
Connection conn = null;
PreparedStatement ps = null;
ResultSet rs = null;
User user = new User();
try {
conn = JdbcUtils.getConnection();
String sql = "select id, name, age, birthday from t_user where id=?";
ps = conn.prepareStatement(sql);
ps.setInt(1, userId);
rs = ps.executeQuery();
while (rs.next()) {
user.setId(rs.getInt("id"));
user.setAge(rs.getInt("age"));
user.setName(rs.getString("name"));
user.setBirthday(rs.getDate("birthday"));
}
} catch (SQLException e) {
throw new DaoException(e.getMessage(), e);
} finally {
JdbcUtils.free(rs, ps, conn);
}
return user;
}
}
模板方法模式重构
上面的CRUD代码太繁琐了!如果还有StudentDaoJdbcImpl、TeacherDaoJdbcImpl,那么同样的代码要写好多遍。所以,必须要重构。大体思路是:相同的代码抽取到父类AbstractDao。观察UserDao:
image.png增删改除了方法名不同,其他都一样,可以归为一类;查询单独一类。
我们先考虑如何重构增删改方法(以delete为观察点)。
image.png我们发现,只有sql模板和设置模板参数的代码不同。可以把sql和参数抽取成父类方法的形参。
AbstractDao
public abstract class AbstractDao {
// 增删改
public int update(String sql, Object...args) {
Connection conn = null;
PreparedStatement ps = null;
ResultSet rs = null;
try {
conn = JdbcUtils.getConnection();
// sql由调用者传入
ps = conn.prepareStatement(sql);
// 遍历设置模板参数
for (int i = 0; i < args.length; i++){
ps.setObject(i + 1, args[i]);
}
return ps.executeUpdate();
} catch (SQLException e) {
throw new DaoException(e.getMessage(), e);
} finally {
JdbcUtils.free(rs, ps, conn);
}
}
}
UserDaoImpl
public class UserDaoImpl extends AbstractDao implements UserDao {
//增
public int addUser(User user) {
String sql = "insert into t_user(name, age, birthday) values (?,?,?) ";
Object[] args = new Object[]{user.getName(), user.getAge(),
user.getBirthday()};
//调用父类AbstractDao的方法
return super.update(sql, args);
}
//删
public int update(User user) {
String sql = "update t_user set name=?, age=?, birthday=? where id=? ";
Object[] args = new Object[]{user.getName(), user.getAge(),
user.getBirthday(), user.getId()};
return super.update(sql, args);
}
//改
public int delete(User user) {
String sql = "delete from t_user where id=?";
Object[] args = new Object[]{user.getId()};
return super.update(sql, args);
}
//待实现
public User getUser(int Id) {
return null;
}
//待实现
public User findUser(String name, int age) {
return null;
}
}
示意图
image.png也就是说,AbstractDao的作用仅仅是“组装sql语句并执行”,不涉及差异性代码。
UserDao/StudentDao/TeacherDao...等有差异的代码仅仅是sql和参数,交给具体实现类编写。
接下来,我们考虑一下如何抽取查询方法。
查询最麻烦的地方在于结果集映射。比如,UserDaoImpl查询得到User,StudentDaoImpl查询得到Student。
image.png- 返回值:用Object接受
- sql和模板参数:效仿update抽取即可
- 结果集映射:???
我们发现,父类无法制定一个通用代码满足所有子类的结果集映射,因为只有子类自己知道映射规则。所以,我们只能把结果集映射的权利交还给子类去实现。子类如果需要查询,就必须自己实现AbstractDao的rowMapper方法。
AbstractDao
public abstract class AbstractDao {
// 增删改
public int update(String sql, Object...args) {
Connection conn = null;
PreparedStatement ps = null;
ResultSet rs = null;
try {
conn = JdbcUtils.getConnection();
// sql由调用者传入
ps = conn.prepareStatement(sql);
// 遍历设置模板参数
for (int i = 0; i < args.length; i++){
ps.setObject(i + 1, args[i]);
}
return ps.executeUpdate();
} catch (SQLException e) {
throw new DaoException(e.getMessage(), e);
} finally {
JdbcUtils.free(rs, ps, conn);
}
}
//查询
public Object query(String sql, Object[] args) {
Connection conn = null;
PreparedStatement ps = null;
ResultSet rs = null;
try {
conn = JdbcUtils.getConnection();
// sql由调用者传入
ps = conn.prepareStatement(sql);
// 遍历设置模板参数
for (int i = 0; i < args.length; i++)
ps.setObject(i + 1, args[i]);
rs = ps.executeQuery();
Object obj = null;
// 结果集映射,子类必须实现抽象方法rowMapper
if (rs.next()) {
obj = rowMapper(rs);
}
return obj;
} catch (SQLException e) {
throw new DaoException(e.getMessage(), e);
} finally {
JdbcUtils.free(rs, ps, conn);
}
}
// 定义成抽象方法,让子类去实现
abstract protected Object rowMapper(ResultSet rs);
}
UserDaoImpl
public class UserDaoImpl extends AbstractDao implements UserDao {
//增
public int addUser(User user) {
String sql = "insert into t_user(name, age, birthday) values (?,?,?) ";
Object[] args = new Object[]{user.getName(), user.getAge(),
user.getBirthday()};
//调用父类AbstractDao的方法
return super.update(sql, args);
}
//删
public int update(User user) {
String sql = "update t_user set name=?, age=?, birthday=? where id=? ";
Object[] args = new Object[]{user.getName(), user.getAge(),
user.getBirthday(), user.getId()};
return super.update(sql, args);
}
//改
public int delete(User user) {
String sql = "delete from t_user where id=?";
Object[] args = new Object[]{user.getId()};
return super.update(sql, args);
}
//查询
public User getUser(int id) {
String sql = "select id, name, age, birthday from t_user where id=?";
Object[] args = new Object[]{id};
Object user = super.query(sql, args);
return (User) user;
}
//查询
public User findUser(String name, int age) {
String sql = "select id, name, age, birthday from t_user where name=? and age=?";
Object[] args = new Object[]{name, age};
Object user = super.query(sql, args);
return (User) user;
}
//UserDaoImpl的结果集映射器
protected Object rowMapper(ResultSet rs) {
User user = null;
try {
user = new User();
user.setId(rs.getInt("id"));
user.setAge(rs.getInt("age"));
user.setName(rs.getString("name"));
user.setBirthday(rs.getDate("birthday"));
} catch (SQLException e) {
throw new DaoException("mapping error");
}
return user;
}
}
示意图
image.png觉得不好理解?换个角度:
image.png至此,模板方法模式重构结束。但是,代码仍然存在隐患。
策略模式重构
我们来分析一个问题:假设现在UserDao增加了一个新方法:
public interface UserDao {
int addUser(User user);
int update(User user);
int delete(User user);
User getUser(int Id);
User findUser(String name, int age);
//新增查询方法:根据年龄查询
List<User> selectUsers(int age);
}
返回值是List<User>,而UserDaoImpl中实现的映射方法rowMapper()只能封装User对象:
public class UserDaoImpl extends AbstractDao implements UserDao {
//利用rowMapper封装bean
public User getUser(int id) {
String sql = "select id, name, age, birthday from t_user where id=?";
Object[] args = new Object[]{id};
Object user = super.query(sql, args);
return (User) user;
}
//无法利用rowMapper封装List
public List<User> selectUsers(int age) {
String sql = "select id, name, age, birthday from t_user where age=?";
Object[] args = new Object[]{age};
List<User> list = super.query(sql, args);
return list;
}
//UserDaoImpl的结果集映射器
protected Object rowMapper(ResultSet rs) {
User user = null;
try {
user = new User();
user.setId(rs.getInt("id"));
user.setAge(rs.getInt("age"));
user.setName(rs.getString("name"));
user.setBirthday(rs.getDate("birthday"));
} catch (SQLException e) {
throw new DaoException("mapping error");
}
return user;
}
}
也就是说,根据返回值的不同(String,User,List<User>)每个查询方法的“映射规则”有所不同。
image.png应该把“映射规则”单独抽取出来,和sql、args一样,在调用父类方法时传入。比如调用getUser()和selectUser()传入的sql、args不同,映射规则也不同。如此一来,sql模板、模板参数、映射规则都高度灵活,可定制。
image.png但是我们很清楚,所谓的“映射规则”其实就是一个方法,比如原先的rowMapper()方法:
//UserDaoImpl的结果集映射器
protected Object rowMapper(ResultSet rs) {
User user = null;
try {
user = new User();
user.setId(rs.getInt("id"));
user.setAge(rs.getInt("age"));
user.setName(rs.getString("name"));
user.setBirthday(rs.getDate("birthday"));
} catch (SQLException e) {
throw new DaoException("mapping error");
}
return user;
}
在JS中,可以把function作为参数传递,然后在另一个方法内部执行。
image.png但是Java不行...怎么办?
没关系,我们之前学习动态代理时其实早就接触过这种思想:直接传方法不行?那我就把这个方法塞进一个对象里,通过对象去调用方法(把需要代理对象执行的代码写在InvocationHandler对象的invoke方法中,再把invocationHandler塞进代理对象供它调用)。
所以,这次也照做就行了。只不过这种模式其实叫策略模式,而且一般是传入接口的实现类。
image.png好了,现在子类已经不需要实现父类的抽象方法了(一个规则无法满足不同返回值映射),改为由子类实现RowMapper接口传入匿名对象的方式,所以AbstractDao中的抽象方法可以删除。也就是说AbstractDao已经没有抽象方法了。于是我把它声明为普通类(可以new),并改名为MyJDBCTemplate。而且,使用MyJDBCTemplate时,我决定不再使用继承,而是选择组合方式(组合比继承灵活)。
image.pngRowMapper
public interface RowMapper {
//映射结果集
Object mapRow(ResultSet rs) throws SQLException;
}
MyJDBCTemplate
public class MyJDBCTemplate {
// 增删改
public int update(String sql, Object...args) {
Connection conn = null;
PreparedStatement ps = null;
ResultSet rs = null;
try {
conn = JdbcUtils.getConnection();
// sql由调用者传入
ps = conn.prepareStatement(sql);
// 遍历设置模板参数
for (int i = 0; i < args.length; i++){
ps.setObject(i + 1, args[i]);
}
return ps.executeUpdate();
} catch (SQLException e) {
throw new DaoException(e.getMessage(), e);
} finally {
JdbcUtils.free(rs, ps, conn);
}
}
//查询,RowMapper是接口类型,需要传入接口的实现类对象
public List<Object> query(String sql, Object[] args, RowMapper rowMapper) {
Connection conn = null;
PreparedStatement ps = null;
ResultSet rs = null;
List list = new ArrayList();
try {
conn = JdbcUtils.getConnection();
// sql由调用者传入
ps = conn.prepareStatement(sql);
// 遍历设置模板参数
for (int i = 0; i < args.length; i++)
ps.setObject(i + 1, args[i]);
rs = ps.executeQuery();
Object obj = null;
// 映射规则由子类传入
while (rs.next()) {
Object o = rowMapper.mapRow(rs);
list.add(o);
}
return list;
} catch (SQLException e) {
throw new DaoException(e.getMessage(), e);
} finally {
JdbcUtils.free(rs, ps, conn);
}
}
}
UserDaoImpl
public class UserDaoImpl implements UserDao {
MyJDBCTemplate jdbcTemplate = new MyJDBCTemplate();
//增
public int addUser(User user) {
String sql = "insert into t_user(name, age, birthday) values (?,?,?) ";
Object[] args = new Object[]{user.getName(), user.getAge(),
user.getBirthday()};
//调用jdbcTemplate的update方法
return jdbcTemplate.update(sql, args);
}
//删
public int update(User user) {
String sql = "update t_user set name=?, age=?, birthday=? where id=? ";
Object[] args = new Object[]{user.getName(), user.getAge(),
user.getBirthday(), user.getId()};
//调用jdbcTemplate的update方法
return jdbcTemplate.update(sql, args);
}
//改
public int delete(User user) {
String sql = "delete from t_user where id=?";
Object[] args = new Object[]{user.getId()};
//调用jdbcTemplate的update方法
return jdbcTemplate.update(sql, args);
}
//查询单个
public User getUser(int id) {
String sql = "select id, name, age, birthday from t_user where id=?";
Object[] args = new Object[]{id};
//调用jdbcTemplate的query方法,传入sql,args,RowMapper匿名对象
List list = jdbcTemplate.query(sql, args, new RowMapper() {
public Object mapRow(ResultSet rs) throws SQLException {
User user = new User();
user.setId(rs.getInt("id"));
user.setAge(rs.getInt("age"));
user.setName(rs.getString("name"));
user.setBirthday(rs.getDate("birthday"));
return user;
}
});
return (User)list.get(0);
}
public User findUser(String name, int age) {
String sql = "select id, name, age, birthday from t_user where name=? and age=?";
Object[] args = new Object[]{name, age};
//调用jdbcTemplate的query方法,传入sql,args,RowMapper匿名对象
List list = jdbcTemplate.query(sql, args, new RowMapper() {
public Object mapRow(ResultSet rs) throws SQLException {
User user = new User();
user.setId(rs.getInt("id"));
user.setAge(rs.getInt("age"));
user.setName(rs.getString("name"));
user.setBirthday(rs.getDate("birthday"));
return user;
}
});
return (User)list.get(0);
}
//查询多个
public List selectUsers(int age) {
String sql = "select id, name, age, birthday from t_user where age=?";
Object[] args = new Object[]{age};
//调用jdbcTemplate的query方法,传入sql,args,RowMapper匿名对象
List list = jdbcTemplate.query(sql, args, new RowMapper() {
public Object mapRow(ResultSet rs) throws SQLException {
User user = new User();
user.setId(rs.getInt("id"));
user.setAge(rs.getInt("age"));
user.setName(rs.getString("name"));
user.setBirthday(rs.getDate("birthday"));
return user;
}
});
return list;
}
}
测试
image.png当然,上面的所谓JDBCTemplate是非常粗糙的,还存在很多不严谨的地方,就当是个玩具。有兴趣的朋友可以去看看Spring的JDBCTemplate。
网友评论