數據庫初始化
????????在軟件開發階段和測試階段,為了方便調試,我們通常會進行一系列的數據庫初始化操作,比如重置數據表,插入記錄等等,或者在部署階段進行數據初始化的操作
????????根據前面章節介紹過的 knex.js
和 sequelize.js
,我們可以利用它們提供的方法進行DDL,本節就數據庫表重置的初始化行為做一點探討,表結果為User{id: num, name: string, age: num}
,數據庫采用sqlite
Knex DDL
以下是利用 knex.schema
的一個簡單示例:
knex.js
const knex = require('knex');
const fs = require('fs');const sqlClient = knex({client: 'sqlite3',connection: {filename: `${__root}/db/data.db`,acquireConnectionTimeout: 1000},useNullAsDefault: true
});module.exports = sqlClient;
init.js
global.__root = __dirname;const knex = require('./knex.js');const drop = knex.schema.dropTableIfExists('user');
const create = knex.schema.createTable('user', (user)=>{user.increments('id').notNullable().primary();user.text('name').notNullable();user.integer('age').notNullable()
});
const promises = [drop,create];
Promise.all(promises)
.then(res=>{console.log('Database inits successfully!')
}).catch(err=>{console.error(err);
})
Sequelize DDL
以下是利用 Sequelize.Model
的一個簡單示例:
sequelize.js
const { Sequelize,DataTypes,Model } = require('sequelize');
const fs = require('fs');const sqlClient = new Sequelize({dialect: 'sqlite',storage: `${__root}/db/data.db`
})const User = sqlClient.define('User', {id: {primaryKey: true,type: DataTypes.INTEGER,allowNull: false,autoIncrement: true},name: {type: DataTypes.STRING,allowNull: false},age: {type: DataTypes.INTEGER,allowNull: false}
}, {tableName: 'user',timestamps: false,
});module.exports = {sqlz: sqlClient,User
}
init.js
global.__root = __dirname;const { User } = require('./sequelize');// User.drop();User.sync();
User.sync({ force: true }) //這個相當于前兩個的結合體.then(res=>{console.log('Database inits successfully!');}).catch(err=>{console.error(err);
})
SQL文件
????????Springboot作為Web后端最流行的框架之一,想必各位都接觸過或者聽說過,在Springboot中,可以在配置文件中設置sql腳本的路徑,在項目啟動時執行sql腳本來完成初始化。
????????這是一種非常好的方法,因為有時候我們項目場景下的數據庫表結構與關系可能非常復雜,而且不同語言,不同框架的實現有些區別,用代碼去完成初始化操作將是一件非常麻煩的事,既然SQL是關系型數據庫通用的語言,那我們就可以通過SQL腳本來定義數據庫表的結構和關系,可以手寫SQL腳本,也可以借助如Navicat之類的工具設計表然后轉儲sql腳本,然后交給我們的程序去執行,或者手動執行。
Node的sql框架千千萬,我在幾個主流框架中似乎都沒看到有提供執行sql文件的特性,其實沒那么復雜,不從構造完美的框架角度,僅以為項目服務的角度考慮來說是這樣的,接下來我們就來簡單實現一下通過sql腳本去初始化數據庫。
有兩條路:
- 運行環境先安裝sqlite3客戶端,node讀取sql腳本內容,node通過
exec
去指定目錄下,打開sqlite3命令行連接sqlite數據庫,同時把sql內容傳遞過去,在sqlite3中執行sql腳本完成數據庫初始化操作 - Node安裝sqlite3依賴,通過sql框架連接sqlite數據庫,node讀取sql腳本內容,對內容進行規范化處理只剩下純凈的sql語句后,交給sql框架以sql語句的形式去運行
Springboot采用的就是第2種方法,那我們也在Node中實現一下吧
實現準備好sql腳本 schemal.sql:
-- 先刪除user表
DROP TABLE IF EXISTS `user`;
-- 定義表結構,并創建user表
CREATE TABLE `user` (id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, --自增主鍵name TEXT NOT NULL,age INTEGER NOT NULL
);
Knex
先用Knex作為sql框架做個示范。獲取到項目根目錄路徑后,建立數據庫連接:
knex.js
const knex = require('knex');
const fs = require('fs');const sqlClient = knex({client: 'sqlite3',connection: {filename: `${__root}/db/data.db`,acquireConnectionTimeout: 1000},useNullAsDefault: true
});module.exports = sqlClient;
接下來,為客戶端實現執行sql文件的方法:
- 定義
runSql
方法的傳參和返回
我這里傳入sql文件的路徑,返回sql語句執行的promise鏈 - 內部實現,首先通過
fs
模塊讀取sql腳本內容并轉為字符串 - 把內容中的注釋去掉
- 去掉內容首尾的空格
- 去掉
\r
- 去掉
\n
(我為了打印sql語段時更加美觀,省去了這一步,不影響執行結果) - 把內容按照
;
號分割成一個個獨立的sql語句字串 - 過濾掉空字串(由每2個sql語句間的空格形成)
sqlClient.runSql = (path)=>{const script = fs.readFileSync(path).toString();console.log("Going to run a sql file:");console.log(script);/*** 拆成一句句sql來執行是因為,knex執行一串語句時,會把它們都算進一個事務內* 利用正則忽略注釋* 去首尾空格* 按冒號分句* 校驗字串是否為sql語句* @type {string[]}*/const sqls = script.replace(/\/\*[\s\S]*?\*\/|(--|\#)[^\r\n]*/gm, '').trim().replaceAll('\r','').split(';').filter(str=>{return str.trim() ? true : false;});console.log("sqls");console.log(sqls);console.log("start run:");const promises = sqls.map(sql=>{sql += ';'; // knex會自動補上冒號,加不加無所謂其實console.log("Going to run a sql:");console.log(sql);return sqlClient.raw(sql);})return promises;
}
到這里,我們就得到了純凈的一條條sql語句,接下來把sql語句丟給knex
即可:
init.js
global.__root = __dirname;const knex = require('./knex.js')const promises = knex.runSql(`${__root}/db/schema.sql`);
Promise.all(promises).then(res=>{console.log("Database inits successfully!")}).catch(err=>{console.error(err);
})
輸出結果:
D:\Workstation\gitee-localRepo\express-demo\DatabaseInit>node index.js
Going to run a sql file:
-- 先刪除user表
DROP TABLE IF EXISTS `user`;
-- 定義表結構,并創建user表
CREATE TABLE `user` (id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, --自增主鍵name TEXT NOT NULL,age INTEGER NOT NULL
);sqls
['DROP TABLE IF EXISTS `user`','\n' +'\n' +'CREATE TABLE `user` (\n' +' id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, \n' +' name TEXT NOT NULL,\n' +' age INTEGER NOT NULL\n' +')'
]
start run:
Going to run a sql:
DROP TABLE IF EXISTS `user`;
Going to run a sql:CREATE TABLE `user` (id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,name TEXT NOT NULL,age INTEGER NOT NULL
);
Server is ready on http://:::8080
Database inits successfully!
很好,我們可以很清晰的看到sql的執行過程
Sequelize
????????如果你把knex
這套照搬過去,把knex.raw
換成sequelize.query
,你也許會尷尬的發現,不太對勁,它先創建了user表,接著又把它給刪了,還大言不慚地打印了成功信息(我的環境下是這樣,不清楚別人會不會,但既然發生了就說明存在一定的問題)。嘗試反復執行knex示例和seuelize示例,前者永遠正確,后者永遠錯誤,而且sequelize似乎更慢一點,產生這樣的區別,可能是它們執行sql語句的實現機制不太一樣,花費精力去看它源碼沒有必要,既然在這個場景下我們這兩個步驟有著明確的先后順序,那我們就通過async/await讓它們完全的順序執行即可:
sqlClient.runSql = async (path)=> {const script = fs.readFileSync(path).toString();console.log("Going to run a sql file:");console.log(script);/*** 拆成一句句sql來執行是因為,knex執行一串語句時,會把它們都算進一個事務內* 忽略注釋* 去首尾空格* 按冒號分句* 校驗字串是否為sql語句* @type {string[]}*/const sqls = script.replace(/\/\*[\s\S]*?\*\/|(--|\#)[^\r\n]*/gm, '').trim().replaceAll('\r','').split(';').filter(str=>{return str.trim() ? true : false;});console.log("sqls");console.log(sqls);console.log("start run:");for (let sql of sqls) {const res = await sqlClient.query(`${sql};`);}
}
輸出結果:
D:\Workstation\gitee-localRepo\express-demo\DatabaseInit>node index.js
Going to run a sql file:
-- 先刪除user表
DROP TABLE IF EXISTS `user`;
-- 定義表結構,并創建user表
CREATE TABLE `user` (id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, --自增主鍵name TEXT NOT NULL,age INTEGER NOT NULL
);sqls
['DROP TABLE IF EXISTS `user`','\n' +'\n' +'CREATE TABLE `user` (\n' +' id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, \n' +' name TEXT NOT NULL,\n' +' age INTEGER NOT NULL\n' +')'
]
start run:
Server is ready on http://:::8080
Executing (default): DROP TABLE IF EXISTS `user`;
Executing (default): CREATE TABLE `user` (id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,name TEXT NOT NULL,age INTEGER NOT NULL
);
Database inits successfully!
Ok!現在Sequelize也按照我們的意愿完成了重置user表的初始化工作
如果初始化過程中涉及嚴格的先后順序,務必做好同步流甚至回滾機制。此外,在實際項目中,為了項目的代碼規范性,應當將數據庫路徑,初始化腳本路徑都寫在配置文件中,而不是像本節為了方便直接寫在需要調用的js文件中。