dao層設(shè)計(jì)
通用性的思考
要如何做到dao層與數(shù)據(jù)庫(kù)和表無(wú)關(guān)是一個(gè)很值得思考的問(wèn)題住练,如果想要偷懶睦疫,不為每個(gè)業(yè)務(wù)都編寫(xiě)特定的dao層實(shí)現(xiàn)碍彭,就必須要考慮所有可能會(huì)出現(xiàn)的情況。
關(guān)于sql語(yǔ)句
其實(shí)要談?wù)摰牟皇峭ú煌ㄓ醚虼瘢驗(yàn)閟ql語(yǔ)句就已經(jīng)是最通用蹂午,最靈活的數(shù)據(jù)庫(kù)操作實(shí)現(xiàn)了,因?yàn)槟憧梢杂盟僮魅魏螖?shù)據(jù)庫(kù)只要你寫(xiě)出對(duì)應(yīng)的sql語(yǔ)句翔怎。
但是正是因?yàn)閟ql語(yǔ)句太靈活窃诉,所以編寫(xiě)很麻煩,而且業(yè)務(wù)的不同導(dǎo)致數(shù)據(jù)庫(kù)表的不同赤套,則sql語(yǔ)句也必定會(huì)不同飘痛,即便一個(gè)業(yè)務(wù)很簡(jiǎn)單,你也幾乎無(wú)法重用以前寫(xiě)過(guò)的sql語(yǔ)句容握。
因此我在程序中要做的就是宣脉,以java代碼的思維去實(shí)現(xiàn)數(shù)據(jù)庫(kù)操作,并且不為用戶(hù)暴露過(guò)多的實(shí)現(xiàn)細(xì)節(jié)剔氏,所謂不暴露過(guò)多的細(xì)節(jié)是指方法的參數(shù)不要太復(fù)雜塑猖,使方法簡(jiǎn)單易用,同時(shí)能滿(mǎn)足一些經(jīng)常出現(xiàn)的業(yè)務(wù)谈跛。
底層當(dāng)然采用的是拼接sql的形式羊苟,最終達(dá)到的效果是,即便你不是很懂sql語(yǔ)句感憾,只要你懂java蜡励,不寫(xiě)sql,也可以操作數(shù)據(jù)庫(kù)阻桅。
于是我便有了這個(gè)接口的設(shè)計(jì):
/**
* Created by liuruijie on 2017/1/17.
* 能滿(mǎn)足數(shù)據(jù)庫(kù)的多種操作的通用的dao層接口
*/
public interface CommonDao<T> {
/**
* 根據(jù)主鍵查詢(xún)一條數(shù)據(jù)
* @param tableName 表名
* @param pkName 主鍵字段名
* @param id 值
* @param type 要轉(zhuǎn)換的返回類(lèi)型
* @return 將記錄轉(zhuǎn)換成的po類(lèi)的實(shí)例
*/
T selectById(String tableName, String pkName, String id, Class<T> type);
/**
* 根據(jù)查詢(xún)條件查詢(xún)記錄
* 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 查詢(xún)條件
* @param type 類(lèi)型
* @return 將記錄轉(zhuǎn)換成的po類(lèi)的實(shí)例的列表
*/
List<T> selectByCriteria(String tableName, Criteria criteria, Class<T> type);
/**
* 查詢(xún)記錄數(shù)
* @param tableName 表名
* @param criteria 查詢(xún)條件
* @return 記錄數(shù)
*/
long countByCriteria(String tableName, Criteria criteria);
/**
* 根據(jù)主鍵刪除一條記錄
* @param tableName 表名
* @param pkName 主鍵字段名
* @param id 主鍵值
* @return 影響行數(shù) 0或1
*/
int removeById(String tableName, String pkName, String id);
/**
* 保存一個(gè)對(duì)象為一條數(shù)據(jù)庫(kù)記錄
* 如果對(duì)象主鍵不存在凉倚,則會(huì)新建
* 如果對(duì)象主鍵已經(jīng)存在,則會(huì)更新
* @param tableName 表名
* @param pkName 主鍵字段名
* @param entity 要保存的對(duì)象實(shí)體
* @return 影響行數(shù) 0或1
*/
int save(String tableName, String pkName, T entity);
/**
* 查詢(xún)條件
*/
interface Criteria{
/**
* 使接下來(lái)的條件取非
*/
Criteria not();
/**
* 使與下一個(gè)條件的連接詞變?yōu)閛r嫂沉,默認(rèn)為and
*/
Criteria or();
/**
* 相等
* @param field 字段名
* @param val 值
*/
Criteria eq(String field, Object val);
/**
* 字符串匹配
* @param field 字段名
* @param val 值
*/
Criteria like(String field, Object val);
/**
* 取兩個(gè)值之間的值
* @param field 字段名
* @param val1 值1
* @param val2 值2
*/
Criteria between(String field, Object val1, Object val2);
/**
* 限制查詢(xún)記錄數(shù)
* @param start 開(kāi)始位置
* @param row 記錄數(shù)
*/
Criteria limit(int start, int row);
/**
* 獲取參數(shù)列表
* @return 參數(shù)列表
*/
List<Object> getParam();
/**
* 獲取拼接好的where條件sql語(yǔ)句
* @return sql
*/
StringBuilder getCriteriaSQL();
}
/**
* 讓實(shí)現(xiàn)類(lèi)自己實(shí)現(xiàn)建立條件的方法
* @return 查詢(xún)條件實(shí)例
*/
Criteria createCriteria();
}
這個(gè)接口提供了最常用的一些操作:根據(jù)主鍵查詢(xún)記錄稽寒,多條件查詢(xún),刪除一條記錄趟章,更新記錄以及插入記錄杏糙,而且都是能滿(mǎn)足大多數(shù)業(yè)務(wù)的,并且需要用戶(hù)提供的參數(shù)也不多尤揣。
當(dāng)然要做到簡(jiǎn)單搔啊,就必須要舍棄一些不常見(jiàn)的細(xì)節(jié),比如說(shuō)這里我就只考慮了單主鍵的情況北戏。
當(dāng)然這些都可以不斷地完善负芋,一開(kāi)始就做出十全十美的東西是肯定不可能的。
接下來(lái)看一下實(shí)現(xiàn):
/**
* Created by liuruijie on 2017/1/17.
* 通用dao層接口的實(shí)現(xiàn)
*/
@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();
}
/**
* 查詢(xún)條件的實(shí)現(xiàn)
*/
class Criteria implements CommonDao.Criteria{
private boolean not; //是否標(biāo)記了非
private boolean begin; //是否正在拼接第一個(gè)條件
private boolean or;//是否修改連接詞為OR
StringBuilder criteriaSQL; //從where開(kāi)始的條件sql
List<Object> param; //參數(shù)列表
String limitStr; //限制條數(shù)
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(){
//判斷是否是第一個(gè)條件
// ,如果是就加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);
}
}
}
實(shí)現(xiàn)使用了spring的jdbcTemplate莽龟,其實(shí)也可以用最原始的jdbc來(lái)實(shí)現(xiàn),雖然麻煩一點(diǎn)锨天,但可以減少依賴(lài)毯盈。
只要接口設(shè)計(jì)合理,實(shí)現(xiàn)起來(lái)無(wú)非就是一些字符串的拼接病袄,不會(huì)太復(fù)雜搂赋。這里面用到了map和對(duì)象互轉(zhuǎn)的工具,為了偷懶益缠,我兩個(gè)轉(zhuǎn)換方法的實(shí)現(xiàn)我是采用的fastjson中序列化json的方法來(lái)轉(zhuǎn)換的脑奠。
/**
* Created by liuruijie on 2017/1/17.
* 對(duì)象工具
*/
public class ObjectUtil {
/**
* 對(duì)象轉(zhuǎn)字典
*/
public static Map<String, Object> objectToMap(Object obj){
return (Map<String, Object>) JSON.toJSON(obj);
}
/**
* 字典轉(zhuǎn)對(duì)象
*/
public static <T> T mapToObject(Map<String, Object> map, Class<T> T){
return (T) JSON.parseObject(JSON.toJSONString(map), T);
}
}
寫(xiě)在最后的一些感受
可能會(huì)有人覺(jué)得,為什么不用ORM框架來(lái)做dao層接口幅慌,也同樣很簡(jiǎn)單宋欺,很方便。
這樣說(shuō)吧胰伍,以前我是使用的mybatis來(lái)做的數(shù)據(jù)庫(kù)訪(fǎng)問(wèn)層齿诞,并且使用了它的插件mybatisGenerator來(lái)做的根據(jù)數(shù)據(jù)庫(kù),逆向生成代碼骂租,不得不承認(rèn)它確實(shí)很方便祷杈,可同樣可以做到不寫(xiě)一條sql語(yǔ)句就能夠操作數(shù)據(jù)庫(kù)。但是用到后來(lái)菩咨,每個(gè)表對(duì)應(yīng)至少一個(gè)po類(lèi)+一個(gè)xml文件+一個(gè)dao接口+一個(gè)Example條件類(lèi)吠式,到業(yè)務(wù)越來(lái)越復(fù)雜,這些東西也就越來(lái)越多抽米,然后你就會(huì)發(fā)現(xiàn)這種自動(dòng)生成的代碼幾乎是不能維護(hù)的。而且它為你生成了太多無(wú)用的代碼糙置,要想精簡(jiǎn)代碼云茸,還是必須要自己寫(xiě)sql語(yǔ)句。
雖然使用流行的框架谤饭,不僅讓寫(xiě)代碼更方便标捺,還能保證穩(wěn)定性,而且自己寫(xiě)sql語(yǔ)句還能進(jìn)行優(yōu)化揉抵,提高執(zhí)行效率亡容,但是請(qǐng)想一想,有多少人能寫(xiě)出高性能的sql語(yǔ)句呢冤今,對(duì)于一些普通的業(yè)務(wù)闺兢,如:xxx人員管理系統(tǒng),xxx圖書(shū)管理系統(tǒng)等戏罢,這些小型項(xiàng)目屋谭,高性能的sql語(yǔ)句又能使其得到多大的提升呢脚囊。
我的一貫思路都是,實(shí)現(xiàn)優(yōu)先桐磁,然后考慮擴(kuò)展性和可重用性悔耘,然后考慮穩(wěn)定性,最后才考慮性能問(wèn)題我擂。能讓程序在最短的時(shí)間內(nèi)能運(yùn)行起來(lái)才是最重要的衬以,而不是為了提升程序在運(yùn)行時(shí)的速度,而使用復(fù)雜的實(shí)現(xiàn)校摩,導(dǎo)致遲遲不能運(yùn)行看峻,最后由于代碼考慮的因素過(guò)多,把自己搞暈秧耗。
到此备籽,我在后端的方向?qū)⒁粋€(gè)web項(xiàng)目的結(jié)構(gòu)設(shè)計(jì)從前后端的交互,到業(yè)務(wù)層的異常處理分井,再到數(shù)據(jù)訪(fǎng)問(wèn)層的設(shè)計(jì)都給出了自己的思路车猬。雖然不是很完美,但是對(duì)于我自己來(lái)說(shuō)尺锚,這個(gè)設(shè)計(jì)還是挺使用的珠闰,這3篇文章中提到的設(shè)計(jì)思路幾乎都是可以運(yùn)用于各種項(xiàng)目中的。