博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
koa+mysql+vue+socket.io全栈开发之数据访问篇
阅读量:6223 次
发布时间:2019-06-21

本文共 10390 字,大约阅读时间需要 34 分钟。

原文地址:

后端搭起大体的框架后,接着涉及到的就是如何将数据持久化的问题,也就是对数据库进行 CURD 操作。

关于数据库方案,mongodbmysql 都使用过,但我选用的是 mysql,原因:

  1. 目前为止 mysqlmongodb 性能相差不大,尤其是 mysql 8.0 版本,速度非常快,查询数据是 mysql 更快,写数据方面 mongodb 则更胜一筹;
  2. mysql 建立 关联数据要更方便些,比如: 一对多,多对多的关系;
  3. mysql 作为关系型数据库,数据一致性方面更好,尤其是事务用起来更顺手;
  4. 本人对 sql 操作比较得心应手,毕竟大部分项目用得都是 mysql,而 mongodb 在正式些的项目上用的就少了,而且目前关系型数据库也在进化,postgrepmysql 都已经支持 json了。

node-mysql

node-mysql 是用 sql 语句对 mysql 进行操作的库, 并没有使用 这种 orm。因为我对 sql 熟悉,原生开发效率高。

连接池

连接数据库我选用 连接池的方式,这种方式能高效的利用数据库连接

//dbPool.jsconst mysql = require('mysql');const dbconfig = require('../config/db');const log = require('../common/logger');let pool = null;/** * get the connection pool of database * 获取数据库连接池 */exports.getPool = function () {    if (!pool) {        log.info("creating pool");        pool = mysql.createPool(dbconfig);    }    return pool;}复制代码

数据库配置文件

emoji 格式要用 utf8mb4 格式存储,所以这里连接字符集选用 utf8mb4,当然客户端和数据结果集 一样也要设置为 utf8mb4

module.exports={	host: "localhost",	port: "3306",	user: "root",	password: "jeff",	database: "chatdb",	charset : 'utf8mb4',//utf8mb4才能保存emoji	multipleStatements: true,// 可同时查询多条语句, 但不能参数化传值	connectionLimit: 100 //连接数量};复制代码

Dao的编写

基本的代码编写方式如下,每个方法基本都是这么一种流程,获取数据库连接,执行 sql 语句,返回结果,处理异常。

exports.queryInfo = function (params, callback){    pool.query('select ...', params, function (error, result, fields) {        if (error) {          log(error);          callback(false);        }        else callback(result)    }); }复制代码

exportDao

这造成了一大堆重复的样板代码,我们需要封装它,用 JavaScript 高阶函数特性 很容易就能实现,同时加上 Promise,调用时就能方便地用 async await 了,还有日志记录功能也加上。

const pool = require("./dbPool").getPool();const log = require('../common/logger');/** * export named query function */const exportDao = opts => Object.keys(opts).reduce((next, key) => {    next[key] = (...args) => new Promise((resolve, reject) => {        if (opts[key]) args.unshift(opts[key]);        log.info('====== execute sql ======')        log.info(args);        pool.query(...args, (err, result, fields) => {
// fields is useless if (err) reject(err) else resolve(result); }); }); return next;}, {});复制代码

userDao文件为例,使用 exportDao 直接就能把里面的 key-value 对象输出为 以key 为方法名的dao方法,挂载到 module.exports 下。

const { exportDao } = require('./common');//直接就exports里面的key值对应的方法module.exports = exportDao({	sql: null,// 有些时候需要直接写sql	count: 'select count(*) as count from user where ?',	getUser: 'select * from user where ?',	insert: 'insert into user set ?',	update: 'update user set ? where id = ?',	delete: 'delete from user where ?'});/* 最终输出格式module.exports = {  sql:() => {},  count:() => {},  ...}*/复制代码

transaction

还有事务 transaction 的功能需要用到,来看一下 node-mysql 官方的例子,层层回调?,如果每个业务都要这样编写,简直不能忍,我们还是手动封装下事务吧。

// 官方的事务样例pool.getConnection(function(err, connection) {    if (err) { throw err; }    connection.beginTransaction(function(err) {    if (err) { throw err; }        connection.query('INSERT INTO posts SET title=?', title, function (error, results, fields) {            if (error) {                return connection.rollback(function() {                    throw error;                });            }            var log = 'Post ' + results.insertId + ' added';            connection.query('INSERT INTO log SET data=?', log, function (error, results, fields) {                if (error) {                    return connection.rollback(function() {                    throw error;                    });                }                connection.commit(function(err) {                    if (err) {                        return connection.rollback(function() {                            throw err;                        });                    }                    console.log('success!');                });            });        });    });});复制代码

下面就是封装好的事务方法,调用参数为数组,数组项既可以是 sql 字符串,也可以是 node-mysql 中的带参数传值的数组,这才是给人用的嘛,心情好多了。推荐还是用 参数化传值,这样可以避免 sql 注入,如果确实要用sql字符串,可以调用 mysql.escape 方法对 参数 进行装换。

//调用封装后的事务const rets = await transaction([     ["insert into user_group values (?,?)",[11,11]],     ["insert into user_friend set ? ",{
user_id:'12',friend_id:12}], 'select * from user' ]);/** * sql transaction 封装后的事务 * @param {Array} list */const transaction = list => { return new Promise((resolve, reject) => { if (!Array.isArray(list) || !list.length) return reject('it needs a Array with sql') pool.getConnection((err, connection) => { if (err) return reject(err); connection.beginTransaction(err => { if (err) return reject(err); log.info('============ begin execute transaction ============') let rets = []; return (function dispatch(i) { let args = list[i]; if (!args) {
//finally commit connection.commit(err => { if (err) { connection.rollback(); connection.release(); return reject(err); } log.info('============ success executed transaction ============') connection.release(); resolve(rets); }); } else { log.info(args); args = typeof args == 'string' ? [args] : args; connection.query(...args, (error, ret) => { if (error) { connection.rollback(); connection.release(); return reject(error); } rets.push(ret); dispatch(i + 1); }); } })(0); }); }); })}复制代码

controller 调用 dao

都封装完毕,最后就是调用, 就以apply控制器为例,其中的 apply 方法是普通调用,accept 方法则使用了事务进行处理。

const { stringFormat } = require('../common/util')const { transaction } = require('../daos/common')const applyDao = require('../daos/apply')exports.apply = async function (ctx) {    const form = ctx.request.body;    const token = await ctx.verify();    const ret = await applyDao.apply({ ...form, from_id: token.uid });    if (!ret.affectedRows) {        return ctx.body = {            code: 2,            message: '申请失败'        };    }    ctx.body = {        code: 0,        message: '申请成功',        data:ret.insertId    };}exports.accept = async function (ctx) {    const { id, friend_id } = ctx.request.body;    const token = await ctx.verify();    const ret = await transaction([// 非用户输入的 id,没有使用 escape 进行转换。        ['update apply set status = 1 where id = ? and to_id = ?', [id, token.uid]],        stringFormat("replace into user_friend values ('$1','$2'),('$2','$1')", token.uid, friend_id)    ]);    if (!ret[0].affectedRows || !ret[1].affectedRows) {        return ctx.body = {            code: 2,            message: '添加好友失败'        };    }    ctx.body = {        code: 0,        message: '添加好友成功'    };}复制代码

sql脚本

当然还需要定义数据结构,有很多工具可以方便建表和建生产sql,这里以部分表为例,项目使用到的表要多得多。我这里还写了些无关紧要的触发器处理 数据插入时间 和数据修改时间,这是个人的习惯。完全可以不用触发器,直接在代码里面赋值,不影响使用。有用到 emoji 的数据表,记得要用 utf8mb4 格式。

create database if not exists chatdb;use chatdb;drop table if exists `user`;CREATE TABLE `user` (  `id` char(36) NOT NULL DEFAULT '' COMMENT '主键',  `name` varchar(50) DEFAULT NULL COMMENT '用户名',  `num` int(8) DEFAULT NULL  COMMENT '用户号码',  `salt` varchar(13) DEFAULT NULL COMMENT '加密的盐',  `hash_password` varchar(64) DEFAULT NULL COMMENT '加密后的密码',  `email` varchar(50) NOT NULL COMMENT 'email地址',  `nick` varchar(50) DEFAULT NULL COMMENT '昵称',  `avatar` varchar(200) DEFAULT NULL COMMENT '头像',  `signature` varchar(200) DEFAULT NULL COMMENT '个性签名',  `status` tinyint(1) DEFAULT 0 COMMENT '状态(0 离线 1 在线 2 隐身)',  `is_admin` tinyint(1) DEFAULT 0 COMMENT '是否管理员',  `is_block` tinyint(1) DEFAULT 0 COMMENT '是否禁用',  `create_date` int(10) unsigned DEFAULT NULL COMMENT '注册时间',  `update_date` int(10) unsigned DEFAULT NULL COMMENT '更新时间',  PRIMARY KEY (`id`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='用户表'; drop table if exists `message`;CREATE TABLE `message` (  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',  `content` text NOT NULL COMMENT '内容',  `type` tinyint(1) DEFAULT 0 COMMENT '类型(0 用户 1 组群)',  `send_id` char(36) NOT NULL COMMENT '发送用户id',  `receive_id` char(36) DEFAULT NULL COMMENT '接收用户id',  `group_id` int(11) DEFAULT NULL COMMENT '组id',  `create_date` int(10) unsigned DEFAULT NULL COMMENT '创建时间',  PRIMARY KEY (`id`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='消息表';drop table if exists `user_message`;CREATE TABLE `user_message` (  `user_id` char(36) DEFAULT NULL COMMENT '接收用户id',  `send_id` char(36) NOT NULL COMMENT '发送用户id',  `message_id` int(11) NOT NULL COMMENT '消息id',  `is_read` tinyint(1) DEFAULT 0 COMMENT '是否读过(0 没有 1 读过)',  PRIMARY KEY (`send_id`,`message_id`)) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='用户消息连接表';-- user表insert触发器delimiter $$create trigger `user_insert` before insert on `user`for each ROWbeginif (new.id = '' or new.id is null)	then set new.id = uuid();end if;if (new.num = 0 or new.num is null)	then set new.num = 1000;end if;if (new.`create_date` = 0 or new.`create_date` is null)	then set new.`create_date` = unix_timestamp(now());end if;if (new.`update_date` = 0 or new.`update_date` is null)	then set new.`update_date` = unix_timestamp(now());end if;END$$-- user表update触发器delimiter $$create trigger `user_update` before update on `user`for each ROWbeginif ((new.`name` <> old.`name`) or (new.`name` is not null and old.`name` is null) 	or (new.`email` <> old.`email`) or (new.`email` is not null and old.`email` is null)	or (new.`nick` <> old.`nick`) or (new.`nick` is not null and old.`nick` is null)	or (new.`avatar` <> old.`avatar`) or (new.`avatar` is not null and old.`avatar` is null)	or (new.`signature` <> old.`signature`) or (new.`signature` is not null and old.`signature` is null))	then set new.`update_date` = unix_timestamp(now());end if;END$$-- message表insert触发器delimiter $$create trigger `message_insert` before insert on `message`for each ROWbeginif (new.`create_date` = 0 or new.`create_date` is null)	then set new.`create_date` = unix_timestamp(now());end if;END$$复制代码

后续

接着就是我们的大前端部分了,将会用到 vue,vuex,请继续关注。

转载于:https://juejin.im/post/5caee312e51d456e770bdc89

你可能感兴趣的文章
原码、反码、补码
查看>>
opencv 4.0 + linux + cuda静态编译
查看>>
Qt MVD框架下修改视图中数据项/标题的背景颜色说明(Qt 5.10.1)
查看>>
软件工程第二次作业——制作网站
查看>>
c# IL 指令集
查看>>
HDU 1231:最大连续子序列(DP)
查看>>
Mysql基础操作
查看>>
Winhex数据恢复学习笔记(四)
查看>>
合工大OJ 1337 一加二减三
查看>>
Python学习之==>函数
查看>>
Chromium base库分割字符串SplitString
查看>>
win95+ie3-win10+ie11 浏览器执行漏洞
查看>>
BZOJ 3110 [Zjoi2013]K大数查询 ——整体二分
查看>>
文本溢出省略号显示时,水平位置发生偏移
查看>>
和真正的程序员在一起是怎样的体验
查看>>
Python之sys模块
查看>>
接口自动化框架好文
查看>>
K3Cloud调用存储过程
查看>>
第212天:15种CSS居中的方式,最全了
查看>>
SQL Server 2008 FILESTREAM特性管理文件
查看>>