dao层设计
通用性的思考
要如何做到dao层与数据库和表无关是一个很值得思考的问题,如果想要偷懒,不为每个业务都编写特定的dao层实现,就必须要考虑所有可能会出现的情况。
关于sql语句
其实要谈论的不是通不通用,因为sql语句就已经是最通用,最灵活的数据库操作实现了,因为你可以用它操作任何数据库只要你写出对应的sql语句。
但是正是因为sql语句太灵活,所以编写很麻烦,而且业务的不同导致数据库表的不同,则sql语句也必定会不同,即便一个业务很简单,你也几乎无法重用以前写过的sql语句。
因此我在程序中要做的就是,以java代码的思维去实现数据库操作,并且不为用户暴露过多的实现细节,所谓不暴露过多的细节是指方法的参数不要太复杂,使方法简单易用,同时能满足一些经常出现的业务。
底层当然采用的是拼接sql的形式,最终达到的效果是,即便你不是很懂sql语句,只要你懂java,不写sql,也可以操作数据库。
于是我便有了这个接口的设计:
/**
* Created by liuruijie on 2017/1/17.
* 能满足数据库的多种操作的通用的dao层接口
*/
public interface CommonDao<T> {
/**
* 根据主键查询一条数据
* @param tableName 表名
* @param pkName 主键字段名
* @param id 值
* @param type 要转换的返回类型
* @return 将记录转换成的po类的实例
*/
T selectById(String tableName, String pkName, String id, Class<T> type);
/**
* 根据查询条件查询记录
* List<Student> students = commonDao
* .selectByCriteria("m_student"
* , commonDao.createCriteria()
* .not().like("id", "2013")
* .between("age", 10, 20)
* .not().eq("gender", "F")
* , Student.class);
* @param tableName 表名
* @param criteria 查询条件
* @param type 类型
* @return 将记录转换成的po类的实例的列表
*/
List<T> selectByCriteria(String tableName, Criteria criteria, Class<T> type);
/**
* 查询记录数
* @param tableName 表名
* @param criteria 查询条件
* @return 记录数
*/
long countByCriteria(String tableName, Criteria criteria);
/**
* 根据主键删除一条记录
* @param tableName 表名
* @param pkName 主键字段名
* @param id 主键值
* @return 影响行数 0或1
*/
int removeById(String tableName, String pkName, String id);
/**
* 保存一个对象为一条数据库记录
* 如果对象主键不存在,则会新建
* 如果对象主键已经存在,则会更新
* @param tableName 表名
* @param pkName 主键字段名
* @param entity 要保存的对象实体
* @return 影响行数 0或1
*/
int save(String tableName, String pkName, T entity);
/**
* 查询条件
*/
interface Criteria{
/**
* 使接下来的条件取非
*/
Criteria not();
/**
* 使与下一个条件的连接词变为or,默认为and
*/
Criteria or();
/**
* 相等
* @param field 字段名
* @param val 值
*/
Criteria eq(String field, Object val);
/**
* 字符串匹配
* @param field 字段名
* @param val 值
*/
Criteria like(String field, Object val);
/**
* 取两个值之间的值
* @param field 字段名
* @param val1 值1
* @param val2 值2
*/
Criteria between(String field, Object val1, Object val2);
/**
* 限制查询记录数
* @param start 开始位置
* @param row 记录数
*/
Criteria limit(int start, int row);
/**
* 获取参数列表
* @return 参数列表
*/
List<Object> getParam();
/**
* 获取拼接好的where条件sql语句
* @return sql
*/
StringBuilder getCriteriaSQL();
}
/**
* 让实现类自己实现建立条件的方法
* @return 查询条件实例
*/
Criteria createCriteria();
}
这个接口提供了最常用的一些操作:根据主键查询记录,多条件查询,删除一条记录,更新记录以及插入记录,而且都是能满足大多数业务的,并且需要用户提供的参数也不多。
当然要做到简单,就必须要舍弃一些不常见的细节,比如说这里我就只考虑了单主键的情况。
当然这些都可以不断地完善,一开始就做出十全十美的东西是肯定不可能的。
接下来看一下实现:
/**
* Created by liuruijie on 2017/1/17.
* 通用dao层接口的实现
*/
@Service
public class CommonDaoImpl<T> implements CommonDao<T>{
@Resource
JdbcTemplate jdbcTemplate;
@Override
public T selectById(String tableName, String pkName, String id, Class<T> type) {
Map<String, Object> obj = jdbcTemplate.queryForMap("SELECT * FROM "+tableName+" WHERE "+pkName+" = ?", id);
return ObjectUtil.mapToObject(obj, type);
}
@Override
public List<T> selectByCriteria(String tableName, CommonDao.Criteria criteria, Class<T> type) {
StringBuilder sqlStr = new StringBuilder("");
sqlStr.append("SELECT * FROM ")
.append(tableName)
.append(criteria.getCriteriaSQL());
System.out.println(sqlStr.toString());
Object[] params = criteria.getParam().toArray(new Object[criteria.getParam().size()]);
List<Map<String, Object>> objs = jdbcTemplate.queryForList(sqlStr.toString(), params);
List<T> results = new ArrayList<>();
for(Map<String, Object> o: objs){
results.add(ObjectUtil.mapToObject(o, type));
}
return results;
}
@Override
public long countByCriteria(String tableName, CommonDao.Criteria criteria) {
String sql = "SELECT COUNT(*) AS num FROM "+tableName + criteria.getCriteriaSQL();
Map<String, Object> map = jdbcTemplate.queryForMap(sql, criteria.getParam().toArray());
return (Long)map.get("num");
}
@Override
public int removeById(String tableName, String pkName, String id) {
String sql = "DELETE FROM " +
tableName +
" WHERE " +
pkName +
" = ?";
return jdbcTemplate.update(sql, id);
}
@Override
public int save(String tableName, String pkName, T entity) {
Map<String, Object> obj = ObjectUtil.objectToMap(entity);
StringBuffer sql1 = new StringBuffer("INSERT INTO ")
.append(tableName)
.append("(");
StringBuffer sql2 = new StringBuffer(" VALUES(");
List<Object> args = new ArrayList<>();
int count = 0;
for(String key: obj.keySet()){
Object arg = obj.get(key);
if (arg==null){
continue;
}
sql1.append(key).append(",");
sql2.append("?,");
args.add(arg);
}
sql1.deleteCharAt(sql1.length() - 1);
sql1.append(") ");
sql2.deleteCharAt(sql2.length() - 1);
sql2.append(") ");
String sql = sql1.append(sql2).toString();
System.out.println(sql);
try {
count += jdbcTemplate.update(sql, args.toArray());
}catch (DuplicateKeyException e){
sql1 = new StringBuffer("UPDATE ")
.append(tableName)
.append(" SET ");
sql2 = new StringBuffer(" WHERE "+pkName+"=?");
args = new ArrayList<>();
for (String key: obj.keySet()){
if (key.equals(pkName)){
continue;
}
Object arg = obj.get(key);
if (arg==null){
continue;
}
sql1.append(key).append("=?,");
args.add(arg);
}
sql1.deleteCharAt(sql1.length() - 1);
args.add(obj.get(pkName));
sql = sql1.append(sql2).toString();
System.out.println(sql);
count+=jdbcTemplate.update(sql, args.toArray());
}
return count;
}
@Override
public CommonDao.Criteria createCriteria() {
return new Criteria();
}
/**
* 查询条件的实现
*/
class Criteria implements CommonDao.Criteria{
private boolean not; //是否标记了非
private boolean begin; //是否正在拼接第一个条件
private boolean or;//是否修改连接词为OR
StringBuilder criteriaSQL; //从where开始的条件sql
List<Object> param; //参数列表
String limitStr; //限制条数
public Criteria(){
criteriaSQL = new StringBuilder("");
param = new LinkedList<>();
not = false;
begin = true;
limitStr = "";
}
public Criteria not(){
not = true;
return this;
}
@Override
public CommonDao.Criteria or() {
or = true;
return this;
}
private void link(){
//判断是否是第一个条件
// ,如果是就加WHERE不加连接词
// ,不是就直接加连接词
if(begin){
criteriaSQL.append(" WHERE ");
}else{
if(or){
criteriaSQL.append(" OR ");
}else{
criteriaSQL.append(" AND ");
}
}
or = false;
}
public Criteria eq(String field, Object val) {
link();
if (not) {
criteriaSQL.append(field)
.append(" != ?");
} else {
criteriaSQL.append(field)
.append(" = ?");
}
not = false;
begin = false;
param.add(val);
return this;
}
public Criteria like(String field, Object val){
link();
if(not){
criteriaSQL.append(field)
.append(" NOT LIKE ?");
}else{
criteriaSQL.append(field)
.append(" LIKE ?");
}
not = false;
begin = false;
param.add("%"+val+"%");
return this;
}
public Criteria between(String field, Object val1, Object val2){
link();
if(not){
criteriaSQL.append(field)
.append(" NOT BETWEEN ? AND ? ");
}else{
criteriaSQL.append(field)
.append(" BETWEEN ? AND ? ");
}
not = false;
begin = false;
param.add(val1);
param.add(val2);
return this;
}
@Override
public CommonDao.Criteria limit(int start, int row) {
limitStr = " limit " + start + "," + row;
return this;
}
public List<Object> getParam(){
return this.param;
}
public StringBuilder getCriteriaSQL(){
return new StringBuilder(criteriaSQL.toString()+limitStr);
}
}
}
实现使用了spring的jdbcTemplate,其实也可以用最原始的jdbc来实现,虽然麻烦一点,但可以减少依赖。
只要接口设计合理,实现起来无非就是一些字符串的拼接,不会太复杂。这里面用到了map和对象互转的工具,为了偷懒,我两个转换方法的实现我是采用的fastjson中序列化json的方法来转换的。
/**
* Created by liuruijie on 2017/1/17.
* 对象工具
*/
public class ObjectUtil {
/**
* 对象转字典
*/
public static Map<String, Object> objectToMap(Object obj){
return (Map<String, Object>) JSON.toJSON(obj);
}
/**
* 字典转对象
*/
public static <T> T mapToObject(Map<String, Object> map, Class<T> T){
return (T) JSON.parseObject(JSON.toJSONString(map), T);
}
}
写在最后的一些感受
可能会有人觉得,为什么不用ORM框架来做dao层接口,也同样很简单,很方便。
这样说吧,以前我是使用的mybatis来做的数据库访问层,并且使用了它的插件mybatisGenerator来做的根据数据库,逆向生成代码,不得不承认它确实很方便,可同样可以做到不写一条sql语句就能够操作数据库。但是用到后来,每个表对应至少一个po类+一个xml文件+一个dao接口+一个Example条件类,到业务越来越复杂,这些东西也就越来越多,然后你就会发现这种自动生成的代码几乎是不能维护的。而且它为你生成了太多无用的代码,要想精简代码,还是必须要自己写sql语句。
虽然使用流行的框架,不仅让写代码更方便,还能保证稳定性,而且自己写sql语句还能进行优化,提高执行效率,但是请想一想,有多少人能写出高性能的sql语句呢,对于一些普通的业务,如:xxx人员管理系统,xxx图书管理系统等,这些小型项目,高性能的sql语句又能使其得到多大的提升呢。
我的一贯思路都是,实现优先,然后考虑扩展性和可重用性,然后考虑稳定性,最后才考虑性能问题。能让程序在最短的时间内能运行起来才是最重要的,而不是为了提升程序在运行时的速度,而使用复杂的实现,导致迟迟不能运行,最后由于代码考虑的因素过多,把自己搞晕。
到此,我在后端的方向将一个web项目的结构设计从前后端的交互,到业务层的异常处理,再到数据访问层的设计都给出了自己的思路。虽然不是很完美,但是对于我自己来说,这个设计还是挺使用的,这3篇文章中提到的设计思路几乎都是可以运用于各种项目中的。