搭建node服務(二):操作MySQL

2020-09-05 宜信技術學院


為了進行複雜信息的存儲和查詢,服務端系統往往需要資料庫操作。資料庫分為關係型資料庫和非關係型資料庫,關係型資料庫有MySQL、Oracle、SQL Server等,非關係型資料庫有Redis(常用來做緩存)、MongoDB等。MySQL是目前很流行的資料庫,本文將要介紹如何在node服務中進行MySQL資料庫操作。

一、安裝依賴



npm install mysql --save

或者

yarn add mysql

二、建立連接


要想進行資料庫操作就需要和資料庫建立連接,然後通過連接進行資料庫的操作。MySQL的資料庫連接方式有以下幾種:

  • mysql.createConnection() 每次請求建立一個連接
  • mysql.createPool() 創建連接池,從連接池中獲取連接
  • mysql.createPoolCluster() 創建連接池集群,連接池集群可以提供多個主機連接

mysqljs文檔中推薦使用第一種方式:每次請求建立一個連接,但是由於頻繁的建立、關閉資料庫連接,會極大的降低系統的性能,所以我選擇了使用連接池的方式,如果對性能有更高的要求,安裝了MySQL 集群,可以選擇使用連接池集群。

1. 資料庫配置

將資料庫相關的配置添加到公用的配置文件中,方便項目的初始化。

  • config.js

module.exports = { … // mysql資料庫配置 mysql: { // 主機 host: &39;, // 埠 port: 3306, // 用戶名 user: &39;, // 密碼 password: &39;, // 資料庫名 database: &39;, // 連接池允許創建的最大連接數,默認值為10 connectionLimit: 50, // 允許掛起的最大連接數,默認值為0,代表掛起的連接數無限制 queueLimit: 0 }};

connectionLimit 和 queueLimit 是數據連接池特有的配置項。

  • connectionLimit 是指連接池允許創建的最大連接數,默認值為10。當獲取連接時,如果連接池中有空閒的連接則直接返回一個空閒連接。如果所有連接都被佔用,則判斷連接池中的連接數是否達到了允許的最大數,如果未達到則創建新的連接,如果已達到則獲取連接的請求掛起,等待其他請求完成操作後釋放的連接。
  • queueLimit 是指允許掛起的最大連接數,默認值為0,代表掛起的連接數無限制。當連接池中允許創建的所有連接都被佔用時,獲取連接的請求掛起,等待可用的連接,所有掛起的請求形成一個隊列,queueLimit則是指這個隊列的最大長度。需要注意的是,當queueLimit為0時並不表示不允許掛起,而是表示對掛起的數目沒有限制。


2.創建連接池

  • db/pool.js

/** * 資料庫連接池 */const mysql = require(&39;);const config = require(&39;);// 創建資料庫連接池const pool = mysql.createPool(config.mysql);pool.on(&39;, function (connection) { console.log(`獲取資料庫連接 [${connection.threadId}]`);});pool.on(&39;, function (connection) { console.log(`創建資料庫連接 [${connection.threadId}]`);});pool.on(&39;, function () { console.log(&39;);});pool.on(&39;, function (connection) { console.log(`資料庫連接 [${connection.threadId}] 已釋放`);});module.exports = pool;

創建資料庫連接池pool後,就可以通過pool獲取資料庫連接了,另外通過監聽連接池的事件可以了解連接池中連接的使用情況。如果將connectionLimit 設為2,queueLimit 設為0,當同時有5個請求獲取資料庫連接時,線程池的事件日誌如下:

正在等待可用資料庫連接正在等待可用資料庫連接正在等待可用資料庫連接創建資料庫連接 [1011]獲取資料庫連接 [1011]資料庫連接 [1011] 已釋放獲取資料庫連接 [1011]創建資料庫連接 [1012]獲取資料庫連接 [1012]資料庫連接 [1011] 已釋放獲取資料庫連接 [1011]資料庫連接 [1012] 已釋放獲取資料庫連接 [1012]資料庫連接 [1011] 已釋放資料庫連接 [1012] 已釋放

由於線程池允許的最大連接數是2,5個請求中會有2個請求能夠得到連接,另外3個請求掛起等待可用連接。由於創建資料庫連接的代價比較大,線程池在創建連接時採用「懶漢式」,也就是,用到時才創建。先得到連接的請求在完成操作後釋放連接,放回到連接池,然後掛起的請求從線程池取出空閒的連接進行操作。

三、執行操作


由於mysql 模塊的接口都為回調方式的,為了操作方便簡單地將接口封裝為Promise,相關方法封裝如下:

const pool = require(&39;);// 獲取連接function getConnection () { return new Promise((resolve, reject) => { pool.getConnection((err, connection) => { if (err) { console.error(&39;, err) reject(err); } else { resolve(connection); } }); });}// 開始資料庫事務function beginTransaction (connection) { return new Promise((resolve, reject) => { connection.beginTransaction(err => { if (err) { reject(err); } else { resolve(); } }); });}// 提交資料庫操作function commit (connection) { return new Promise((resolve, reject) => { connection.commit(err => { if (err) { reject(err); } else { resolve(); } }); })}// 回滾資料庫操作function rollback (connection) { return new Promise((resolve, reject) => { connection.rollback(err => { if (err) { reject(err); } else { resolve(); } }); })}

1. 執行普通操作

對於不需要使用事務的普通操作,獲取資料庫連接connection後,使用connection進行資料庫操作,完成後釋放連接到連接池,則執行完成一次操作。

  • db/execute.js

/** * 執行資料庫操作【適用於不需要事務的查詢以及單條的增、刪、改操作】 * 示例: * let func = async function(conn, projectId, memberId) { ... }; * await execute( func, projectId, memberId); * @param func 具體的資料庫操作異步方法(第一個參數必須為資料庫連接對象connection) * @param params func方法的參數(不包含第一個參數 connection) * @returns {Promise.<*>} func方法執行後的返回值 */async function execute (func, ...params) { let connection = null; try { connection = await getConnection() let result = await func(connection, ...params); return result } finally { connection && connection.release && connection.release(); }}

2. 執行事務操作

對於很多業務都需要執行事務操作,例如:銀行轉帳,A帳戶轉帳給B帳戶 100元,這個業務操作需要執行兩步,從A帳戶減去100元,然後給B帳戶增加100元。兩個子操作必須全部執行成功才能完成完整的業務操作,如果任意子操作執行失敗就需要撤銷之前的操作,進行回滾。

對於需要使用事務的操作,獲取資料庫連接connection後,首先需要調用connection.beginTransaction() 開始事務,然後使用connection進行多步操作,完成後執行connection.commit() 進行提交,則執行完成一次事務操作。如果在執行過程中出現了異常,則執行connection.rollback() 進行回滾操作。

  • db/execute.js

/** * 執行資料庫事務操作【適用於增、刪、改多個操作的執行,如果中間數據操作出現異常則之前的資料庫操作全部回滾】 * 示例: * let func = async function(conn) { ... }; * await executeTransaction(func); * @param func 具體的資料庫操作異步方法(第一個參數必須為資料庫連接對象connection) * @returns {Promise.<*>} func方法執行後的返回值 */async function executeTransaction(func) { const connection = await getConnection(); await beginTransaction(connection); let result = null; try { result = await func(connection); await commit(connection); return result } catch (err) { console.error(&39;); await rollback(connection); throw err; } finally { connection && connection.release && connection.release(); }}


四、增刪改查


增刪改查是處理數據的基本原子操作,將這些操作根據操作的特點進行簡單的封裝。

  • db/curd.js

/** * 查詢操作 * @param connection 連接 * @param sql SQL語句 * @param val SQL參數 * @returns {Promise} resolve查詢到的數據數組 */function query (connection, sql, val) { // console.info(&39;, sql, &39;, val); return new Promise((resolve, reject) => { connection.query(sql, val, (err, rows) => { if (err) { console.error(&39;, sql, &39;, val); reject(err); } else { let results = JSON.parse(JSON.stringify(rows)); resolve(results); } }); });}/** * 查詢單條數據操作 * @param connection 連接 * @param sql SQL語句 * @param val SQL參數 * @returns {Promise} resolve查詢到的數據對象 */function queryOne (connection, sql, val) { return new Promise((resolve, reject) => { query(connection, sql, val).then( results => { let result = results.length > 0 ? results[0] : null; resolve(result); }, err => reject(err) ) });}/** * 新增數據操作 * @param connection 連接 * @param sql SQL語句 * @param val SQL參數 * @param {boolean} skipId 跳過自動添加ID, false: 自動添加id,true: 不添加id * @returns {Promise} resolve 自動生成的id */function insert (connection, sql, val, skipId) { let id = val.id; if (!id && !skipId) { id = uuid(); val = {id, ...val}; } return new Promise((resolve, reject) => { // console.info(&39;, sql, &39;, val); connection.query(sql, val, (err, results) => { if (err) { console.error(&39;, sql, &39;, val); reject(err); } else { resolve(id); } }); });}/** * 更新操作 * @param connection 連接 * @param sql SQL語句 * @param val SQL參數 * @returns {Promise} resolve 更新數據的行數 */function update (connection, sql, val) { // console.info(&39;, sql, &39;, val); return new Promise((resolve, reject) => { connection.query(sql, val, (err, results) => { if (err) { console.error(&39;, sql, &39;, val); reject(err); } else { resolve(results.affectedRows); } }); });}/** * 刪除操作 * @param connection 連接 * @param sql SQL語句 * @param val SQL參數 * @returns {Promise} resolve 刪除數據的行數 */function del (connection, sql, val) { // console.info(&39;, sql, &39;, val); return new Promise((resolve, reject) => { connection.query(sql, val, (err, results) => { if (err) { console.error(&39;, sql, &39;, val); reject(err); } else { // console.log(&39;, results); resolve(results.affectedRows); } }); });}

五、代碼分層


將代碼分層可以降低代碼的耦合度,提高可復用性、可維護性,這裡將代碼分成了3層:Dao層、Service層和Controller層。

  • DAO層:主要負責數據持久化工作;
  • Service層:主要負責業務模塊的邏輯設計,此層的業務實現,可以調用DAO層的接口;
  • Controller層:負責具體的業務模塊流程的控制,在此層可以調用Service層的接口。

1.DAO層

  • dao/userDao.js

const { query, queryOne, update, insert, del } = require(&39;);class UserDao { static async queryUserById (connection, id) { const sql = `SELECT user.id, user.account, user.name, user.email, user.phone, user.birthday, user.enable, user.deleteFlag, user.creator, user.createTime, user.updater, user.updateTime FROM sys_user user WHERE user.id = ?`; const user = await queryOne(connection, sql, id); return user; } …}module.exports = UserDao;

2.Service層

  • service/userService.js 簡單調用一個DAO層方法:

const { execute, executeTransaction } = require(&39;);const UserDao = require(&39;);class UserService { static async findUserById (id) { return await execute(UserDao.queryUserById, id);}…}module.exports = UserService;

對於複雜些的業務邏輯可以使用匿名函數來實現:

static async findUserWithRoles (id) { return await execute (async connection => { const user = await UserDao.queryUserById(connection, id); if (user) { user.roles = await RoleDao.queryRolesByUserId(connection, id); } return user; });}

如果要執行事務操作,則需要使用executeTransaction 方法:

static async updateUserRoleRelations (userId, roleIds) { return await executeTransaction(async connection => { const relations = await UserDao.queryUserRoleRelations(connection, userId); const oldRoleIds = relations.map(item => item.roleId); const newRoleIds = roleIds || []; // 新增的角色數組 const addList = []; // 移除的角色數組 const removeList = []; newRoleIds.forEach(roleId => { if (oldRoleIds.indexOf(roleId) === -1) { addList.push(roleId); } }); oldRoleIds.forEach(roleId => { if (newRoleIds.indexOf(roleId) === -1) { removeList.push(roleId); } }); if (addList.length > 0) { await UserDao.insertUserRoleRelations(connection, userId, addList); } if (removeList.length > 0) { await UserDao.deleteUserRoleRelations(connection, userId, removeList); } });}

3.Controller層

  • controler/userController.js

const UserService = require(&39;);class UserControler { static async getUserById (ctx) { // 用戶ID const id = ctx.params.id; // 是否包含用戶角色信息,如果withRoles 為 &34; 表示需要包含角色信息 const withRoles = ctx.query.withRoles; let user; if (withRoles === &39;) { user = await UserService.findUserWithRoles(id); } else { user = await UserService.findUserById(id); } if (user) { ctx.body = user; } else { ctx.body = { code: 1004, msg: &39; } } } …}module.exports = UserControler;

此示例基於Koa框架,controller 層實現完成後需要添加路由:

const router = new KoaRouter();const UserController = require(&39;);// 獲取指定ID的用戶router.get(&39;, UserController.getUserById);// 獲取所有用戶router.get(&39;, UserControler.getUsers);

對於Koa框架如何使用,這裡不再介紹,路由添加完畢後,啟動服務,即可使用這些接口,如果本地服務啟動的埠為3000,接口請求地址如下:

  • http://localhost:3000/users/3571a123-0454-49b4-a2bc-8b30a37f0b14
  • http://localhost:3000/users/3571a123-0454-49b4-a2bc-8b30a37f0b14?withRoles=1
  • http://localhost:3000/users/


六、說明&總結


本文介紹了mysql模塊的基本使用,對其進行了簡單封裝,並提供了使用示例。除了使用mysql模塊來操作資料庫,也可以使用mysql2模塊,mysql2的基本用法與mysql一致,另外mysql2還支持Promise,使用起來更方便。本文相關的代碼已提交到GitHub以供參考,項目地址:[https://github.com/liulinsp/node-server-typeorm-demo](https://github.com/liulinsp/node-server-typeorm-demo)。

拓展閱讀:

相關焦點

  • 搭建node服務(三):使用TypeScript
    本文將介紹如何在node服務中使用TypeScript。)?匹配一個任意字符(不包括目錄分隔符)**/ 遞歸匹配任意子目錄2.五、總結本文介紹了如何在node服務中使用TypeScript,具體的TypeScript語法規則網上有很多相關的資料,這裡就不再介紹了。
  • 搭建node服務(一):日誌處理
    本文將要介紹如何在node服務中處理日誌。一、技術選型選擇了3種主流的技術進行對比:1.1 log4jslog4js是一種node日誌管理工具,可以將自定義格式的日誌輸出到各種渠道。由於團隊內部服務端系統很多是基於java的,這些系統大部分使用log4j生成日誌。日誌管理相關的日誌歸集系統和日誌查詢系統對log4j格式的日誌支持的更好,所以自己最終選用了log4j的JavaScript版log4js來生成日誌,下面會對log4js的基本使用進行介紹。
  • mysql主從搭建詳解已出,手把手教你!看完立馬會
    前段時間,剛好學了這個mysql主從搭建,為了記錄自己學習成果,特以筆記形式記錄下來。如有不足之處,謝謝指正!庫一般不同步)binlog-ignore-db = mysql# 開啟二進位日誌功能,可以隨便取,最好有含義(關鍵就是這裡了)log-bin = mysql-bin# 為每個session 分配的內存,在事務過程中用來存儲二進位日誌的緩存binlog_cache_size = 1M# 主從複製的格式(mixed,statement,row,默認格式是statement
  • Apache+MySQL+Redis+nodejs
    Linux+Apache+MySQL+Redis+nodejscentos7.8apache(httpd-2.4.39)Mysql-5.7.29Redis-5.0.7nodejs(node-12.2.0)
  • OpenResty、PHP-fpm與NodeJs操作MySQL的性能對比
    但Node只起了一個進程,而且 strace 確認了確實是非阻塞的(不像 node-mysql-libmysqlclient 和 node-db-mysql 那樣濫用 OS 線程池玩阻塞通信),很是奇怪
  • MySQL資料庫高可用集群搭建-PXC集群部署
    Percona XtraDB Cluster(下文簡稱PXC集群)提供了MySQL高可用的一種實現方法。集群是有節點組成的,推薦配置至少3個節點,但是也可以運行在2個節點上。PXC的優點:1)服務高可用;2)數據同步複製(並發複製),幾乎無延遲;3)多個可同時讀寫節點,可實現寫擴展,不過最好事先進行分庫分表,讓各個節點分別寫不同的表或者庫,避免讓galera解決數據衝突;4)新節點可以自動部署,部署操作簡單;5)數據嚴格一致性,尤其適合電商類應用;6)完全兼容MySQL; 一些名詞介紹:WS:write set寫數據集,寫/更新事務IST:Incremental
  • Node js 連接 MySQL 與 MongoDB
    MySQL1、設計表首先通過可視化工具進行表的設計,然後添加幾條測試數據:2、安裝 Node.js 連接 MySQL 的包npm i mysql -d複製代碼3、連接 MySQLMySQL.js// 引入 mysql 包const mysql = require('mysql
  • 在Node.js中使用MySQL&MySQL JavaScript客戶端
    我會向你解釋如何使用該模塊連接到 MySQL 資料庫,執行常規的 CRUD 操作,之後就是對存儲的過程進行檢查,以及對用戶的輸入進行轉義這些技術。 這個頗受歡迎的教程在 2017 年 11 月 07 日進行了更新。其中的修改包括將語法更新到了 ES6,解決了node-mysql 模塊被重新命名的問題,增加了更多對初學者友好的文字說明,並在 ORM 上新增加了一個部分。
  • Mysql資料庫的編譯安裝方法(為搭建zabbix系統)
    Zabbix學習筆記(四)- Mysql資料庫的編譯安裝方法(為搭建zabbix系統)1、 環境介紹作業系統:Centos7.3 Linux版本:MySQL-5.6.44鏡像源:阿里鏡像源2、 依賴包等yum install -y gcc gcc-c++
  • MySQL系列二 - 搭建MySQL主從集群
    前面一章我們在centos上搭建好了MySQL資料庫,今天我們搭建一個主從同步的MySQL簡單集群。上一章是在虛擬機(192.168.64.123)上搭建的,我用的是vmware15虛擬機搭建的,今天在擴展一臺節點用於搭建主從集群,計劃 IP:192.168.64.123 虛擬機作為主節點,新建的虛擬機 IP:192.168.64.124 做為從節點。124的資料庫搭建什麼的參照上一章步驟執行就行。
  • Node.js 學習資料和教程(值得收藏)
    Node.jsHomePageNode官網七牛鏡像Infoq深入淺出Node.js系列(進階必讀)Node.js中文文檔被誤解的 Node.jsNode.js C++ addon編寫實戰系列熱門node.js模塊排行榜,方便找出你想要的模塊nodejs多線程,真正的非阻塞淺析nodejs的buffer類利用libuv編寫異步多線程的addon實例Node.js
  • 搭建node服務(四):Decorator裝飾器
    Decorator(裝飾器)是ECMAScript中一種與class相關的語法,用於給對象在運行期間動態的增加功能。Node.js還不支持Decorator,可以使用Babel進行轉換,也可以在TypeScript中使用Decorator。本示例則是基於TypeScript來介紹如何在node服務中使用Decorator。
  • 資料庫基礎:mysql主從集群搭建
    我們進一步詳細介紹原理的細節, 這有一張圖:上圖中有兩個伺服器, 演示了從一個主伺服器(master) 把數據同步到從伺服器(slave)的過程。這是一個主-從複製的例子。對於一個mysql伺服器, 一般有兩個線程來負責複製和被複製。當開啟複製之後。1. 作為主伺服器Master, 會把自己的每一次改動都記錄到 二進位日誌 Binarylog 中。(從伺服器會負責來讀取這個log, 然後在自己那裡再執行一遍。)
  • mysql實現高可用架構之MHA實戰篇—搭建MHA環境
    本實驗環境共有四個節點, 其角色分配如下(實驗機器均為centos 7.3)其中master對外提供寫服務,備選master(實際的slave,主機名server03)提供讀服務,slave也提供相關的讀服務,一旦master宕機,將會把備選master提升為新的master,slave指向新的master。
  • 搭建簡單的Oracle到MySQL同步
    1.1、A上的OGG環境搭建(1)上傳OGG介質到A伺服器並解壓cd /app/ogg/ogg_homeunzip 122022_ggs_Linux_x64_MySQL_64bit.zip(3)配置ogg子目錄.
  • Docker + Nodejs + Kafka + Redis + MySQL搭建簡單秒殺環境
    seckill service在更新完redis之後,會發送一條消息給kafka表示一次成功的秒殺  seckill kafka consumer: 基於nodejs的kafka consumer,會從kafka中去獲取秒殺成功的消息,處理並且存儲到mysql中  mysql:一個mysql的docker container,最終秒殺成功的請求都會對應著資料庫表中的一條記錄
  • JavaScript學習筆記(二十四)—MYSQL基礎操作
    MYSQLmysql 是一個資料庫的名字和 php 合作的比較好的資料庫之前我們說過一個問題,前端向後端索要數據,後端就是去資料庫中查詢數據,返回給前端接下來就聊聊使用 php 操作資料庫>MySQL 是最流行的關係型資料庫管理系統(非關係型資料庫簡略介紹) 關係資料庫管理系統(Relational Database Management System)的特點 數據以表格的形式出現 每行為各種記錄名稱 許多的行和列組成一張表單 若干的表單組成database 主鍵:主鍵是唯一的。
  • nodejs之express詳解(一)
    >二、前端頁面2.3 node連結資料庫相對數據的讀取和操作就需要操作數據node操作數據比 php操作更簡單 ,我們看下如何操作數據,這時候還是需要apache資料庫連結資料庫語法連結資料庫打開本地xampp啟動資料庫 創建數據
  • 秒級搭建MySQL資料庫服務,太香了
    MySQL資料庫服務。這種方案門檻低,對於非資料庫專業人員也能秒級搭建好一條MySQL服務。搭建步驟拉取MySQL資料庫鏡像docker pull mysql:latest直接執行這個命令,意思是拉取最新的鏡像,但是實際項目可能需要制定的資料庫版本,所以這裡需要制定標籤,拉取需要的鏡像
  • node學習筆記(三十八)
    簡單的說 Node.js 就是運行在服務端的 JavaScript。擴展;I : input即輸入埠o : output即輸出埠CPU與外部設備、存儲器的連接和數據交換都需要通過接口設備來實現,前者稱為I/O接口,後者被稱為存儲器接口二、什麼是V8引擎