您好,登錄后才能下訂單哦!
為了進行復雜信息的存儲和查詢,服務端系統往往需要數據庫操作。數據庫分為關系型數據庫和非關系型數據庫,關系型數據庫有MySQL、Oracle、SQL Server等,非關系型數據庫有Redis(常用來做緩存)、MongoDB等。MySQL是目前很流行的數據庫,本文將要介紹如何在node服務中進行MySQL數據庫操作。
npm install mysql --save
或者
yarn add mysql
要想進行數據庫操作就需要和數據庫建立連接,然后通過連接進行數據庫的操作。MySQL的數據庫連接方式有以下幾種:
mysqljs文檔中推薦使用第一種方式:每次請求建立一個連接,但是由于頻繁的建立、關閉數據庫連接,會極大的降低系統的性能,所以我選擇了使用連接池的方式,如果對性能有更高的要求,安裝了MySQL 集群,可以選擇使用連接池集群。
將數據庫相關的配置添加到公用的配置文件中,方便項目的初始化。
module.exports = {
…
// mysql數據庫配置
mysql: {
// 主機
host: 'localhost',
// 端口
port: 3306,
// 用戶名
user: 'root',
// 密碼
password: '123456',
// 數據庫名
database: 'server-demo',
// 連接池允許創建的最大連接數,默認值為10
connectionLimit: 50,
// 允許掛起的最大連接數,默認值為0,代表掛起的連接數無限制
queueLimit: 0
}
};
connectionLimit 和 queueLimit 是數據連接池特有的配置項。
/**
* 數據庫連接池
*/
const mysql = require('mysql');
const config = require('../config');
// 創建數據庫連接池
const pool = mysql.createPool(config.mysql);
pool.on('acquire', function (connection) {
console.log(`獲取數據庫連接 [${connection.threadId}]`);
});
pool.on('connection', function (connection) {
console.log(`創建數據庫連接 [${connection.threadId}]`);
});
pool.on('enqueue', function () {
console.log('正在等待可用數據庫連接');
});
pool.on('release', 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('./pool');
// 獲取連接
function getConnection () {
return new Promise((resolve, reject) => {
pool.getConnection((err, connection) => {
if (err) {
console.error('獲取數據庫連接失敗!', 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();
}
});
})
}
對于不需要使用事務的普通操作,獲取數據庫連接connection后,使用connection進行數據庫操作,完成后釋放連接到連接池,則執行完成一次操作。
/**
* 執行數據庫操作【適用于不需要事務的查詢以及單條的增、刪、改操作】
* 示例:
* 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();
}
}
對于很多業務都需要執行事務操作,例如:銀行轉賬,A賬戶轉賬給B賬戶 100元,這個業務操作需要執行兩步,從A賬戶減去100元,然后給B賬戶增加100元。兩個子操作必須全部執行成功才能完成完整的業務操作,如果任意子操作執行失敗就需要撤銷之前的操作,進行回滾。
對于需要使用事務的操作,獲取數據庫連接connection后,首先需要調用connection.beginTransaction() 開始事務,然后使用connection進行多步操作,完成后執行connection.commit() 進行提交,則執行完成一次事務操作。如果在執行過程中出現了異常,則執行connection.rollback() 進行回滾操作。
/**
* 執行數據庫事務操作【適用于增、刪、改多個操作的執行,如果中間數據操作出現異常則之前的數據庫操作全部回滾】
* 示例:
* 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('事務執行失敗,操作回滾');
await rollback(connection);
throw err;
} finally {
connection && connection.release && connection.release();
}
}
增刪改查是處理數據的基本原子操作,將這些操作根據操作的特點進行簡單的封裝。
/**
* 查詢操作
* @param connection 連接
* @param sql SQL語句
* @param val SQL參數
* @returns {Promise} resolve查詢到的數據數組
*/
function query (connection, sql, val) {
// console.info('sql執行query操作:\n', sql, '\n', val);
return new Promise((resolve, reject) => {
connection.query(sql, val, (err, rows) => {
if (err) {
console.error('sql執行失敗!', sql, '\n', 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('sql執行insert操作:\n', sql, '\n', val);
connection.query(sql, val, (err, results) => {
if (err) {
console.error('sql執行失敗!', sql, '\n', val);
reject(err);
} else {
resolve(id);
}
});
});
}
/**
* 更新操作
* @param connection 連接
* @param sql SQL語句
* @param val SQL參數
* @returns {Promise} resolve 更新數據的行數
*/
function update (connection, sql, val) {
// console.info('sql執行update操作:\n', sql, '\n', val);
return new Promise((resolve, reject) => {
connection.query(sql, val, (err, results) => {
if (err) {
console.error('sql執行失敗!', sql, '\n', 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('sql執行delete操作:\n', sql, '\n', val);
return new Promise((resolve, reject) => {
connection.query(sql, val, (err, results) => {
if (err) {
console.error('sql執行失敗!', sql, '\n', val);
reject(err);
} else {
// console.log('delete result', results);
resolve(results.affectedRows);
}
});
});
}
將代碼分層可以降低代碼的耦合度,提高可復用性、可維護性,這里將代碼分成了3層: Dao層、Service層和Controller層。
const { query, queryOne, update, insert, del } = require('../db/curd');
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;
const { execute, executeTransaction } = require('../db/execute');
const UserDao = require('../dao/userDao');
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);
}
});
}
const UserService = require('../service/userService');
class UserControler {
static async getUserById (ctx) {
// 用戶ID
const id = ctx.params.id;
// 是否包含用戶角色信息,如果withRoles 為 "1" 表示需要包含角色信息
const withRoles = ctx.query.withRoles;
let user;
if (withRoles === '1') {
user = await UserService.findUserWithRoles(id);
} else {
user = await UserService.findUserById(id);
}
if (user) {
ctx.body = user;
} else {
ctx.body = {
code: 1004,
msg: '用戶不存在!'
}
}
}
…
}
module.exports = UserControler;
此示例基于Koa框架,controller 層實現完成后需要添加路由:
const router = new KoaRouter();
const UserController = require('./controler/userControler');
// 獲取指定ID的用戶
router.get('/users/:id', UserController.getUserById);
// 獲取所有用戶
router.get('/users', UserControler.getUsers);
對于Koa框架如何使用,這里不再介紹,路由添加完畢后,啟動服務,即可使用這些接口,如果本地服務啟動的端口為3000,接口請求地址如下:
本文介紹了mysql模塊的基本使用,對其進行了簡單封裝,并提供了使用示例。除了使用mysql模塊來操作數據庫,也可以使用mysql2模塊,mysql2的基本用法與mysql一致,另外mysql2還支持Promise,使用起來更方便。本文相關的代碼已提交到GitHub以供參考,項目地址: https://github.com/liulinsp/node-server-typeorm-demo。
作者:劉琳
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。