原文地址:koa+mysql+vue+socket.io全棧開發之數據訪問篇javascript
後端搭起大致的框架後,接着涉及到的就是如何將數據持久化的問題,也就是對數據庫進行 CURD 操做。前端
關於數據庫方案, mongodb 和 mysql 都使用過,但我選用的是 mysql,緣由:vue
node-mysql 是用 sql 語句對 mysql 進行操做的庫, 並無使用 Sequelize 這種 orm。由於我對 sql 熟悉,原生開發效率高。java
鏈接數據庫我選用 鏈接池的方式,這種方式能高效的利用數據庫鏈接node
//dbPool.js const 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。mysql
module.exports={ host: "localhost", port: "3306", user: "root", password: "jeff", database: "chatdb", charset : 'utf8mb4',//utf8mb4才能保存emoji multipleStatements: true,// 可同時查詢多條語句, 但不能參數化傳值 connectionLimit: 100 //鏈接數量 };
基本的代碼編寫方式以下,每一個方法基本都是這麼一種流程,獲取數據庫鏈接,執行 sql 語句,返回結果,處理異常。git
exports.queryInfo = function (params, callback){ pool.query('select ...', params, function (error, result, fields) { if (error) { log(error); callback(false); } else callback(result) }); }
這形成了一大堆重複的樣板代碼,咱們須要封裝它,用 JavaScript 高階函數特性 很容易就能實現,同時加上 Promise,調用時就能方便地用 async await 了,還有日誌記錄功能也加上。github
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 下。sql
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 的功能須要用到,來看一下 node-mysql 官方的例子,層層回調😢,若是每一個業務都要這樣編寫,簡直不能忍,咱們仍是手動封裝下事務吧。mongodb
// 官方的事務樣例 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); }); }); }) }
都封裝完畢,最後就是調用, 就以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,這裏以部分表爲例,項目使用到的表要多得多。我這裏還寫了些可有可無的觸發器處理 數據插入時間 和數據修改時間,這是我的的習慣。徹底能夠不用觸發器,直接在代碼裏面賦值,不影響使用。有用到 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 ROW begin if (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 ROW begin if ((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 ROW begin if (new.`create_date` = 0 or new.`create_date` is null) then set new.`create_date` = unix_timestamp(now()); end if; END $$
接着就是咱們的大前端部分了,將會用到 vue,vuex,請繼續關注。