0x01 前言
前序://www.greatytc.com/p/b5d88389444d
在上述操作中,有许多问题,因此我们将对代码进行重构。问题如下:
每次进行数据库操作都要进行数据库连接,发送数据库连接四要素
driverClassName
,url
,username
,password
用于加载数据库注册驱动,不利于维护,存在硬编码问题。获取数据库连接
connection
我们仅仅需要获得已经连接好的connection
对象,不需要关心如何创建的。每次进行数据库连接都会创建一个
Connection
类的实例,性能开销大。释放数据库连接代码也存在代码重复问题。
拼接数据库语句 SQL 存在硬编码问题,且容易造成 SQL 注入攻击。
DML 操作和 DQL 操作存在重复代码。
根据上述问题,我们提出以下的解决方案。
解决问题一:数据库连接四要素构成 properties 配置文件
db.properties
,并将加载配置文件抽取到工具类DruidUtil
中。解决问题二:抽取数据库连接操作
getConnection()
到工具类DruidUtil
中。解决问题三:工具类
DruidUtil
中,把加载数据库注册驱动的代码放在静态代码块中,加载 JVM 的时候一并加载,只执行一次。Connection
实例由数据库连接池Druid
管理。解决问题四:同样抽取释放资源代码到工具类
DruidUtil
中。解决问题五:采用
PreparedStatement
构建 SQL 语句,采取?
占位符预编译 SQL 语句。解决问题六:将 DML 和 DQL 操作抽取到工具类
JdbcTemplate
工具类中。
Druid 连接池
Druid 是一个用于大数据实时查询和分析的高容错、高性能开源分布式系统,旨在快速处理大规模的数据,并能够实现快速查询和分析。尤其是当发生代码部署、机器故障以及其他产品系统遇到宕机等情况时, Druid 仍能够保持 100% 正常运行。创建 Druid 的最初意图主要是为了解决查询延迟问题,当时试图使用 Hadoop 来实现交互式查询分析,但是很难满足实时分析的需要。而 Druid 提供了以交互方式访问数据的能力,并权衡了查询的灵活性和性能而采取了特殊的存储格式。
0x02 项目结构
dao
包用于访问数据库。domain
包定义操作的实际对象。handler
包封装了查询数据的统一处理操作。test
包用于单元测试。util
包封装数据库连接、释放和提交数据库操作语句的操作。
0x03 具体实现
创建 domain
创建具体操作对象,定义字段 id
,name
,age
,phone
,address
。并在数据库中创建对象表格。
// dao.example.domain.Account
package dao.example.domain;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class Account {
private Long id;
private String name;
private Integer age;
private String phone;
private String address;
}
创建数据库连接配置文件
创建数据库连接配置 properties 文件 db.properties
。
driverClassName=com.mysql.jdbc.Driver
url=jdbc:mysql://127.0.0.1:3306/db
username=root
password=root
利用 Druid 连接池管理 Connection
利用 Druid 管理 Connection 对象,静态代码块加载配置资源,getConnection()
方法获取数据库连接。 releaseSqlConnection()
方法释放数据库操作资源。
导包使用 java.sql.*
下包,非 com.mysql.*
下包。使模板类能统一操作各类数据库,而非只能操作 MySQL 数据库。
// dao.example.util.DruidUtil
package dao.example.util;
import java.io.InputStream;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Properties;
import javax.sql.DataSource;
import com.alibaba.druid.pool.DruidDataSourceFactory;
public class DruidUtil {
// 工具类,私有化无参构造函数
private DruidUtil() {
}
// 静态数据源变量,供全局操作且用于静态代码块加载资源。
private static DataSource dataSource;
// 静态代码块,加载配置文件。
static {
InputStream inStream = Thread.currentThread().getContextClassLoader().getResourceAsStream("db.properties");
Properties properties = new Properties();
try {
properties.load(inStream);
dataSource = DruidDataSourceFactory.createDataSource(properties);
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 创建数据库连接实例
* @return 数据库连接实例 connection
*/
public static Connection getConnection() {
try {
return dataSource.getConnection();
} catch (SQLException e) {
e.printStackTrace();
}
throw new RuntimeException("获取数据库连接异常");
}
/**
* 释放数据库连接 connection 到数据库缓存池,并关闭 rSet 和 pStatement 资源
* @param rSet 数据库处理结果集
* @param pStatement 数据库操作语句
* @param connection 数据库连接对象
*/
public static void releaseSqlConnection(ResultSet rSet, PreparedStatement pStatement, Connection connection) {
try {
if (rSet != null) {
rSet.close();
}
} catch (SQLException e) {
e.printStackTrace();
} finally {
try {
if (pStatement != null) {
pStatement.close();
}
} catch (SQLException e) {
e.printStackTrace();
} finally {
try {
if (connection != null) {
connection.close();
}
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}
}
JDBC 操作模板方法
上述中我们将抽取 DML 和 DQL 操作,我们创建 JdbcTemplate
模板类,模板类中定义 DML 操作方法 update()
, DML 操作方法 query()
。
首先,对于 DML 操作没有返回结果集需要处理,而 DQL 则有返回结果集,如何对返回结果集进行统一、规范的处理是我们需要考虑的问题。我们创建一个 BeanHandler
类进行结果集处理。
返回结果集在项目中可能存在多个类型,因此我们采取泛型创建 BeanHandler
中的处理方法 T handler(ResultSet rSet)
。然而采取泛型之后,在调用 handler(...)
操作的对象具体类型 T
是什么类型就不得而知。因此我们利用构造函数 public BeanHandler(Class<T> clazz)
传入需要操作的具体的类型字节码。就知道所操作的对象的具体类型。
结果集操作模板
// dao.example.handler.IResultSetHandler<T>
package dao.example.handler;
import java.sql.ResultSet;
public interface IResultSetHandler<T> {
T handler(ResultSet rSet);
}
为什么定义接口?在项目开发中,当涉及到业务具体的操作对象后,一切的业务需求都是可能会变化的,定义接口规范,方便维护和拓展,符合开闭原则。
// dao.example.handler.impl.BeanHandler<T>
package dao.example.handler.impl;
import java.beans.BeanInfo;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.sql.ResultSet;
import java.util.ArrayList;
import java.util.List;
import dao.example.handler.IResultSetHandler;
public class BeanHandler<T> implements IResultSetHandler<List<T>> {
// 创建字节码对象
private Class<T> clazz;
// 创建有参构造函数,用于传入具体操作对象的类型
public BeanHandler(Class<T> clazz) {
this.clazz = clazz;
}
/**
* 数据库集操作类
* @param rSet 数据库处理结果集
* @return 数据库结果集 List 集合
*/
@Override
public List<T> handler(ResultSet rSet) {
// 创建 List 用于存放装箱后的对象
List<T> list = new ArrayList<T>();
try {
// 获取类的属性描述符
BeanInfo beanInfo = Introspector.getBeanInfo(clazz, Object.class);
PropertyDescriptor[] descriptors = beanInfo.getPropertyDescriptors();
// 对结果集进行装箱操作
while (rSet.next()) {
T obj = clazz.newInstance();
for (PropertyDescriptor descriptor : descriptors) {
Object value = rSet.getObject(descriptor.getName());
descriptor.getWriteMethod().invoke(obj, value);
}
list.add(obj);
}
} catch (Exception e) {
e.printStackTrace();
}
return list;
}
}
DML & DQL 操作模板
query()
方法中,使用接口作为形参,而实际调用中,传入具体的实现类形参,符合开闭原则。
// dao.example.util.JdbcTemplate
package dao.example.util;
import java.io.InputStream;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Properties;
import javax.sql.DataSource;
import com.alibaba.druid.pool.DruidDataSourceFactory;
public class JdbcTemplate {
// 工具类,私有化无参构造函数
private JdbcTemplate() {
}
/**
* DML 操作模板方法
* @param sql 执行操作的 SQL 语句
* @param arguments SQL 语句参数
*/
public static void update(String sql, Object... arguments) {
// 获取数据库连接 connection
Connection connection = DruidUtil.getConnection();
// 创建数据库语句载体
PreparedStatement pStatement = null;
try {
pStatement = connection.prepareStatement(sql);
// 给预编译好的 sql 语句中的占位符进行赋值
if (arguments != null && arguments.length > 0) {
for (int i = 0; i < arguments.length; i++) {
pStatement.setObject(i + 1, arguments[i]);
}
}
// 执行 SQL 语句
pStatement.executeUpdate();
} catch (SQLException e) {
e.printStackTrace();
} finally {
// 释放数据库连接
DruidUtil.releaseSqlConnection(null, pStatement, connection);
}
}
/**
* DQL 操作模板
* @param sql 执行操作的 SQL 语句
* @param handler 对数据库返回结果集进行装箱的操作类
* @param arguments SQL 语句参数
* @return 返回数据库查询结果集
*/
public static <T> T query(String sql, IResultSetHandler<T> handler, Object... arguments) {
Connection connection = DruidUtil.getConnection();
PreparedStatement pStatement = null;
ResultSet rSet = null;
try {
pStatement = connection.prepareStatement(sql);
if (arguments != null && arguments.length > 0) {
for (int i = 0; i < arguments.length; i++) {
pStatement.setObject(i + 1, arguments[i]);
}
}
rSet = pStatement.executeQuery();
// 调用处理结果集类对数据库查询结果集进行装箱
return handler.handler(rSet);
} catch (SQLException e) {
e.printStackTrace();
} finally {
DruidUtil.releaseSqlConnection(rSet, pStatement, connection);
}
return null;
}
}
具体实现数据库访问层 DAO
定义数据库操作方法。
// dao.example.dao.IAccountDAO
package dao.example.dao;
import java.util.List;
import dao.example.domain.Account;
public interface IAccountDAO {
void save(Account account);
void delete(Long id);
void update(Account account);
Account get(Long id);
List<Account> list();
}
数据库操作方法具体实现。
// dao.example.dao.impl.AccountDAOImpl
package dao.example.dao.impl;
import java.util.List;
import dao.example.dao.IAccountDAO;
import dao.example.domain.Account;
import dao.example.handler.impl.BeanHandler;
import dao.example.util.JdbcTemplate;
public class AccountDAOImpl implements IAccountDAO {
@Override
public void save(Account account) {
String sql = "INSERT INTO t_account(name, age, phone, address) VALUES(?, ?, ?, ?)";
Object[] arguments = new Object[] { account.getName(), account.getAge(), account.getPhone(), account.getAddress() };
JdbcTemplate.update(sql, arguments);
}
@Override
public void delete(Long id) {
String sql = "DELETE FROM t_account WHERE id = ?";
JdbcTemplate.update(sql, id);
}
@Override
public void update(Account account) {
String sql = "UPDATE t_account SET name = ?, age = ?, phone = ?, address = ? WHERE id = ?";
Object[] arguments = new Object[] { account.getName(), account.getAge(), account.getPhone(), account.getAddress(), account.getId() };
JdbcTemplate.update(sql, arguments);
}
@Override
public Account get(Long id) {
String sql = "SELECT id, name, age, phone, address FROM t_account WHERE id = ?";
return JdbcTemplate.query(sql, new BeanHandler<>(Account.class), id).get(0);
}
@Override
public List<Account> list() {
String sql = "SELECT id, name, age, phone, address FROM t_account";
return JdbcTemplate.query(sql, new BeanHandler<>(Account.class));
}
}
单元测试
// dao.example.test.testDAO
package dao.example.test;
import org.junit.Test;
import dao.example.dao.impl.AccountDAOImpl;
import dao.example.domain.Account;
public class testDAO {
@Test
public void testSave() {
new AccountDAOImpl().save(new Account(null, "张三", 25, "18819412345", "广州"));
}
@Test
public void testDelete() {
new AccountDAOImpl().delete(2L);
}
@Test
public void testUpdate() {
new AccountDAOImpl().update(new Account(2L, "李四", 27, "18819412345", "广州"));
}
@Test
public void testGet() {
System.out.println(new AccountDAOImpl().get(2L));
}
@Test
public void testList() {
System.out.println(new AccountDAOImpl().list());
}
}
0x04 小结
此次简单 JDBC 操作,我们需要知道在日后的开发中,需要对代码进行重构。抽取相同操作代码作为模板类,方便日后的维护。解决硬编码问题,将某些资源操作配置写入配置文件中。松耦合、解决硬编码、易维护都是开发当中重中之重。