Node 架构从三层到 N 层,实现代码重用和解耦

作者:郭泽豪

三层架构(3-tier Application)通常意义上是将整个业务应用划分为:控制层(Controller)、业务逻辑层(Service)以及数据访问层(Dao),三层架构在Java Web项目中很常见,那么这种架构能否运用在Node项目中,实现业务代码的可重用和解耦呢?

答案是可以的。最近我在用Node重构部门的项目,通过一番技术调研,主要的方式是(1)上一些国内外Node相关的知名社区,看看大家对于这个技术的讨论;(2)有没有完备的文档;(3)有没有前人发现一些致命的bug,这些bug是否已经修复,这些bug对于我们的项目影响范围有多大;(4)上手难易程度。权衡之后选择一个技术。最终确定下来的主要技术包括Express4.x,sequelize4.x,接下来以我的项目实践为例子,谈谈以下的内容。

  • 为什么要选用三层或N层架构
  • 如何使用Express和Sequelize搭建Node三层架构
  • 每层之间是通过什么方式进行数据流动的
  • 为了让业务代码能够分层解耦,在代码实现过程中我是如何思考的,比如数据库事务
  • 从三层到N层的演化

一、为什么要选用三层或N层架构

熟悉express框架的开发者都知道,我们可以用express全局命令生成express项目的目录结构,express项目的主要文件和目录包括app.js(node服务器实例的创建、配置及启动,项目程序的入口),routes目录(路由控制器目录,里面默认会包含index.js)以及views目录(视图目录),express实际上是一个MVC模型,对于express不熟悉的,可以查看我的KM文章《Nodejs入门:从零开始开发微博系统》。

在这里,我们试想一下,如果我们的业务代码不分层,而是在app.js配置路由规则,把路由对应的处理逻辑,包括解析请求,数据处理,数据库操作等逻辑代码堆积在routes目录中,你会发现很多时间你在写着同样的代码,这种方式写出来的代码是很恶心的,也就是代码冗余的问题,其实代码冗余与数据库数据冗余的道理是一样的,只要遵从一定的标准,比如范式,你就可以写出优雅的代码,但是往往这个过程是伴随着一个开发者的素养和思想的,这里我会讲讲在系统框架搭建的过程中我是如何思考的。

二、如何使用Express和Sequelize搭建Node三层架构

现在Node Web框架比较火的两个框架是Koa和Express,二者的区别大家可以自行查阅,koa是express原班人马开发的使用Node新特性的中间件框架,可以说是大势所趋,但是我觉得时机还不够成熟,另一方面也是我对express比较熟,所以我还是选用express。数据库访问层我采用Sequelize ORM框架,比较好的ORM框架还有orm2,waterline,bookshelf,通过前述的一番技术调研,选用Sequelize这个ORM框架还是靠谱的。

一开始我并没有采用ORM框架,而是将Mysql的连接池管理、打开连接、释放连接以及一些DML等操作封装在一个公共组件内,业务代码只要涉及数据库操作,就引入这个组件,通过这个组件执行相应的SQL命令完成相应的业务逻辑,感觉还不错的样子,架构图如图1所示。但是当在实现包含很多数据库DML操作的业务逻辑时,你会发现路由处理层的代码当中充斥一堆的嵌套回调,代码的可读性很差。

另外你会发现不同业务代码包含很多重复的SQL语句,这样会导致后期的可维护性也会很差,我们可以看看相应的代码,图2是两层嵌套的DML操作。或许有些开发者可能会在数据库公共组件的上层根据功能模块划分再抽象出Dao层,具体的Dao层可能包括UserDao,TaskDao等,Dao层的查询结果通过回调函数返回给路由处理层,架构图如图3所示。我的SYNCDB就是用这种架构,代码如图4所示。这种方式起码要比第一种方式有所改进,但是还是有一定的缺陷,你会发现很多业务逻辑掺杂在Dao层,但是从功能职责来说,Dao层应该只负责数据库操作的。

很自然地,我们会在Dao层的上层再抽象出Service业务层,Service层跟Dao层的数据流动还是通过回调函数吗?如果还是回调函数,那还是避免不了嵌套回调。那时我在想怎么才能从这种嵌套回调中解脱出来,我想到让Dao层的DML执行结果返回Promise对象,或者是Service层用流程控制库比如async,step,在这个反复的纠结的过程中也接触到目前流行的async/await编程模型,Node7.x才支持这个新特性。

我一度有想过自己通过async/await的方式从嵌套回调中脱身,这种方式写出来的Node代码很酷,但是结果有可能就是装逼挖坑给自己跳,从开发进度、学习成本等方面考虑,我最终选择了Sequelize,Sequelize的每次DML操作结果都是返回一个Promise对象,这是符合我的初衷的,业务层通过执行then函数处理成功返回的结果,通过catch函数捕获异常对象,另外Sequelize支持外键查询以及事务处理,完全符合我们的项目开发要求。最终的架构图如图5所示,如图6所示,我们代码的类似这样,相比前面两种是不是逻辑清晰很多,职责更加明确一些呢?

图1 数据库公共组件


var user_id ='';
var page = parseInt(req.params.page);
var pagesize = 3;
var offset = pagesize(page-1);
var sql = 'select * from project where status!=0 and recommend =1 order by id desc limit '+offset+','+pagesize;
conn.query(sql,function(err,rows){
    if (err) {
        console.log(err);
    }else{
        var i = 0;
        var datas = new Array();
        rows.forEach(function(row){
            var user_id = row.user_id;
            var user_sql = 'select avatar,nickname from user where id='+user_id;
            conn.query(user_sql,function(err,result){
                if (err) {
                    console.log(err);
                }else{
                    var nickname = result[0].nickname;
                    var avatar = result[0].avatar;
                    console.log("nickname="+nickname);
                    console.log("avatar="+avatar);
                    row.nickname = nickname;
                    row.avatar = avatar;
                    console.log(row);   
                }
            })
            datas[i]=row;                 
            i++;
        })
        console.log(datas);
        res.json({code: 200,content:datas})
    }
});

图2 callback嵌套过深

图3 数据库公共组件上层抽象出Dao层

// 路由控制层代码
exports.getOldTables = function(req, res){
    Old_table.get(null, function(err, old_tables){
        if(err){
            console.log('old_table查询失败');
            return ;
        }else{
            if(old_tables && old_tables.length){
                console.log('old_table查询成功,总共有%d张表',old_tables.length);
                var default_table_name = old_tables[0].table_name;
                if(req.params.table_name)
                    default_table_name = req.params.table_name;
                Old_table.get(default_table_name, function(err, old_table_selected){
                    if(err){
                        console.log('old_table查询'+ default_table_name +'失败');
                        return ;
                    }
                    if(old_table_selected){
                        res.render('index',{
                            tables : old_tables,
                            selected_table : old_table_selected[0],
                            flag : 'old'
                        });
                    }
                });
            }else{
                res.render('index',{
                    tables : [],
                    selected_table : null,
                    flag : 'old'
                });
            }
        }
    });
};
//Dao层代码
Old_table.get = function get(table_name, callback){
    var db = new Mysqldb(db_config);
    var sql = 'SELECT * FROM old_table';
    if(table_name)
        sql += " WHERE table_name = '" + table_name + "'";
    console.log(sql);
    db.select(sql, function(err){
        db.close();
        if(err)
            callback(err, null);
    }, function(rows){
        db.close();
        if(rows){
            var old_tables = [];
            rows.forEach(function(row, index){
                var old_table = new Old_table(row.project_name, row.db_name, row.table_name, row.create_table_sql);
                old_tables.push(old_table);
            });
            callback(null, old_tables);
        }
    });
};

图4 Dao层的代码掺杂很多职责以外的逻辑

图5 Node三层架构

//路由处理层代码
exports.postUserInfo = function(req, res){
    var result = {};
    var uin = req.cookies.ptui_loginuin;
    let ADDRESS_LENGTH_LIMIT_UP = 50;
    UserService.getUserInfoByUin(function(code, msg, userInfo){
        logger.info('update userinfo : %s', uin);
        if(code != LeagueResultCode.Success){
            logger.error('update userinfo, get userinfo error, uin : %s', uin);
            return res.send({'code' : code, 'msg' : msg});
        }
        let user_type = req.body['user_type'].trim();
        let user_name = req.body['user_name'].trim();
        let email =  req.body['email'].trim();
        let tel_phone = req.body['tel_phone'].trim();
        let real_name = req.body['real_name'].trim();
        let area_prov = req.body['area_prov'].trim();
        let area_city = req.body['area_city'].trim();
        let area_region = req.body['area_region'].trim();
        let area_address = req.body['area_address'].trim();
        let business_type = req.body['business_type'].trim();
        let id_number = req.body['id_number'].trim();
        let id_image_front = req.body['id_image_front'].trim();
        let id_image_back = req.body['id_image_back'].trim();
        let bank_name = req.body['bank_name'].trim();
        let bank_prov = req.body['bank_prov'].trim();
        let bank_city = req.body['bank_city'].trim();
        let bank_full_name = req.body['bank_full_name'].trim();
        let bank_user_name = req.body['bank_user_name'].trim();
        let card_num = req.body['card_num'].trim();
        let card_image_front = req.body['card_image_front'].trim();
        let card_image_back = req.body['card_image_back'].trim();
        let contact_qq = req.body['contact_qq'].trim();
        let license_image = req.body['license_image'].trim();

        let unmodify_flag = '***';//检测到含有*的认为没改动
        if(id_number.indexOf(unmodify_flag) != -1){//没改过身份证
            id_number = userInfo.id_number;
        }
        if(card_num.indexOf(unmodify_flag) != -1){//没改过银行卡号
            card_num = userInfo.card_num;
        }
        if(id_image_back.indexOf(unmodify_flag) != -1){//没改过身份证图片
            id_image_back = userInfo.id_image_back;
        }
        if(id_image_front.indexOf(unmodify_flag) != -1){//没改过身份证图片
            id_image_front = userInfo.id_image_front;
        }
        if(card_image_back.indexOf(unmodify_flag) != -1){//没改过银行卡图片
            card_image_back = userInfo.card_image_back;
        }
        if(card_image_front.indexOf(unmodify_flag) != -1){//没改过银行卡图片
            card_image_front = userInfo.card_image_front;
        }
        //检测数据的合法性
        if(StringUtils.isEmpty(user_name)){
            return res.send({'code' : LeagueResultCode.ValidatorUserName, 'msg' : 'user_name err.'});
        }else if(StringUtils.isEmpty(contact_qq)){
            return res.send({'code' : LeagueResultCode.ValidatorContactQQ, 'msg' : 'contact_qq err.'});
        }else if(StringUtils.isEmpty(real_name)){
            return res.send({'code' : LeagueResultCode.ValidatorRealName, 'msg' : 'real_name err.'});
        }else if(StringUtils.isNotEmpty(email) && !Validator.isEmail(email)){
            return res.send({'code' : LeagueResultCode.ValidatorEmail, 'msg' : 'email err.'});
        }else if(StringUtils.isEmpty(id_number) || !Validator.isIDCard(id_number)){
            return res.send({'code' : LeagueResultCode.ValidatorIDCard, 'msg' : 'id card err.'});
        }else if(StringUtils.isEmpty(id_image_front) || StringUtils.isEmpty(id_image_back)){
            return res.send({'code' : LeagueResultCode.ValidatorIDImage, 'msg' : 'id image err.'});
        }else if(StringUtils.isEmpty(bank_full_name)){
            return res.send({'code' : LeagueResultCode.ValidatorBankFullName, 'msg' : 'bank_full_name err.'});
        }else if(StringUtils.isEmpty(bank_user_name)){
            return res.send({'code' : LeagueResultCode.ValidatorBankUserName, 'msg' : 'bank_user_name err.'});
        }else if(StringUtils.isEmpty(card_num) || !Validator.isIntegerPositive(card_num)){
            return res.send({'code' : LeagueResultCode.ValidatorBankCard, 'msg' : 'bank card err.'});
        }else if(StringUtils.isEmpty(card_image_front) || StringUtils.isEmpty(card_image_back)){
            return res.send({'code' : LeagueResultCode.ValidatorCardImage, 'msg' : 'card_image err.'});
        }else if(user_type == LeagueConsts.USER_TYPE_COMPANY && String.isEmpty(license_image)){
            return res.send({'code' : LeagueResultCode.ValidatorLicenseImage, 'msg' : 'license_image err.'});
        }else if(StringUtils.isNotEmpty(area_address) && area_address.length > ADDRESS_LENGTH_LIMIT_UP){
            return res.send({'code' : LeagueResultCode.Validator.ValidatorAreaAddress_Length, 'msg' : 'area_address err.'});
        }else{
            logger.info('user_type|user_name|email|tel_phone|real_name|area_prov|area_city|area_region|area_address|business_type|id_number|id_image_front|id_image_back|\
                bank_name|bank_prov|bank_city|bank_full_name|bank_user_name|card_num|card_image_front|card_image_back|contact_qq|license_image is %s|%s|%s|%s|%s|%s|%s|%s|\
                %s|%s|%s|%s|%s|%s|%s|%s|%s|%s|%s|%s|%s|%s|%s', user_type,user_name,email,tel_phone,real_name,area_prov,area_city,area_region,area_address,business_type,id_number,id_image_front,id_image_back,
                bank_name,bank_prov,bank_city,bank_full_name,bank_user_name,card_num,card_image_front,card_image_back,contact_qq,license_image);
            UserService.updateUser(uin, user_type, user_name, email, userInfo.mobile_phone, tel_phone, real_name, area_prov, area_city, area_region, area_address, business_type,
                id_number, id_image_front, id_image_back, bank_name, bank_prov, bank_city, bank_full_name, bank_user_name, card_num, contact_qq, license_image, card_image_front,
                card_image_back, function(code, msg, data){
                    res.send({'code' : code, 'msg' : msg});
                });
        }
    });
}
//Service层代码
exports.updateUser = function(uin, user_type, user_name, email, mobile_phone, tel_phone, real_name, area_prov, area_city, area_region, area_address, business_type,
                id_number, id_image_front, id_image_back, bank_name, bank_prov, bank_city, bank_full_name, bank_user_name, card_num, contact_qq, license_image, card_image_front,
                card_image_back, callback){

    UserDao.updateUser(uin, user_type, user_name, email, mobile_phone, tel_phone, real_name, area_prov, area_city, area_region, area_address, business_type,
                id_number, id_image_front, id_image_back, bank_name, bank_prov, bank_city, bank_full_name, bank_user_name, card_num, contact_qq, license_image, card_image_front,
                card_image_back).then(function(result){
                    callback(LeagueResultCode.Success, '', result);
                }).catch(function(err){
                    callback(LeagueResultCode.SysException, '', null);
                });
}
//Dao层代码
exports.updateUser = function(uin, user_type, user_name, email, mobile_phone, tel_phone, real_name, area_prov, area_city, area_region, area_address, business_type,
                id_number, id_image_front, id_image_back, bank_name, bank_prov, bank_city, bank_full_name, bank_user_name, card_num, contact_qq, license_image, card_image_front,
                card_image_back){
    return db.User_Info.update({
        'uin' : uin,
        'user_type' : user_type,
        'uin_name' : user_name,
        'email' : email,
        'mobile_phone' : mobile_phone,
        'tel_phone' : tel_phone,
        'real_name' : real_name,
        'area_prov' : area_prov,
        'area_city' : area_city,
        'area_region' : area_region,
        'area_address' : area_address,
        'business_type' : business_type,
        'id_number' : id_number,
        'id_image_front' : id_image_front,
        'id_image_back' : id_image_back,
        'bank_name' : bank_name,
        'bank_prov' : bank_prov,
        'bank_city' : bank_city,
        'bank_full_name' : bank_full_name,
        'bank_user_name' : bank_user_name,
        'card_num' : card_num,
        'contact_qq' : contact_qq,
        'license_image' : license_image,
        'card_image_front' : card_image_front,
        'card_image_back' : card_image_back
    });
}

图6 Node三层架构的代码

三、每层之间是通过什么方式进行数据流动的

通过图5,我们来分析每层之间的数据是通过什么方式进行流动的,首先是我们的表示层发送request请求到路由处理层,即控制层,路由处理层会解析request请求的参数,做一些合法性的校验,如果参数不合法,直接向表示层响应异常状态码。如果参数合法,异步调用Service业务层,Service层会对Dao层发起异步DML操作,Dao层会通过Sequelize的ORM技术操作数据库,Sequelize执行完返回Promise对象给Dao层,Dao层往上传递Promise对象返回给Service层,Service层会为Promise对象设置then函数以及catch函数,如果底层的DML操作失败,则会执行catch函数,如果底层的DML操作成功,则会执行then函数,then函数以及catch函数的执行结果通过callback的方式返回给路由处理层。核心的代码主要在Service层,Service层可能会包含事务管理,并行DML操作等。

四、为了让业务代码能够分层解耦,在代码实现过程中我是如何思考的

为了让业务代码能够分层解耦,每层的职责比较单一,要高类聚,层与层之间不要侵入,要低耦合,想象总是美好的,但是现实是骨感的。我在实现系统分层的过程中也遇到一些问题,我们知道,有时候我们的业务可能会包括很多DML操作,这些DML操作是要保证原子性、一致性、隔离性以及持久性的,也就是事务,我发现在Service层使用Sequelize的事务来保证Dao层的DML操作的ACID特性会侵入Dao层的代码,首先先讲Sequelize实现事务的方式,Sequelize的事务实现方式分为自动提交和手工提交两种方式,Sequelize自动提交和回滚事务的代码如图7所示,手工提交和回滚事务的代码如图8所示。

我发现两种方式都有这样的一句代码{transaction : t},如果我们把User_info.create方法封装到Dao层,代码如图9所示,但是图9的代码是无法实现一个事务的,必须将{transaction : t}这段语句写入userSaveSimple函数和saveAccountLog函数中,也就是说Service层需要启动事务并将事务实例t传到Dao层中,但这样做会侵入到Dao层的代码,我对这种代码是抗拒的,我在想有没有什么办法能够不侵入Dao层。这个问题困扰了我整整一天,通过不断地运行测试代码,阅读Sequelize的源码和谷歌,我终于找到了答案,CLS+unmanagement transaction,在启动事务时,设置transaction的命名空间,这样事务中的DML操作就不需要加入{transaction : t}这段语句,代码如图10所示。

db.sequelize.transaction(function(t){
    return db.User_Info.create({
        'uin' : 0,
        'uin_name' : '0',
        'mobile_phone' : '0',
        'user_type' : 0,
        'create_time' : new Date().getTime(),
        'modify_time' : new Date().getTime(),
        'login_time' : new Date().getTime(),
        'activity_source' : '0',
        'total_value' : 0,
        'available_value' : 0,
        'unconfirmed_value' : 0
    },{transaction : t}).then(function(result){
        throw 'error';
        return db.Account_Log.create({
            'type' : 'T2',
            'day' : DateUtils.getDay(0),
            'uin' : 0,
            'task_id' : '0',
            'account_value' : 0,
            'note' : '0',
            'create_time' : new Date().getTime()
        },{transaction : t});
    });
}).then(function(result){

}).catch(function(err){
    console.log(err);
});
Executing (a9a19f5d-4246-4cbd-9bf1-f14edcc88713): START TRANSACTION;
Executing (a9a19f5d-4246-4cbd-9bf1-f14edcc88713): INSERT INTO `t_user_info` (`uin`,`uin_name`,`mobile_phone`,`info_confirm_state`,`user_type`,`activity_source`,`total_value`,`available_value`,`unconfirmed_value`,`create_time`,`modify_time`,`login_time`,`modify_mobile_count`,`is_first_exchange`,`is_auto_exchange`) VALUES (0,'0','0','0',0,'0',0,0,0,1502721949510,1502721949510,1502721949510,'0','0','0');
Executing (a9a19f5d-4246-4cbd-9bf1-f14edcc88713): ROLLBACK;
error

图7 Sequelize自动提交和回滚事务的代码以及执行结果

db.sequelize.transaction().then(function (t) {
    db.User_Info.create({
        'uin' : 0,
        'uin_name' : '0',
        'mobile_phone' : '0',
        'user_type' : 0,
        'create_time' : new Date().getTime(),
        'modify_time' : new Date().getTime(),
        'login_time' : new Date().getTime(),
        'activity_source' : '0',
        'total_value' : 0,
        'available_value' : 0,
        'unconfirmed_value' : 0
    },{transaction : t}).then(function(result){
        throw 'error';
        return db.Account_Log.create({
        'type' : 'T2',
        'day' : DateUtils.getDay(0),
        'uin' : 0,
        'task_id' : '0',
        'account_value' : 0,
        'note' : '0',
        'create_time' : new Date().getTime()
    },{transaction : t}).then(function(result){
            return t.commit();
        }).catch(function(err){
            console.log(err);
            return t.rollback();
        });

    }).catch(function(err){
        console.log(err);
        return t.rollback();
    });
});
Executing (f7de6b29-e416-4527-8388-81548b564976): START TRANSACTION;
Executing (f7de6b29-e416-4527-8388-81548b564976): INSERT INTO `t_user_info` (`uin`,`uin_name`,`mobile_phone`,`info_confirm_state`,`user_type`,`activity_source`,`total_value`,`available_value`,`unconfirmed_value`,`create_time`,`modify_time`,`login_time`,`modify_mobile_count`,`is_first_exchange`,`is_auto_exchange`) VALUES (0,'0','0','0',0,'0',0,0,0,1502774310032,1502774310032,1502774310032,'0','0','0');
error
Executing (f7de6b29-e416-4527-8388-81548b564976): ROLLBACK;

图8 Sequelize手工提交和回滚事务以及执行结果

db.sequelize.transaction().then(function (t) {
    userSaveSimple(0, '0', '0', 0, '0', 0, 0, 0).then(function(result){
        saveAccountLog('2', 0, '0', 0, '0').then(function(result){
            return t.commit();
        }).catch(function(err){
            console.log(err);
            return t.rollback();
        });
    }).catch(function(err){
        console.log(err);
        return t.rollback();
    });
});

图9 db.User_Info.create以及db.Account_Log.create封装到Dao层

//通过cls创建Sequelize事务的命名空间
var cls = require('continuation-local-storage')
var config = require('../conf/db.js');
var Sequelize = require('sequelize');
var namespace = cls.createNamespace('t');
Sequelize.useCLS(namespace);
var db = {
    sequelize : new Sequelize(config.sequelize.database, config.sequelize.username, config.sequelize.password, config.sequelize),
    _namespace : namespace
};
db.sequelize.transaction().then(function (t) {
    db._namespace.set('transaction', t);
    userSaveSimple(0, '0', '0', 0, '0', 0, 0, 0).then(function(result){
        throw 'error';
        saveAccountLog('2', 0, '0', 0, '0').then(function(result){
            return t.commit();
        }).catch(function(err){
            console.log(err);
            return t.rollback();
        });
    }).catch(function(err){
        console.log(err);
        return t.rollback();
    });
});
function userSaveSimple(uin, uin_name, mobile_phone, user_type, activity_source, total_value, available_value, unconfirmed_value){
    return db.User_Info.create({
        'uin' : uin,
        'uin_name' : uin_name,
        'mobile_phone' : mobile_phone,
        'user_type' : user_type,
        'create_time' : new Date().getTime(),
        'modify_time' : new Date().getTime(),
        'login_time' : new Date().getTime(),
        'activity_source' : activity_source,
        'total_value' : total_value,
        'available_value' : available_value,
        'unconfirmed_value' : unconfirmed_value
    });
}
function saveAccountLog(type, uin, task_id, account_value, note){
    return db.Account_Log.create({
        'type' : type,
        'day' : DateUtils.getDay(0),
        'uin' : uin,
        'task_id' : task_id,
        'account_value' : account_value,
        'note' : note,
        'create_time' : new Date().getTime()
    });
}
Executing (becbe905-7e78-4413-9bc7-c8d0199e6734): START TRANSACTION;
Executing (becbe905-7e78-4413-9bc7-c8d0199e6734): INSERT INTO `t_user_info` (`uin`,`uin_name`,`mobile_phone`,`info_confirm_state`,`user_type`,`activity_source`,`total_value`,`available_value`,`unconfirmed_value`,`create_time`,`modify_time`,`login_time`,`modify_mobile_count`,`is_first_exchange`,`is_auto_exchange`) VALUES (0,'0','0','0',0,'0',0,0,0,1502723044160,1502723044160,1502723044160,'0','0','0');
Executing (becbe905-7e78-4413-9bc7-c8d0199e6734): INSERT INTO `t_account_log` (`log_id`,`type`,`day`,`uin`,`task_id`,`account_value`,`note`,`create_time`) VALUES (DEFAULT,'T2','20170814',0,'0',0,'0',1502723044242);
{ SequelizeDatabaseError: Incorrect integer value: 'T2' for column 'type' at row 1
    at Query.formatError (D:\web_index\node_modules\sequelize\lib\dialects\mysql\query.js:222:16)
    at Query.connection.query [as onResult] (D:\web_index\node_modules\sequelize\lib\dialects\mysql\query.js:55:23)
    at Query.Command.execute (D:\web_index\node_modules\mysql2\lib\commands\command.js:30:12)
    at Connection.handlePacket (D:\web_index\node_modules\mysql2\lib\connection.js:515:28)
    at PacketParser.onPacket (D:\web_index\node_modules\mysql2\lib\connection.js:94:16)
    at PacketParser.executeStart (D:\web_index\node_modules\mysql2\lib\packet_parser.js:77:14)
    at Socket.<anonymous> (D:\web_index\node_modules\mysql2\lib\connection.js:102:29)
    at emitOne (events.js:96:13)
    at Socket.emit (events.js:188:7)
    at readableAddChunk (_stream_readable.js:176:18)
    at Socket.Readable.push (_stream_readable.js:134:10)
    at TCP.onread [as _originalOnread] (net.js:547:20)
    at TCP.onread (D:\web_index\node_modules\async-listener\glue.js:188:31)
  name: 'SequelizeDatabaseError',
  parent: 
   { Error: Incorrect integer value: 'T2' for column 'type' at row 1
       at Packet.asError (D:\web_index\node_modules\mysql2\lib\packets\packet.js:703:13)
       at Query.Command.execute (D:\web_index\node_modules\mysql2\lib\commands\command.js:28:22)
       at Connection.handlePacket (D:\web_index\node_modules\mysql2\lib\connection.js:515:28)
       at PacketParser.onPacket (D:\web_index\node_modules\mysql2\lib\connection.js:94:16)
       at PacketParser.executeStart (D:\web_index\node_modules\mysql2\lib\packet_parser.js:77:14)
       at Socket.<anonymous> (D:\web_index\node_modules\mysql2\lib\connection.js:102:29)
       at emitOne (events.js:96:13)
       at Socket.emit (events.js:188:7)
       at readableAddChunk (_stream_readable.js:176:18)
       at Socket.Readable.push (_stream_readable.js:134:10)
       at TCP.onread [as _originalOnread] (net.js:547:20)
       at TCP.onread (D:\web_index\node_modules\async-listener\glue.js:188:31)
     code: 'ER_TRUNCATED_WRONG_VALUE_FOR_FIELD',
     errno: 1366,
     sqlState: '#HY000',
     sql: 'INSERT INTO `t_account_log` (`log_id`,`type`,`day`,`uin`,`task_id`,`account_value`,`note`,`create_time`) VALUES (DEFAULT,\'T2\',\'20170814\',0,\'0\',0,\'0\',1502723044242);' },
  original: 
   { Error: Incorrect integer value: 'T2' for column 'type' at row 1
       at Packet.asError (D:\web_index\node_modules\mysql2\lib\packets\packet.js:703:13)
       at Query.Command.execute (D:\web_index\node_modules\mysql2\lib\commands\command.js:28:22)
       at Connection.handlePacket (D:\web_index\node_modules\mysql2\lib\connection.js:515:28)
       at PacketParser.onPacket (D:\web_index\node_modules\mysql2\lib\connection.js:94:16)
       at PacketParser.executeStart (D:\web_index\node_modules\mysql2\lib\packet_parser.js:77:14)
       at Socket.<anonymous> (D:\web_index\node_modules\mysql2\lib\connection.js:102:29)
       at emitOne (events.js:96:13)
       at Socket.emit (events.js:188:7)
       at readableAddChunk (_stream_readable.js:176:18)
       at Socket.Readable.push (_stream_readable.js:134:10)
       at TCP.onread [as _originalOnread] (net.js:547:20)
       at TCP.onread (D:\web_index\node_modules\async-listener\glue.js:188:31)
     code: 'ER_TRUNCATED_WRONG_VALUE_FOR_FIELD',
     errno: 1366,
     sqlState: '#HY000',
     sql: 'INSERT INTO `t_account_log` (`log_id`,`type`,`day`,`uin`,`task_id`,`account_value`,`note`,`create_time`) VALUES (DEFAULT,\'T2\',\'20170814\',0,\'0\',0,\'0\',1502723044242);' },
  sql: 'INSERT INTO `t_account_log` (`log_id`,`type`,`day`,`uin`,`task_id`,`account_value`,`note`,`create_time`) VALUES (DEFAULT,\'T2\',\'20170814\',0,\'0\',0,\'0\',1502723044242);' }
Executing (becbe905-7e78-4413-9bc7-c8d0199e6734): ROLLBACK;

图10 通过CLS+unmanagement transaction防止Service层代码侵入Dao层

在项目团队合作中,有很多东西需要我们去考虑的,比如团队开发效率,这里我举些例子,使用过Sequelize的开发者都知道我们要生成数据库表与对象的映射文件,如图11所示。那么如果我们数据库有上百张表,那么要对应写上百个映射文件,这纯粹就是苦力活。有没有什么方法能够根据数据库的表结构自动生成这些映射文件,答案是有的,github上有一个Sequelize-auto的工具能做到,只要输入一些配置参数,运行相应的命令就能生成对应的ORM映射文件。再举一个例子,在项目当中我们需要根据表模式创建对应的对象,同样这样的工作也是苦力活,但是如果很多开发者去妥协这些苦力活,团队的开发效率是得不到提高的,如果有人去开发一个模板工具,那么这样的工具是一劳永逸,伴随着整个项目周期的。如图12是我自己开发一个模板工具,我们只要输入数据库的一个表名和默认值,就可以生成js对象。

/* jshint indent: 2 */

module.exports = function(sequelize, DataTypes) {
  return sequelize.define('t_user_task', {
    uin: {
      type: DataTypes.BIGINT,
      allowNull: false,
      primaryKey: true
    },
    task_id: {
      type: DataTypes.STRING(11),
      allowNull: false,
      primaryKey: true
    },
    product_id: {
      type: DataTypes.STRING(25),
      allowNull: false
    },
    channel_number: {
      type: DataTypes.STRING(20),
      allowNull: false,
      defaultValue: '0'
    },
    url: {
      type: DataTypes.STRING(128),
      allowNull: true
    },
    create_time: {
      type: DataTypes.BIGINT,
      allowNull: true
    },
    total_value: {
      type: DataTypes.BIGINT,
      allowNull: true,
      defaultValue: '0'
    }
  }, {
    tableName: 't_user_task'
  });
};

图11 sequelize数据库表与对象的映射文件

var Mysql = require('./mysqldb.js');
var db = new Mysql({
    "host" : "localhost",
    "user" : "***",
    "password" : "***",
    "database" : "***"
});
var constructor_template = 'function %s(%s){%s}';
var format_template = '%s.format = function(obj){\n\tvar %s = null;\n\tif(obj){\n%s\t}\n\treturn %s;\n}';
function auto(table_name, default_value){
    db.getAllFields(table_name, function(err, results){
        let small_class_name = table_name.slice(2);
        class_name = small_class_name.slice(0, 1).toUpperCase() + small_class_name.slice(1, small_class_name.length);
        var constructor_params = '';
        var init_statements = '\n';
        var format_statements = '\t\t'+small_class_name + ' = new ' + class_name + '();\n';
        results.forEach(function(value, index){
            constructor_params =  constructor_params + value + ', ';
            init_statements = init_statements + '\tthis.' + value + ' = ' + value + ';\n';
            format_statements = format_statements + '\t\t' + small_class_name + '.' + value + ' = ' + 'obj.' + value + ' || ' + ''+default_value+';\n'
        });
        constructor_params = constructor_params.slice(0, constructor_params.length-2);
        console.log(constructor_template, class_name, constructor_params, init_statements);
        console.log(format_template, class_name, small_class_name, format_statements, small_class_name);
        console.log('module.exports = %s;', class_name);
    });
}
auto('t_user_task', '0');
function User_task(uin, task_id, product_id, channel_number, url, create_time, total_value){
    this.uin = uin;
    this.task_id = task_id;
    this.product_id = product_id;
    this.channel_number = channel_number;
    this.url = url;
    this.create_time = create_time;
    this.total_value = total_value;
}
User_task.format = function(obj){
    var user_task = null;
    if(obj){
        user_task = new User_task();
        user_task.uin = obj.uin || 0;
        user_task.task_id = obj.task_id || 0;
        user_task.product_id = obj.product_id || 0;
        user_task.channel_number = obj.channel_number || 0;
        user_task.url = obj.url || 0;
        user_task.create_time = obj.create_time || 0;
        user_task.total_value = obj.total_value || 0;
    }
    return user_task;
}
module.exports = User_task;

图12 利用自己开发的orm-auto工具生成数据库的t_user_task对应的js对象

四、从三层到N层的演化

通过Controller、Service、Dao三层架构确实使得代码的可重用性得到很大的提高,减少了代码冗余,提高了开发效率,另外代码的可读性也得到提高。其实在Express中包含丰富的中间件,中间件层在架构中也扮演很重要的角色,这里我没有在架构图中画出来。对于很多业务来说,可能会引入缓存层,比如memcached将一些频繁访问的结果缓存在内存中,降低DB服务器的压力,所以说我们架构不止是三层,在接入其他层时,我们同样要考虑层内的高类聚以及层之间的低耦合,另外如何无缝地接入其他层提供服务,在这个过程中系统的可扩展性也是需要我们考虑的。

作者:MIG无线合作开发部实习生marcozhguo

电子邮箱:446882229@qq.com

原创声明,本文系作者授权云+社区发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

编辑于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏大内老A

《WCF技术剖析》博文系列汇总[持续更新中]

近半年以来,一直忙于我的第一本WCF专著《WCF技术剖析(卷1)》的写作,一直无暇管理自己的Blog。在《WCF技术剖析(卷1)》写作期间,对WCF又有了新的感...

1658
来自专栏数据和云

Delphi 程序错误写法造成Oracle数据库负载异常

作者介绍 ? 张洪涛 富士康 DBA 注意:本文涉及的问题在9i的环境中测试的,经验证,同样适用于11g. 在用Toad的SGA Trace工具监控我们的Or...

2756
来自专栏影子

springboot添加多数据源连接池并配置Mybatis

2273
来自专栏MasiMaro 的技术博文

数据更新接口与延迟更新

title: 数据更新接口与延迟更新 tags: [OLEDB, 数据库编程, VC++, 数据库] date: 2018-02-12 14:29:35 ...

952
来自专栏杨建荣的学习笔记

缓慢的update语句性能分析(r6笔记第61天)

最近处理一个问题的时候,先是收到DB time升高的报警,然后查看DB time的情况发现,已经有近1000%的负载了。 ? 带着好奇心想看看到底是什么样的...

2675
来自专栏用户2442861的专栏

Spring+Mybatis+SpringMVC后台与前台分页展示实例(附工程)

      林炳文Evankaka原创作品。转载请注明出处http://blog.csdn.net/evankaka

672
来自专栏.NET开发那点事

.NET开源高性能Socket通信中间件Helios介绍及演示

  Helios是一套高性能的Socket通信中间件,使用C#编写。Helios的开发受到Netty的启发,使用非阻塞的事件驱动模型架构来实现高并发高吞吐量。H...

1052
来自专栏乐沙弥的世界

SQL*Plus 执行脚本时传递参数(@script_name var1,var2)

      在使用sqlplus执行sql脚本时,经常碰到向脚本传递参数的情形。类似于shell脚本的参数传递,我们同样可以向sql脚本传递参数,其方法是脚本后...

2041
来自专栏cmazxiaoma的架构师之路

SpringBoot之路(一)之初识SpringBoot

2462
来自专栏数据之美

Hive 常见问题与技巧【Updating】

1Q: 是否有像类似于phpmyadmin一样的hive查询客户端,能以界面的方式查询hive语句和导出数据 A: 有的,客户端的话可以使用squirre...

1977

扫码关注云+社区