模型层与长久化

回顾 从零搭建 Node.js 企业级 Web 服务器(一):接口与分层,一块残缺的业务逻辑是由视图层、管制层、服务层、模型层独特定义与实现的,管制层与服务层实现了业务处理过程,模型层定义了业务实体并以 对象-关系映射 拜访数据库提供长久化能力。

对象-关系映射

对象-关系映射 是指在应用程序中的对象与关系型数据库中的数据间建设映射关系以便捷拜访数据库的技术,简称 ORM。ORM 的优劣决定了 SQL 执行的高效性与稳定性,进而间接影响服务节点的性能指标,是十分重要的模块。sequelize 是 Node.js 最老牌的 ORM 模块,首个版本公布于 2010 年,保护至今单测覆盖率始终放弃在 95%+,值得举荐。思考到 sequelize 最新大版本 6.x 才公布 1 个月,本文抉择了 sequelize 5.x 版本 v5.22.3 作为依赖。在上一章已实现的工程 host1-tech/nodejs-server-examples - 04-exception 的根目录执行 sequelize 装置命令:

$ yarn add 'sequelize@^5.22.3'  # 本地装置 sequelize,应用 5.x 版本# ...info Direct dependencies└─ sequelize@5.22.3# ...

应用 sequelize 须要装置对应数据库的驱动,本章应用 sqlite 作为底层数据库,执行驱动模块 sqlite3 的装置命令:

$ # sqlite3 会从海内站点下载二进制包,此处设置 sqlite3 国内镜像$ npm config set node_sqlite3_binary_host_mirror http://npm.taobao.org/mirrors/sqlite3/$ yarn add sqlite3  # 本地装置 sqlite3

另外 sequelize 提供了配套命令行工具 sequelize-cli,能够不便地对模型层业务实体进行治理,执行 sequelize-cli 装置命令:

$ yarn add -D sequelize-cli# ...info Direct dependencies└─ sequelize-cli@6.2.0# ...

初始化模型层

当初配置 sequelize-cli 而后灵便应用 sequelize-cli 初始化模型层:

// .sequelizercconst { resolve } = require('path');const modelsDir = resolve('src/models');module.exports = {  config: `${modelsDir}/config`,  'migrations-path': `${modelsDir}/migrate`,  'seeders-path': `${modelsDir}/seed`,  'models-path': modelsDir,};
$ yarn sequelize init           # 脚本创立 src/models 目录寄存模型层逻辑$ tree -L 3 -a -I node_modules  # 展现除了 node_modules 之外包含 . 结尾的全副目录内容构造.├── .dockerignore├── .sequelizerc├── Dockerfile├── package.json├── public│   ├── 500.html│   ├── glue.js│   ├── index.css│   ├── index.html│   └── index.js├── src│   ├── controllers│   │   ├── chaos.js│   │   ├── health.js│   │   ├── index.js│   │   └── shop.js│   ├── middlewares│   │   ├── index.js│   │   └── urlnormalize.js│   ├── models│   │   ├── config│   │   ├── index.js│   │   ├── migrate│   │   └── seed│   ├── moulds│   │   ├── ShopForm.js│   │   └── yup.js│   ├── server.js│   ├── services│   │   └── shop.js│   └── utils│       └── cc.js└── yarn.lock

丑化一下 src/models/index.js

// src/models/index.jsconst fs = require('fs');const path = require('path');const Sequelize = require('sequelize');const basename = path.basename(__filename);const env = process.env.NODE_ENV || 'development';const config = require(__dirname + '/config')[env];const db = {};let sequelize;if (config.use_env_variable) {  sequelize = new Sequelize(process.env[config.use_env_variable], config);} else {  sequelize = new Sequelize(    config.database,    config.username,    config.password,    config  );}fs.readdirSync(__dirname)  .filter((file) => {    return (      file.indexOf('.') !== 0 && file !== basename && file.slice(-3) === '.js'    );  })  .forEach((file) => {    const model = require(path.join(__dirname, file))(      sequelize,      Sequelize.DataTypes    );    db[model.name] = model;  });Object.keys(db).forEach((modelName) => {  if (db[modelName].associate) {    db[modelName].associate(db);  }});db.sequelize = sequelize;db.Sequelize = Sequelize;module.exports = db;

调整一下 src/models/config

$ # 将 src/models/config 挪动至 src/models/config/index.js$ mv src/models/config src/models/config.js$ mkdir src/models/config$ mv src/models/config.js src/models/config/index.js
// src/models/config/index.js-{-  "development": {-    "username": "root",-    "password": null,-    "database": "database_development",-    "host": "127.0.0.1",-    "dialect": "mysql"-  },-  "test": {-    "username": "root",-    "password": null,-    "database": "database_test",-    "host": "127.0.0.1",-    "dialect": "mysql"-  },-  "production": {-    "username": "root",-    "password": null,-    "database": "database_production",-    "host": "127.0.0.1",-    "dialect": "mysql"-  }-}+module.exports = {+  development: {+    dialect: 'sqlite',+    storage: 'database/index.db',+    define: {+      underscored: true,+    },+    migrationStorageTableName: 'sequelize_meta',+  },+};

新增模型层店铺的业务实体定义:

$ # 生成店铺 model 文件与 schema 迁徙文件$ yarn sequelize model:generate --name Shop --attributes name:string  $ tree src/models # 展现 src/models 目录内容构造src/models├── config│   └── index.js├── index.js├── migrate│   └── 20200725045100-create-shop.js├── seed└── shop.js

丑化一下 src/models/shop.js

// src/models/shop.jsconst { Model } = require('sequelize');module.exports = (sequelize, DataTypes) => {  class Shop extends Model {    /**     * Helper method for defining associations.     * This method is not a part of Sequelize lifecycle.     * The `models/index` file will call this method automatically.     */    static associate(models) {      // define association here    }  }  Shop.init(    {      name: DataTypes.STRING,    },    {      sequelize,      modelName: 'Shop',    }  );  return Shop;};

调整一下 src/models/shop.js

// ...module.exports = (sequelize, DataTypes) => {  // ...  Shop.init(    {      name: DataTypes.STRING,    },    {      sequelize,      modelName: 'Shop',+      tableName: 'shop',    }  );  return Shop;};

丑化一下 src/models/migrate/20200725045100-create-shop.js

// src/models/migrate/20200725045100-create-shop.jsmodule.exports = {  up: async (queryInterface, Sequelize) => {    await queryInterface.createTable('Shops', {      id: {        allowNull: false,        autoIncrement: true,        primaryKey: true,        type: Sequelize.INTEGER,      },      name: {        type: Sequelize.STRING,      },      createdAt: {        allowNull: false,        type: Sequelize.DATE,      },      updatedAt: {        allowNull: false,        type: Sequelize.DATE,      },    });  },  down: async (queryInterface, Sequelize) => {    await queryInterface.dropTable('Shops');  },};

调整一下 src/models/migrate/20200725045100-create-shop.js

module.exports = {  up: async (queryInterface, Sequelize) => {-    await queryInterface.createTable('Shops', {+    await queryInterface.createTable('shop', {      id: {        allowNull: false,        autoIncrement: true,        primaryKey: true,        type: Sequelize.INTEGER,      },      name: {        type: Sequelize.STRING,      },-      createdAt: {+      created_at: {        allowNull: false,        type: Sequelize.DATE,      },-      updatedAt: {+      updated_at: {        allowNull: false,        type: Sequelize.DATE,      },    });  },  down: async (queryInterface, Sequelize) => {-    await queryInterface.dropTable('Shops');+    await queryInterface.dropTable('shop');  },};

筹备初始店铺数据:

$ # 生成初始店铺 seed 文件$ yarn sequelize seed:generate --name first-shop$ tree src/models # 展现 src/models 目录内容构造src/models├── config│   └── index.js├── index.js├── migrate│   └── 20200725045100-create-shop.js├── seed│   └── 20200725050230-first-shop.js└── shop.js

丑化一下 src/models/seed/20200725050230-first-shop.js

// src/models/seed/20200725050230-first-shop.jsmodule.exports = {  up: async (queryInterface, Sequelize) => {    /**     * Add seed commands here.     *     * Example:     * await queryInterface.bulkInsert('People', [{     *   name: 'John Doe',     *   isBetaMember: false     * }], {});     */  },  down: async (queryInterface, Sequelize) => {    /**     * Add commands to revert seed here.     *     * Example:     * await queryInterface.bulkDelete('People', null, {});     */  },};

调整一下 src/models/seed/20200725050230-first-shop.js

module.exports = {  up: async (queryInterface, Sequelize) => {-    /**-     * Add seed commands here.-     *-     * Example:-     * await queryInterface.bulkInsert('People', [{-     *   name: 'John Doe',-     *   isBetaMember: false-     * }], {});-     */+    await queryInterface.bulkInsert('shop', [+      { name: '良品铺子', created_at: new Date(), updated_at: new Date() },+      { name: '来伊份', created_at: new Date(), updated_at: new Date() },+      { name: '三只松鼠', created_at: new Date(), updated_at: new Date() },+      { name: '百草味', created_at: new Date(), updated_at: new Date() },+    ]);  },  down: async (queryInterface, Sequelize) => {-    /**-     * Add commands to revert seed here.-     *-     * Example:-     * await queryInterface.bulkDelete('People', null, {});-     */+    await queryInterface.bulkDelete('shop', null, {});  },};

向数据库写入表格构造与初始数据:

$ mkdir database          # 新建 database 目录寄存数据库文件$ touch database/.gitkeep # 写入 .gitkeep 让目录能够由 git 提交$ tree -L 1               # 展现当前目录内容构造.├── Dockerfile├── database├── node_modules├── package.json├── public├── src└── yarn.lock$ yarn sequelize db:migrate       # 向数据库写入表格构造# ...$ yarn yarn sequelize db:seed:all # 想数据库写入初始数据# ...

加上数据库拜访逻辑

ORM 提供了非常弱小且易用的接口,只需对业务层做无限步调整即可实现长久化:

// src/services/shop.js-// 店铺数据-const memoryStorage = {-  '1001': { name: '良品铺子' },-  '1002': { name: '来伊份' },-  '1003': { name: '三只松鼠' },-  '1004': { name: '百草味' },-};--// 模仿延时-async function delay(ms = 200) {-  await new Promise((r) => setTimeout(r, ms));-}const { Shop } = require('../models');class ShopService {-  async init() {-    await delay();-  }+  async init() {}  async find({ id, pageIndex = 0, pageSize = 10 }) {-    await delay();-    if (id) {-      return [memoryStorage[id]].filter(Boolean);+      return [await Shop.findByPk(id)];    }-    return Object.keys(memoryStorage)-      .slice(pageIndex * pageSize, (pageIndex + 1) * pageSize)-      .map((id) => ({ id, ...memoryStorage[id] }));+    return await Shop.findAll({+      offset: pageIndex * pageSize,+      limit: pageSize,+    });  }  async modify({ id, values }) {-    await delay();--    const target = memoryStorage[id];+    const target = await Shop.findByPk(id);    if (!target) {      return null;    }-    return Object.assign(target, values);+    Object.assign(target, values);+    return await target.save();  }  async remove({ id }) {-    await delay();--    const target = memoryStorage[id];+    const target = await Shop.findByPk(id);    if (!target) {      return false;    }-    return delete memoryStorage[id];+    return target.destroy();  }  async create({ values }) {-    await delay();--    const id = String(-      1 +-        Object.keys(memoryStorage).reduce((m, id) => Math.max(m, id), -Infinity)-    );--    return { id, ...(memoryStorage[id] = values) };+    return await Shop.create(values);  }}// 单例模式let service;module.exports = async function () {  if (!service) {    service = new ShopService();    await service.init();  }  return service;};

拜访 http://localhost:9000/ 从新体验店铺治理性能:

应用容器

先在本地新建 .npmrc 文件,应用国内镜像减速构建:

# .npmrcregistry=http://r.cnpmjs.org/node_sqlite3_binary_host_mirror=http://npm.taobao.org/mirrors/sqlite3/

改用非 slim 版 Node.js 根底镜像:

-FROM node:12.18.2-slim+FROM node:12.18.2WORKDIR /usr/app/05-databaseCOPY . .RUN yarnEXPOSE 9000CMD yarn start

而后构建镜像并启动容器:

$ # 构建容器镜像,命名为 05-database,标签为 1.0.0$ docker build -t 05-database:1.0.0 .# ...Successfully tagged 05-database:1.0.0$ # 以镜像 05-database:1.0.0 运行容器,命名为 05-database$ # 挂载 database 寄存数据库文件$ # 重启策略为无条件重启$ docker run -p 9090:9000 -v "$PWD/database":/usr/app/05-database/database -d --restart always --name 05-database 05-database:1.0.0

拜访 http://localhost:9090/ 可看到与本地运行时一样的数据:

本章源码

host1-tech/nodejs-server-examples - 05-database

更多浏览

从零搭建 Node.js 企业级 Web 服务器(零):动态服务
从零搭建 Node.js 企业级 Web 服务器(一):接口与分层
从零搭建 Node.js 企业级 Web 服务器(二):校验
从零搭建 Node.js 企业级 Web 服务器(三):中间件
从零搭建 Node.js 企业级 Web 服务器(四):异样解决
从零搭建 Node.js 企业级 Web 服务器(五):数据库拜访