JavaEE中的MVC(一)Dao层彻底封装

时间:2022-07-01 09:13:30

注:这是一个“重复造*”的过程,本文简单地实现了一个ORM框架

最近Android工作实在难找,考虑是不是该转行做Java了,今天开始,花几天的事件,研究一下JavaEE各层优化。

本文介绍的是Dao的优化,目前,像是Hibernate、Mybatis等框架都属于ORM框架,ORM是关系映射的意思;

在我们使用这些框架的时候,我们都需要去写配置文件,类名对应于哪个表,成员变量对应于哪个列等等;

在这些框架工作的时候,要先读取这些配置文件,然后根据文件中的映射关系,帮我们动态地去拼接SQL,或者自动地将数据打包成JavaBean。

增删改方法封装

使用PreparedStatement执行一条Sql语句的流程如下:

  1. 首先,Sql语句通常会有这么几种情况:
    ①更新语句:UPDATE accounts SET pwd=? WHERE (id=?),
    ②插入语句:INSERT INTO accounts ( pwd, account, addTime) VALUES (?,?,?)
    ③删除语句:DELETE FROM accounts WHERE (id=?)
  2. 有了这些Sql语句之后,我们会调用Connection.prepareStatement(sql)方法;
  3. 然后依次调用PreparedStatement的set方法;
  4. 最后执行executeUpdate()方法。

这个流程有几个共同的特点:

  1. 这几个查询语句的执行结果都可以使用Boolean值表示;
  2. 参数的设置,都是调用PreparedStatement的set方法,查看API,可以看到PreparedStatement有一个setObject()方法,因为参数是Object,也就是说,PreparedStatement的set方法都可以使用setObject()替代;

代码封装

根据上面的说法,就可以实现下面这样的封装,一个能执行任何增删改Sql语句的方法:

    protected boolean executeUpdates(String sql, Object... params) throws SQLException {
pstmt = conn.prepareStatement(sql);
for (int i = 0; i < params.length; i++) {
pstmt.setObject(i + 1, params[i]);
}
return pstmt.executeUpdate() > 0;
}

查询方法封装

查询语句之所以不同于其它方法,原因是有一个ResultSet需要返回,ResultSet是一个需要被关闭的对象,怎么处理ResultSet?

  • 思路一:对ResultSet进行二次封装(目前我已经实现,代码相对复杂,这里就不具体展开,有兴趣可以一起讨论);
  • 思路二:接口回调,或者方法回调。

这里就采用方法回调。

ResultSetParser接口设计

public interface ResultSetParser<T> {
/**
* 处理结果集
*
* @param rs
* ResultSet
* @return List<T>
*/
List<T> parse(ResultSet rs); /**
* 处理结果集
*
* @param rs
* ResultSet
* @return Object
*/
Object simpleParse(ResultSet rs);
}

Parser方法回调实现类

这个类实现了ResultSetParser接口,但是方法都没真正实现,只是写了空方法,由真正的子类去实现

public abstract class Parser<T> implements ResultSetParser<T> {
@Override
public List<T> parse(ResultSet resultSet) {
return null;
} @Override
public Object simpleParse(ResultSet resultSet) {
return null;
}
}

代码封装

于是,就有了下面这样的封装,一个能执行任何查询Sql语句的方法,其中参数ResultSetParser由调用者做具体的实现:

    protected List<T> executeQuerys(ResultSetParser<T> resultSetPaser, String sql, Object... params) throws Exception {
pstmt = conn.prepareStatement(sql);
for (int i = 0; i < params.length; i++) {
pstmt.setObject(i + 1, params[i]);
}
rs = pstmt.executeQuery();
return resultSetPaser.parse(rs);
}

实现类BaseDao最终封装(源码)

实际取名是DBHelper,为什么不取名BaseDao,因为最初的思路,我是将其设计为工具类,并不是非得继承才可以使用。

public class DBHelper<T> {
private Connection conn = null;
private PreparedStatement pstmt = null;
private ResultSet rs = null;
// 开启事务标志
private boolean autoCommit = true; /**
* 核心方法,开启事务
*/
public void beginTransaction() {
autoCommit = false;
} /**
* 核心方法,提交事务
*/
public void commit() {
try {
conn.commit();
autoCommit = true;
} catch (SQLException e) {
e.printStackTrace();
}
} /**
* 核心方法,获取数据库连接
*/
private void begin() {
try {
conn = ConnectionPool.getConnection();
if (autoCommit)
return;
conn.setAutoCommit(false);
} catch (Exception e) {
e.printStackTrace();
}
} /**
* 核心方法:释放资源
*/
private void close(ResultSet resultSet, Statement statement, Connection connection) {
try {
if (resultSet != null)
resultSet.close();
if (statement != null)
statement.close();
if (connection != null)
connection.close();
} catch (Exception e) {
e.printStackTrace();
}
} /**
* 核心方法:为PreparedStatement设置参数
*
* @param pstmt
* PreparedStatement
* @param params
* 参数
*/
private void setParams(PreparedStatement pstmt, Object... params) {
try {
for (int i = 0; i < params.length; i++) {
pstmt.setObject(i + 1, params[i]);
}
} catch (SQLException e) {
e.printStackTrace();
}
} /**
* 增加、删除、修改的统一方法
*
* @param sql
* SQL语句
* @param params
* 参数
*/
protected boolean executeUpdate(String sql, Object... params) {
begin();
try {
pstmt = conn.prepareStatement(sql);
setParams(pstmt, params);
return pstmt.executeUpdate() > 0;
} catch (SQLException e) {
e.printStackTrace();
} finally {
close(rs, pstmt, conn);
}
return false;
} /**
* 查询的统一方法
*
* @param rsUtil
* 处理结果集的接口
* @param sql
* 语句
* @param params
* 参数
* @return List<T>
*/
protected List<T> executeQuery(ResultSetParser<T> rsUtil, String sql, Object... params) {
begin();
try {
pstmt = conn.prepareStatement(sql);
setParams(pstmt, params);
rs = pstmt.executeQuery();
return rsUtil.parse(rs);
} catch (SQLException e) {
e.printStackTrace();
} finally {
close(rs, pstmt, conn);
}
return null;
} /**
* 查询的统一方法
*
* @param rsUtil
* 处理结果集的接口
* @param sql
* 语句
* @param params
* 参数
* @return List<T>
*/
protected Object executeSimpleQuery(ResultSetParser<T> resultSetPaser, String sql, Object... params) {
begin();
try {
pstmt = conn.prepareStatement(sql);
setParams(pstmt, params);
rs = pstmt.executeQuery();
return resultSetPaser.simpleParse(rs);
} catch (SQLException e) {
e.printStackTrace();
} finally {
close(rs, pstmt, conn);
}
return null;
}
}

BaseDao投入实战

我们需要一个AccountsDao,只要去继承BaseDao就好了,假如我们有个添加一个Accounts对象的需求,代码变得异常地简单,如下所示,仅仅只需要两行。

public class AccountsDao extends DBHelper<Accounts> {
public void insert(Accounts accounts) {
String sql = "INSERT INTO `accounts` (`integral`, `pwd`, `account`, `addTime`, `login`, `money`, `isEnable`) VALUES (?,?,?,?,?,?,?)";
super.executeUpdate(sql, accounts.getIntegral(), accounts.getPassword(), accounts.getAccount(),
accounts.getAddTime(), accounts.getLogin(), accounts.getMoney(), accounts.getIsEnable());
}
}

利用反射机制设计万能Dao

其实看到上面这一串代码,可能还是略显蛋疼,假如说我们一张表有20列,这个时候去写一个Sql语句,那真的要疯了,你要写20个?号,如果有Where子句,还需要更多。

我的思路是采用反射机制来做,设计一个Javabean,他的类名和字段都和数据库的相匹配,利用反射拼出Sql语句。

因为算法的关系,这肯定会牺牲一定的查询效率,但是可以完成数据连接层的彻底封装。

注意:使用注解、反射、配置文件,都会浪费一定的时间去解析,因此,最好可以去考虑设计一个缓存域,用于缓存已经查询的数据,也可以考虑缓存反射生成的Sql语句。

测试用Javabean

public class Accounts {
private long id;
private long integral;
private String pwd;
private String account;
private Timestamp addTime;
private Timestamp login;
private int money;
private boolean isEnable;
//方法补齐...

数据库对应表

JavaEE中的MVC(一)Dao层彻底封装

Dao实现

/**
* 利用反射机制设计Dao
*
* @author CSS 2016/12/1
* @version 1.0
*
*/
public class EasyDao<T> extends DBHelper<T> {
/**
* 分页查询
* @param begin 开始位置
* @param count 取多少行记录
* @return
*/
public List<T> getList(Class<T> cl, int begin, int count) {
StringBuffer sql = new StringBuffer(120);
sql.append("select * from ").append(cl.getSimpleName());
if (begin != -1)
sql.append(" LIMIT ?");
if (count != -1)
sql.append(",?");
System.out.println(sql.toString()); return executeQuery(new Parser<T>() {
@Override
public List<T> parse(ResultSet resultSet) {
return fillArrayList(cl, resultSet);
}
}, sql.toString(), begin, count);
} protected List<T> fillArrayList(Class<T> clazz, ResultSet resultSet) {
List<T> list = new ArrayList<T>();
try {
Field[] fields = clazz.getDeclaredFields();
resultSet.beforeFirst();
while (resultSet.next()) {
T t = (T) clazz.newInstance(); for (int i = 0; i < fields.length; i++) {
fields[i].setAccessible(true);
fields[i].set(t, resultSet.getObject(fields[i].getName()));
}
list.add(t);
}
} catch (Exception e) {
e.printStackTrace();
}
return list;
}
}

在万能Dao中引入注解的使用

设计注解接口

/**
* 指明字段在数据库中对应的列名
* @author ChenSS
*
*/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Column {
String value();
}

在Javabean中使用注解

public class Accounts {
@Column("id")
private long id;
@Column("integral")
private long integral;
@Column("pwd")
private String pwd;
@Column("account")
private String account;
@Column("addTime")
private Timestamp addTime;
@Column("login")
private Timestamp login;
@Column("money")
private int money;
@Column("isEnable")
private boolean isEnable;

Dao实现

曾经写过关于注解使用的文章,想使用参数注解设计,但是设计最终没完成,目前我依旧没能力去解决那些问题,这里换了个思路,使用字段注解。

/**
* 利用注解设计Dao,解决了反射硬性要求数据库字段与Javabean对应的问题,比直接用反射更加灵活
*
* @author CSS 2016/12/1
* @version 1.0
*
*/
public class EasyDao2<T> extends DBHelper<T> {
/**
* 分页查询
*
* @param begin
* 开始位置
* @param count
* 取多少行记录
* @return
*/
public List<T> getList(Class<T> cl, int begin, int count) {
StringBuffer sql = new StringBuffer(120);
sql.append("select * from ").append(cl.getSimpleName());
if (begin != -1)
sql.append(" LIMIT ?");
if (count != -1)
sql.append(",?");
System.out.println(sql.toString()); return executeQuery(new Parser<T>() {
@Override
public List<T> parse(ResultSet resultSet) {
return fillArrayList(cl, resultSet);
}
}, sql.toString(), begin, count);
} protected List<T> fillArrayList(Class<T> clazz, ResultSet resultSet) {
List<T> list = new ArrayList<T>();
try {
Field[] fields = clazz.getDeclaredFields();
resultSet.beforeFirst();
while (resultSet.next()) {
T t = (T) clazz.newInstance();
for (int i = 0; i < fields.length; i++) { //获取注解值
Column column = (Column) fields[i].getAnnotations()[0];
if (column == null)
continue;
fields[i].setAccessible(true);
fields[i].set(t, resultSet.getObject(column.value())); }
list.add(t);
}
} catch (Exception e) {
e.printStackTrace();
}
return list;
}
}

测试类

public class Test {
public static void main(String[] args) {
EasyDao2<Accounts> accountDao=new EasyDao2<>();
List<Accounts> list=accountDao.getList(Accounts.class, 0, 1);
System.out.println(list.toString()); AccountsDao accountsDao=new AccountsDao();
accountsDao.insert(list.get(0));
}
}

C3P0连接池配置

首先你需要一个C3P0的Jar包,c3p0-config.xml放在src根目录下,ConnectionPool位置任意。

连接池Java代码

/**
* 数据库链接对象管理类
*
* @author CSS
* @version 1.0
*
*/
public class ConnectionPool {
private static ComboPooledDataSource dpds = null; private ConnectionPool() {
} static {
if (dpds == null)
createComboPooledDataSource();
} private synchronized static void createComboPooledDataSource() {
if (dpds == null)
dpds = new ComboPooledDataSource("mysql");
} public synchronized static Connection getConnection() {
try {
return dpds.getConnection();
} catch (SQLException e) {
e.printStackTrace();
}
return null;
} @Override
protected void finalize() throws Throwable {
if (dpds != null)
DataSources.destroy(dpds);
super.finalize();
}
}

c3p0-config.xml

<?xml version="1.0" encoding="UTF-8"?>
<c3p0-config>
<named-config name="mysql">
<!-- 配置数据库用户名 -->
<property name="user">root</property>
<!-- 配置数据库密码 -->
<property name="password"></property>
<!-- 配置数据库链接地址 -->
<property name="jdbcUrl">jdbc:mysql://192.168.28.217:3307/medicine?useUnicode=true&amp;characterEncoding=UTF-8
</property>
<!-- <property name="jdbcUrl">jdbc:mysql://10.50.8.50:3307/medicine?useUnicode=true&amp;characterEncoding=UTF-8&amp;useSSL=false
</property> --> <!-- 配置数据库驱动 -->
<property name="driverClass">com.mysql.jdbc.Driver</property>
<!-- 数据库连接池一次性向数据库要多少个连接对象 -->
<property name="acquireIncrement">40</property>
<!-- 初始化连接数 -->
<property name="initialPoolSize">20</property>
<!-- 最小连接数 -->
<property name="minPoolSize">5</property>
<!--连接池中保留的最大连接数。Default: 15 -->
<property name="maxPoolSize">30</property>
<!--JDBC的标准参数,用以控制数据源内加载的PreparedStatements数量。但由于预缓存的statements 属于单个connection而不是整个连接池。所以设置这个参数需要考虑到多方面的因素。如果maxStatements与maxStatementsPerConnection均为0,则缓存被关闭。Default:0 -->
<property name="maxStatements">0</property>
<!--maxStatementsPerConnection定义了连接池内单个连接所拥有的最大缓存statements数。Default: 0 -->
<property name="maxStatementsPerConnection">0</property>
<!--c3p0是异步操作的,缓慢的JDBC操作通过帮助进程完成。扩展这些操作可以有效的提升性能 通过多线程实现多个操作同时被执行。Default:3 -->
<property name="numHelperThreads">3</property>
<!--用户修改系统配置参数执行前最多等待300秒。Default: 300 -->
<property name="propertyCycle">3</property>
<!-- 获取连接超时设置 默认是一直等待单位毫秒 -->
<property name="checkoutTimeout">1000</property>
<!--每多少秒检查所有连接池中的空闲连接。Default: 0 -->
<property name="idleConnectionTestPeriod">3</property>
<!--最大空闲时间,多少秒内未使用则连接被丢弃。若为0则永不丢弃。Default: 0 -->
<property name="maxIdleTime">10</property>
<!--配置连接的生存时间,超过这个时间的连接将由连接池自动断开丢弃掉。当然正在使用的连接不会马上断开,而是等待它close再断开。配置为0的时候则不会对连接的生存时间进行限制。 -->
<property name="maxIdleTimeExcessConnections">5</property>
<!--两次连接中间隔时间,单位毫秒。Default: 1000 -->
<property name="acquireRetryDelay">1000</property>
<!--c3p0将建一张名为Test的空表,并使用其自带的查询语句进行测试。如果定义了这个参数那么属性preferredTestQuery将被忽略。你不能在这张Test表上进行任何操作,它将只供c3p0测试使用。Default:
null -->
<property name="automaticTestTable">Test</property>
<!-- 获取connnection时测试是否有效 -->
<property name="testConnectionOnCheckin">true</property>
</named-config>
</c3p0-config>