和同事一塊兒有一個公司內部平臺的項目,平臺須要對於用戶上傳的圖片,視頻等資源進行管理和存儲。html
在項目一期,因爲申請DB資源的流程比較複雜,因此咱們僅僅將用戶上傳的內容記錄存儲在了localStorage
中,固然這是很不安全的,很是容易丟失,因此在二期的時候,咱們開始了接入DB的工做。前端
下面的整篇文章都會和這部分描述的技術棧相關,固然,sequelize
在實際業務場景的使用相關內容,其實自己和技術棧的關係並非很是大,若是你在sequelize
的使用過程當中遇到了問題,這裏或許會有解答~node
採用mysql做爲數據存儲引擎,其實自己也是無奈之舉,由於DBA告訴咱們目前MongoDB的資源不足,而且對於這種結構化數據的存儲,mysql對於將來將平臺擴展到整個公司使用,甚至對外開放,則是必須的。mysql
做爲一款比較成熟的node.js開發框架,公司toC端不少業務架構都使用了基於egg的ReactSSR,egg對於sequelize
的支持仍是比較好的,提供了專門的插件來輔助使用sequelize
進行DB接入。sql
這纔是本文的核心,sequelize
目前能夠說是目前最爲成熟的node.js ORM框架了。CRUD操做不可能徹底使用SQL語句進行,這樣很容易出現各類SQL漏洞,一個成熟的ORM框架能夠幫咱們避免掉這些風險,而且將CRUD操做封裝成對象函數方法以後,操做起來也更加方便,可是這樣會提高必定的開發學習成本。數據庫
總體的方案都出來了,剩下的就是爬坑。因爲之前仍是作過一些和數據庫有關的工做,SQL語句和部分ORM的實現還有過一點接觸,可是。。。我依然在坑裏栽了好久,長成了參天大樹,提及參,我就想到西遊記裏面的人蔘果。。。文體兩開花。npm
接入DB,首先須要考慮就是如何設計數據模型。固然這對於前端來講,仍是有一些難度的。因而請教了最近在合做的後端大哥。後端
通常來講,一個系統都須要有特定的用戶進行登陸。又須要對於用戶進行分組,一個用戶能夠加入多個組,一個組能夠有多個用戶。n:m的關聯關係須要在設計數據模型的時候就體現出來。api
生產環境中,n:m的關聯是須要中間表來輔助的,來存儲例如用戶和用戶組之間的映射關係。安全
關聯能夠經過設置foreignKey
來進行關聯,可是被後端大哥批鬥了,爲了保證數據庫的性能,通常不多采用外鍵關聯兩個數據模型,而是採用邏輯關聯,經過開發者人工保證寫入和刪除的順序。
索引是必不可少的,咱們在提交DBA工單的時候,必需要創建索引,尤爲是有些數據表會存儲很是多的記錄,這時對於主鍵創建索引,能夠大幅提升查找的效率。
根據上面的三個重點,完成了個人數據表設計。這裏能夠給出一個簡單的栗子數據庫模型。後面的實現也是根據這個栗子進行的。
CREATE TABLE `group` (
`id` INTEGER PRIMARY KEY AUTO_INCREMENT,
`name` VARCHAR(255) NOT NULL,
`db_create_time` DATETIME,
`db_update_time` DATETIME
);
CREATE TABLE `user` (
`id` INTEGER PRIMARY KEY AUTO_INCREMENT,
`name` VARCHAR(255) NOT NULL,
`db_create_time` DATETIME,
`db_update_time` DATETIME
);
CREATE TABLE `group_users` (
`user` INTEGER NOT NULL,
`group` INTEGER NOT NULL,
CONSTRAINT `pk_group_users` PRIMARY KEY (`user`, `group`)
);
CREATE INDEX `idx_group_users` ON `group_users` (`group`);
複製代碼
egg框架自己提供了不少即插即用的plugin
,官方最基本的插件集中就有egg-sequelize
插件。插件配置起來很是簡單。
下面可能不會說的很是詳細,我主要講一講本身在進行開發時候遇到的各類坑。
|-- app // node服務端相關代碼
|-- controller
|-- api // node端接口controller
|-- group.js // 組相關controller
|-- user.js // 用戶相關controller
|-- extend
|-- helper.js // helper擴展
|-- middleware
|-- model // sequelize數據模型
|-- user.js
|-- group.js
|-- group_user.js
|-- service // 可複用的數據處理及查詢方法
|-- utils // service中拿不到helper,部分utils放在這裏
|-- router.js // 路由
|-- build // 構建代碼
|-- client // 客戶端相關代碼
|-- config // 配置文件
複製代碼
// config.local.js
module.exports = {
sequelize: {
// 數據庫類型
dialect: 'mysql',
// 數據庫名
database: 'swiss',
// 數據庫IP和端口
host: '127.0.0.1',
port: '3306',
// 數據庫鏈接的用戶和密碼
username: 'root',
password: '123',
// 是否自動進行下劃線轉換(這裏是由於DB默認的命名規則是下劃線方式,而咱們使用的大多數是駝峯方式)
underscored: true,
// 時區,sequelize有不少自動時間的方法,都是和時區相關的,記得設置成東8區(+08:00)
timezone: '+08:00',
},
}
複製代碼
各類配置項一目瞭然,記得要設置好timezone
,不然你全部默認爲當前時間的值都會出錯。underscored
表示自動將駝峯表示法轉換爲mysql的下劃線表示法(固然後面會說到,他的轉換機制有些時候讓我感受費解,但願瞭解的大佬們能夠幫我解釋一下~)。
直接將啓動項配置寫死在配置文件裏面不是不能夠,可是若是須要和同事一塊兒合做開發的話,這樣寫死可能不夠靈活。能夠將某些配置項提取出來,經過命令行傳入參數,來進行開發環境的動態配置。
~ npm run dev -- --u=root --p=123
// config.local.js
let DB_USER = 'root';
let DB_PASSWORD = '123';
const ARGV_2 = JSON.parse(process.argv[2] || {});
DB_USER = (ARGV_2 && ARGV_2.u) || 'root';
DB_PASSWORD = (ARGV_2 && ARGV_2.p) || '123';
module.exports = {
sequelize: {
// ....
username: `${DB_USER}`,
password: `${DB_PASSWORD}`,
// ....
}
};
複製代碼
model
egg-sequelize會自動將sequelize
實例掛載到app.model
上面,而後靜態方法和屬性則會直接被綁定到app
上,經過app.Sequelize
進行獲取。
model
層做爲MVC的最底層,須要注意到數據模型的pure,model
文件也應該是純淨的,這個文件裏面應該是和數據庫中的表一一對應,一個model
文件對應一個DB中的表,這個文件中不該該包含任何和邏輯相關的代碼,應該徹底是數據模型的定義。
// app/model/user.js
module.exports = app => {
// egg-sequelize插件會將Sequelize類綁定到app上線,從裏面能夠取到各類靜態類型
const { TEXT, INTEGER, NOW } = app.Sequelize;
const User = app.model.define(
'user',
{
name: TEXT,
createAt: {
type: DATE,
// 能夠重寫某個字段的字段名
field: 'db_create_time',
allowNull: false,
defaultValue: NOW,
},
updateAt: {
type: DATE,
field: 'db_update_time',
allowNull: false,
defaultValue: NOW,
},
},
{
timestamps: false,
freezeTableName: true,
tableName: 'users',
underscored: true,
}
);
// 定義關聯關係
User.associate = () => {
// 定義多對多關聯
User.belongsToMany(app.model.Groups, {
// 中間表的model
through: app.model.groupUser,
// 進行關聯查詢時,關聯表查出來的數據模型的alias
as: 'project',
// 是否採用外鍵進行物理關聯
constraints: false,
});
// 這裏若是一個模型和多個模型都有關聯關係的話,關聯關係須要統必定義在這裏
};
return User;
};
複製代碼
上面的代碼有很是多須要注意的地方,咱們經過這個文件定義了一個數據模型,這個模型能夠映射到數據庫中的某一個表,這裏就是映射到了users
表,用來存儲用戶信息。
id
字段會被設置爲主鍵,而且是AUTO_INCREMENT
的,不須要咱們本身聲明;timestamps
字段能夠表示是否採用默認的createAt
和updateAt
字段,咱們經過field
字段重寫了這兩個字段的字段名;associate
字段能夠用來設置數據模型的關聯關係,若是一個數據模型關聯了多個數據模型,那麼這個方法裏面也能夠定義多個關係;belongsToMany
表示n:m的關係映射,這個在官方文檔中描述的很是清楚了;as
能夠爲這個映射設置別名,這樣在進行查詢的時候,獲得的結果就是以別名來標識的;constraints
:這個屬性很是重要,能夠用來表示這個關聯關係是否採用外鍵關聯。在大多數狀況下咱們是不須要經過外鍵來進行數據表的物理關聯的,直接經過邏輯進行關聯便可;through
:這個屬性表示關聯表的數據模型,也就是保存關聯關係的數據庫表的模型。上面的這些屬性,在開發過程當中多多少少都消耗了我一些時間**-1s**,模型的設置和數據庫表之間的關係很是緊密,必定要保證你的數據模型和數據表之間沒有歧義。
一樣地,咱們能夠定義到關聯表和中間表的模型:
// app/model/group.js
module.exports = app => {
const { TEXT, INTEGER, NOW } = app.Sequelize;
const Group = app.model.define(
'group',
{
name: TEXT,
createAt: {
type: DATE,
field: 'db_create_time',
allowNull: false,
defaultValue: NOW,
},
updateAt: {
type: DATE,
field: 'db_update_time',
allowNull: false,
defaultValue: NOW,
},
},
{
timestamps: false,
freezeTableName: true,
tableName: 'groups',
underscored: true,
}
);
// 定義關聯關係
Group.associate = () => {
Group.belongsToMany(app.model.User, {
through: app.model.groupUser,
as: 'partner',
constraints: false,
});
};
return Group;
};
// app/model/group_user.js
// 中間表不須要定義關聯關係
module.exports = app => {
const { INTEGER } = app.Sequelize;
const GroupUser = app.model.define(
'group_user',
{
user_id: INTEGER,
group_id: INTEGER,
},
{
timestamps: false,
freezeTableName: true,
tableName: 'group_user',
underscored: true,
}
);
return GroupUser;
};
複製代碼
controller
在egg中,controller
模塊的做用相似於MVC模式中的控制器,進行從model
到view
的轉換,而在提供接口的時候,controller
負責的是提供從model
到api
的轉換,通過model
從數據庫中查詢出來的結果,將在controller
裏面進行包裝,而後返回給接口的調用者。
在進行數據訪問的時候,不少的接口請求均可以拆分爲幾個相似的CRUD操做,好比:
service
裏面。而controller
只負責請求的響應處理。當一個接口請求跨過了middleware
的處理,通過了router
的分發以後:
// app/router.js
module.exports = app => {
app.get('/api/user/get', app.controller.api.user.get);
app.post('/api/group/set', app.controller.api.group.set);
}
複製代碼
會被轉發到對應的controller
進行處理。
// app/controller/user.js
module.exports = class UserController extends Controller {
async get = () => {
const { uuid } = this.ctx.session;
if (!uuid) {
ctx.body = {
code: 401,
message: 'unauthorized',
};
return;
}
const userInfo = await this.ctx.service.user.getUserById({ id: uuid });
if (userInfo) {
ctx.body = {
code: 200,
message: 'success',
data: userInfo
}
} else {
ctx.body = {
code: 500,
message: 'error',
}
}
}
}
複製代碼
service
egg官方文檔對於service的描述是這樣的:
簡單來講,Service 就是在複雜業務場景下用於作業務邏輯封裝的一個抽象層,提供這個抽象有如下幾個好處:
- 保持 Controller 中的邏輯更加簡潔。
- 保持業務邏輯的獨立性,抽象出來的 Service 能夠被多個 Controller 重複調用。
- 將邏輯和展示分離,更容易編寫測試用例。
也就是controller
中要儘可能保持clean,而後,能夠複用的業務邏輯被統一抽出來,放到service
中,被多個controller
進行復用。
咱們將CRUD操做,所有提取到service
中,封裝成一個個通用的CRUD方法,來提供給其餘service
進行嵌套的時候調用,或者提供給controller
進行業務邏輯調用。
好比:讀取用戶信息的過程:
// app/service/user.js
module.exports = class UserService extends Service {
// 經過id獲取用戶信息
async getUserById = ({
id,
}) => {
const { ctx } = this;
let userInfo = {};
try {
userInfo = await ctx.model.User.findAll({
where: {
id,
},
// 查詢操做的時候,加入這個參數能夠直接拿到對象類型的查詢結果,不然還須要經過方法調用解析
raw: true,
});
} catch (err) {
ctx.logger.error(err);
}
return userInfo;
}
}
複製代碼
sequelize
事務以前有說到,在創建模型的時候,咱們創建了User
和Group
之間的關聯關係,而且經過了一個關聯表進行二者之間的關聯。
因爲咱們沒有創建二者之間的外鍵關聯,因此在寫入的時候,咱們要進行邏輯的關聯寫入。
若是咱們須要新建一個用戶,而且爲這個用戶新建一個默認的group
,因爲組和用戶有着多對多的關係,因此這裏咱們採用belongsToMany
來創建關係。一個用戶能夠屬於多個組,而且一組也能夠包含多個用戶。
在創建的時候,須要按照必定的順序,寫入三張表,一旦某個寫入操做失敗以後,須要對於以前的寫入操做進行回滾,防止DB中產生垃圾數據。這裏須要用到事務機制進行寫入控制,而且人工保證寫入順序。
// app/service/user.js
module.exports = class UserService extends Service {
async setUser = ({
name,
}) => {
const { ctx } = this;
let transaction;
try {
// 這裏須要注意,egg-sequelize會將sequelize實例做爲app.model對象
transaction = await ctx.model.transaction();
// 建立用戶
const user = await ctx.model.User.create({
name,
}, {
transaction,
});
// 建立默認組
const group = await ctx.model.Group.create({
name: 'default',
}, {
transaction,
});
const userId = user && user.getDataValue('id');
const groupId = group && group.getDataValue('id');
if (!userId || !groupId) {
throw new Error('建立用戶失敗');
}
// 建立用戶和組之間的關聯
const associate = await ctx.mode.GroupUser.create({
user_id: userId,
group_id: groupId,
}, {
transaction,
});
await transaction.commit();
return userId;
} catch (err) {
ctx.logger.error(err);
await transaction.rollback();
}
}
}
複製代碼
經過sequelize
提供的事務功能,能夠將串聯寫入過程當中的錯誤進行回滾,保證了每次寫入操做的原子性。
既然咱們已經建立了關聯關係,那麼若是經過關聯關係,查詢到對應的數據庫內容呢?
在多對多的關聯條件下,若是咱們要查詢某個用戶的全部分組信息,須要經過用戶id來查詢其關聯的全部group
。
// service
async getGroupByUserId = ({
id,
}) => {
const { ctx } = this;
const group = await ctx.model.User.findAll({
attributes: ['project.id', 'project.name'],
include: [
{
model: ctx.model.Group,
as: 'project',
// 指定關聯表查詢屬性,這裏表示不須要任何關聯表的屬性
attributes: [],
through: {
// 指定中間表的屬性,這裏表示不須要任何中間表的屬性
attributes: []
}
}
],
where: {
id,
},
raw: true,
// 這個須要和上面的中間表屬性配合,表示不忽略include屬性中的attributes參數
includeIgnoreAttributes: false,
});
}
複製代碼
經過上面的關聯查詢方法,能夠獲得這樣的一條SQL語句:
SELECT `project`.`id`, `project`.`name` FROM `users` AS `user` LEFT OUTER JOIN ( `group_user` AS `project->group_user` INNER JOIN `groups` AS `project` ON `project`.`id` = `project->group_user`.`group_id`) ON `user`.`id` = `project->group_user`.`user_id` WHERE `user`.`id` = 1;
複製代碼
對應的查詢結果:
[ { id: 1, name: 'default' } ]
複製代碼
而在一對多和多對一的關係下,其本質和多對多基本上是一致的,是在多的方向存儲一個冗餘字段,來保存其對應的惟一元素的主鍵,不管是何種關係,其默認在sequelize
中實現的數據模型,都是範式化的,若是須要反範式來提升數據庫效率,仍是須要本身去作冗餘的。
hooks
在數據庫查詢的過程當中,不免須要在真正的CRUD先後進行一些數據的處理。
考慮到這樣的一個場景:
在客戶端,咱們前端存儲的用戶名並非經過name
來表示的,而是經過nickName
字段來進行表示的,在每次進行讀寫操做以前,例如這裏容許用戶本身修改本身的名字,當請求發送到服務端以後,交予service
進行處理。
// app/model/user
const User = app.model.define({
// ...
}, {
hooks: {
beforeUpdate: (user, options) => {
const name = user.nickName;
delete user.nickName;
user.name = name;
}
}
});
複製代碼
咱們在定義模型的時候,直接定義好這個hook
,beforeUpdate
會在User
模型每次調用update
以前,調用這個hook。這個hook會傳入update操做傳入的參數實例,能夠直接對這個實例進行修改,保證明際update
操做的實例是正確的。
hooks
可使用的地方不少,這裏只是簡單介紹一下使用的方法,hooks
中間也能夠包含異步操做,可是要注意,若是包含異步操做的話,須要返回一個Promise
。咱們還能夠在進行具備反作用的操做以前,對於用戶權限進行校驗。
hooks
的使用是須要了解到其功能,而後根據本身的業務場景,靈活地進行使用的。
egg提供了很是多的可擴展空間,除了使用其做爲前端頁面的部署環境以外,還能夠承擔一些model
層的工做,有興趣的小夥伴能夠試下經過egg實現先後端分離的全棧開發工做~
在實際業務場景的實踐過程當中,sequelize
的不少解決方案都要從官方文檔中一個字一個字的查找,有些問題甚至須要去翻issue
才能找到對應的處理方法,不知道爲何官方文檔會有那麼多版的中文翻譯。。實踐的方案卻特別少,前端的小夥伴們大部分仍是熱衷於MongoDB。確實關係型數據的操做相較於NoSQL仍是比較複雜的。不過解決問題的過程雖然煩惱,可是結果總仍是愉悅的。
sequelize
還有不少須要挖掘的地方,它自己提供的不少功能在此次迭代的過程當中都沒有用到。好比scope
、migration
,有機會能夠嘗試下一些新的功能和實現方案。