在 Egg 项目中使用 Sequelize ORM 实现 MySQL 数据层管理 后端Web框架【免费下载链接】egg Born to build better enterprise frameworks and apps with Node.js Koa. https://307.run/eggcode项目地址https://gitcode.com/gh_mirrors/eg/egg点击查看免费下载本文是一篇面向 Egg 框架开发者的实战教程当基于 egg-mysql 插件 的裸 SQL 方式无法满足复杂应用的数据层组织需求时如何在 Egg 项目中引入 Sequelize ORM完成从环境配置、Model 定义、Migrations 版本管理到 RESTful CURD 接口开发与单元测试的全流程。读完本文你将掌握 egg-sequelize 插件的配置与加载机制、sequelize-cli 的迁移工作流、factory-girl 的测试数据构造方案并能直接复制文中示例搭建一个可运行、可测试的 ORM 数据层项目。为什么在 Egg 中选择 Sequelize在前面的教程 MySQL 数据访问 中我们通过egg-mysql插件直接执行 SQL 来完成增删改查。这种方式简单直接但当业务复杂到一定程度时裸 SQL 会带来两个问题一是数据模型散落在各条查询语句中难以维护二是不同环境开发、测试、CI之间数据结构的同步与迭代变更缺乏版本管理手段。此时 ORM 框架的价值就体现出来了。在 Node.js 社区中Sequelize 是使用最广泛的 ORM 框架之一它支持 MySQL、PostgreSQL、SQLite 和 MSSQL 等多个数据源并提供 Model 定义、关联关系、查询构建器、事务等能力。egg-sequelize插件在此基础上做了 Egg 适配将定义好的 Model 对象自动加载到app和ctx上让开发者可以在 Controller、Service 中直接以app.model.User或ctx.model.User的方式访问数据模型与 Egg 的目录约定和生命周期无缝结合。本章将用「对 MySQL 中users表做 CURD」的完整例子一步步演示如何在 Egg 项目中使用 Sequelize。准备工作安装 MySQL由于本示例使用 Sequelize 连接 MySQL 数据源需要先在本机安装并启动 MySQL。如果是 MacOS可以通过 homebrew 快速完成brew install mysql brew services start mysql其他平台请按官方方式安装安装完成后确保mysql命令行可用、服务处于运行状态。初始化项目并配置 egg-sequelize首先通过npm初始化一个 Egg 项目--typesimple表示使用 simple 模板$ mkdir sequelize-project cd sequelize-project $ npm init egg --typesimple $ npm i接着安装两个依赖egg-sequelizeEgg 的 Sequelize 插件负责把 Model 加载到app和ctxmysql2MySQL 驱动模块供 Sequelize 连接 MySQL 使用。npm install --save egg-sequelize mysql2在config/plugin.js中启用插件exports.sequelize { enable: true, package: egg-sequelize, };在config/config.default.js中编写 sequelize 配置这里的dialect指定数据源类型为 MySQLhost、port、database指定连接地址与数据库名exports.sequelize { dialect: mysql, host: 127.0.0.1, port: 3306, database: egg-sequelize-doc-default, };按环境区分数据库Egg 的配置体系支持按环境覆盖config.default.js为基础配置config.${env}.js按NODE_ENV覆盖。我们可以利用这一点在不同环境中连接不同的数据库避免开发数据与测试数据互相污染。例如新建一个config/config.unittest.js让单元测试连接独立的数据库egg-sequelize-doc-unittestexports.sequelize { dialect: mysql, host: 127.0.0.1, port: 3306, database: egg-sequelize-doc-unittest, };完成上述配置之后一个使用 Sequelize 的 Egg 项目就初始化完成了。egg-sequelize和sequelize本身还支持更多配置项如连接池、日志、时区等按需在其官方文档中查阅即可。初始化数据库与 Migrations接下来暂时离开 Egg 项目代码先设计和初始化数据库。创建数据库在本地用 mysql 命令快速创建开发与测试两个数据库mysql -u root -e CREATE DATABASE IF NOT EXISTS egg-sequelize-doc-default; mysql -u root -e CREATE DATABASE IF NOT EXISTS egg-sequelize-doc-unittest;users 表结构设计users表的数据结构如下CREATE TABLE users ( id int(11) NOT NULL AUTO_INCREMENT COMMENT primary key, name varchar(30) DEFAULT NULL COMMENT user name, age int(11) DEFAULT NULL COMMENT user age, created_at datetime DEFAULT NULL COMMENT created time, updated_at datetime DEFAULT NULL COMMENT updated time, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENTuser;为什么需要 Migrations我们可以直接通过 mysql 命令把表建好但这并不是对多人协作友好的开发模式项目演进过程中每个迭代都可能变更表结构如何跟踪每次迭代的数据变更并在开发、测试、CI 等不同环境和迭代版本之间快速、一致地切换数据结构这正是 Migrations数据库迁移要解决的问题——把表结构的变更纳入版本管理让数据结构随代码一起演进。Sequelize 官方提供了sequelize-cli工具来实现 Migrations同样可以在 Egg 项目中引入。安装sequelize-clinpm install --save-dev sequelize-cli通过 .sequelizerc 自定义目录结构我们希望把数据库 Migrations 相关的内容统一放在database目录下因此在项目根目录新建.sequelizerc配置文件告诉 sequelize-cli 各项资源的存放位置use strict; const path require(path); module.exports { config: path.join(__dirname, database/config.json), migrations-path: path.join(__dirname, database/migrations), seeders-path: path.join(__dirname, database/seeders), models-path: path.join(__dirname, app/model), };其中config迁移工具使用的数据库连接配置migrations-path迁移文件目录seeders-path种子数据文件目录models-path指向app/model与 Egg 的 Model 目录约定保持一致。初始化迁移配置与目录执行以下两条命令npx sequelize init:config npx sequelize init:migrations执行后会生成database/config.json文件和database/migrations目录。将database/config.json修改为我们项目使用的数据库配置开发与测试环境分别对应前面创建的两个数据库{ development: { username: root, password: null, database: egg-sequelize-doc-default, host: 127.0.0.1, dialect: mysql }, test: { username: root, password: null, database: egg-sequelize-doc-unittest, host: 127.0.0.1, dialect: mysql } }编写第一个 Migration使用 sequelize-cli 生成创建users表的迁移文件npx sequelize migration:generate --nameinit-users执行后在database/migrations目录下生成${timestamp}-init-users.js文件。将其修改为初始化users表的逻辑use strict; module.exports { // 在执行数据库升级时调用的函数创建 users 表 up: async (queryInterface, Sequelize) { const { INTEGER, DATE, STRING } Sequelize; await queryInterface.createTable(users, { id: { type: INTEGER, primaryKey: true, autoIncrement: true }, name: STRING(30), age: INTEGER, created_at: DATE, updated_at: DATE, }); }, // 在执行数据库降级时调用的函数删除 users 表 down: async (queryInterface) { await queryInterface.dropTable(users); }, };up与down是一对可逆操作up负责升级数据结构创建表down负责降级回滚删除表这正是 Migrations 能够跟踪迭代变更并安全回滚的机制所在。执行迁移# 升级数据库 npx sequelize db:migrate # 如果有问题需要回滚可以通过 db:migrate:undo 回退一个变更 # npx sequelize db:migrate:undo # 可以通过 db:migrate:undo:all 回退到初始状态 # npx sequelize db:migrate:undo:all执行之后数据库初始化完成users表已按迁移文件创建且变更记录被 sequelize 持久化后续每次迭代只需新增迁移文件并执行db:migrate即可。编写代码Model、Controller 与路由定义 User Model在app/model/目录下编写userModel。egg-sequelize 约定app/model目录下的每个文件会被加载为对应 Model并在导出函数中通过app.SequelizeSequelize 实例和app.model.define来定义use strict; module.exports (app) { const { STRING, INTEGER, DATE } app.Sequelize; const User app.model.define(user, { id: { type: INTEGER, primaryKey: true, autoIncrement: true }, name: STRING(30), age: INTEGER, created_at: DATE, updated_at: DATE, }); return User; };字段定义与前面 SQL 建表语句一一对应id为自增主键name为长度 30 的字符串age为整型created_at/updated_at为日期类型。定义完成后这个 Model 就可以在 Controller 和 Service 中通过app.model.User或者ctx.model.User访问了。注意define时传入的单数名useregg-sequelize 会将其映射为复数表名users与数据库表名一致。编写 RESTful Controller编写app/controller/users.js为users表提供完整的 CURD 能力。Controller 中的ctx即当前请求上下文通过ctx.model.User即可访问 Model// app/controller/users.js const Controller require(egg).Controller; function toInt(str) { if (typeof str number) return str; if (!str) return str; return parseInt(str, 10) || 0; } class UserController extends Controller { // 列表GET /users?limitoffset async index() { const ctx this.ctx; const query { limit: toInt(ctx.query.limit), offset: toInt(ctx.query.offset), }; ctx.body await ctx.model.User.findAll(query); } // 详情GET /users/:id async show() { const ctx this.ctx; ctx.body await ctx.model.User.findByPk(toInt(ctx.params.id)); } // 创建POST /users async create() { const ctx this.ctx; const { name, age } ctx.request.body; const user await ctx.model.User.create({ name, age }); ctx.status 201; ctx.body user; } // 更新PATCH/PUT /users/:id async update() { const ctx this.ctx; const id toInt(ctx.params.id); const user await ctx.model.User.findByPk(id); if (!user) { ctx.status 404; return; } const { name, age } ctx.request.body; await user.update({ name, age }); ctx.body user; } // 删除DELETE /users/:id async destroy() { const ctx this.ctx; const id toInt(ctx.params.id); const user await ctx.model.User.findByPk(id); if (!user) { ctx.status 404; return; } await user.destroy(); ctx.status 200; } } module.exports UserController;其中用到的 Sequelize 查询 API 对应关系如下Controller 动作Sequelize API语义indexModel.findAll({ limit, offset })分页查询列表showModel.findByPk(id)按主键查询单条createModel.create(data)插入一条记录updateinstance.update(data)更新实例字段destroyinstance.destroy()删除实例挂载 RESTful 路由最后将 Controller 挂载到路由上Egg 提供router.resources一键生成 RESTful 风格的路由映射// app/router.js module.exports (app) { const { router, controller } app; router.resources(users, /users, controller.users); };resources 背后的路由映射源码视角router.resources由 packages/router/src/EggRouter.ts 中的REST_MAP驱动框架遍历 Controller 上已实现的动作index、new、create、show、edit、update、destroy按预设的 HTTP 方法与路径后缀批量注册路由。以/users为例生成的路由表为MethodPathRoute NameController.ActionGET/usersusersapp.controller.users.indexGET/users/newnew_userapp.controller.users.newGET/users/:iduserapp.controller.users.showGET/users/:id/editedit_userapp.controller.users.editPOST/usersusersapp.controller.users.createPATCH/PUT/users/:iduserapp.controller.users.updateDELETE/users/:iddestroy_userapp.controller.users.destroy我们示例 Controller 实现了index、show、create、update、destroy五个动作因此对应注册了上表中的五组路由接口即开发完成。单元测试factory-girl 快速构造数据针对users表的 CURD 接口已开发完接下来编写单元测试验证逻辑。由于前面将单元测试环境和开发环境指向了不同数据库需要先通过 Migrations 初始化测试库的数据结构NODE_ENVtest npx sequelize db:migrate:up有数据库访问的单元测试直接写起来会特别繁琐很多接口需要先创建一系列数据才能测试造测试数据的过程非常耗时。为了简化引入 [factory-girl] 模块快速生成测试数据。安装factory-girlnpm install --save-dev factory-girl定义数据工厂在test/factories.js中定义 factory 的数据模型并挂载到app.factory上// test/factories.js use strict; const { factory } require(factory-girl); module.exports (app) { // 可以通过 app.factory 访问 factory 实例 app.factory factory; // 定义 user 模型和默认数据 factory.define(user, app.model.User, { name: factory.sequence(User.name, (n) name_${n}), age: 18, }); };factory.sequence会为每次创建生成递增的唯一name如name_1、name_2……age使用默认值 18测试中也可以随时覆盖。初始化测试环境并清理数据在test/.setup.js中引入 factory并确保每个测试用例执行完后清理数据避免用例之间相互影响const { app } require(egg-mock/bootstrap); const factories require(./factories); before(() factories(app)); afterEach(async () { // 在每个测试案例执行完后清理数据库 await Promise.all([app.model.User.destroy({ truncate: true, force: true })]); });编写测试用例接下来编写真正的测试用例test/app/controller/users.test.js。它覆盖了 GET 列表、GET 详情、POST 创建、DELETE 删除四类接口借助egg-mock的app.httpRequest()发起真实 HTTP 请求// test/app/controller/users.test.js const { assert, app } require(egg-mock/bootstrap); describe(test/app/controller/users.test.js, () { describe(GET /users, () { it(should work, async () { // 通过 factory-girl 快速创建用户对象到数据库中 await app.factory.createMany(user, 3); const res await app.httpRequest().get(/users?limit2); assert(res.status 200); assert(res.body.length 2); assert(res.body[0].name); assert(res.body[0].age); }); }); describe(GET /users/:id, () { it(should work, async () { const user await app.factory.create(user); const res await app.httpRequest().get(/users/${user.id}); assert(res.status 200); assert(res.body.age user.age); }); }); describe(POST /users, () { it(should work, async () { app.mockCsrf(); let res await app.httpRequest().post(/users).send({ age: 10, name: name, }); assert(res.status 201); assert(res.body.id); res await app.httpRequest().get(/users/${res.body.id}); assert(res.status 200); assert(res.body.name name); }); }); describe(DELETE /users/:id, () { it(should work, async () { const user await app.factory.create(user); app.mockCsrf(); const res await app.httpRequest().delete(/users/${user.id}); assert(res.status 200); }); }); });几个测试细节值得注意app.factory.createMany(user, 3)一次创建 3 条记录配合/users?limit2验证分页参数生效POST、DELETE 等写操作需要调用app.mockCsrf()绕过 Egg 默认的 CSRF 校验每个用例结束后test/.setup.js中的afterEach会truncate清空users表保证用例隔离。在 CI 中运行测试如果在 CI 环境运行单元测试需要确保执行测试代码之前先执行一次 migrate保证测试库的数据结构是最新的。可以在package.json中声明scripts.ci{ scripts: { ci: eslint . NODE_ENVtest npx sequelize db:migrate egg-bin cov } }该命令依次执行eslint代码检查 → 以NODE_ENVtest执行迁移同步测试库结构→egg-bin cov运行测试并生成覆盖率。注意这里用的是db:migrate执行所有未执行的迁移而前面手动初始化测试库时用的是db:migrate:up仅执行指定的最新迁移在 CI 场景下前者更能保证结构完整同步。完整示例与脚手架完整示例一个更完整的 sequelize 示例项目可以在eggjs/examples/sequelize中查看它包含本文介绍的全部模块以及更丰富的业务场景。脚手架Egg 官方提供了 sequelize 脚手架集成了本文用到的egg-sequelize、sequelize-cli与factory-girl等模块。可以直接通过以下命令基于它快速初始化新应用npm init egg --typesequelize初始化后即具备完整的数据层工程结构可在此基础上直接开始业务开发。赞分享后端Web框架【免费下载链接】egg Born to build better enterprise frameworks and apps with Node.js Koa. https://307.run/eggcode项目地址https://gitcode.com/gh_mirrors/eg/egg点击查看免费下载相关推荐Egg.js中使用Sequelize实现ORM数据库操作详解Egg.js中使用Sequelize实现ORM数据库操作详解 前言 在现代Web应用开发中数据库操作是不可或缺的一部分。Egg.js作为企业级Node.js框后端Web框架使用 drizzle-orm 的 customType 在 MySQL 中定义自定义数据类型使用 drizzle orm 的 customType 在 MySQL 中定义自定义数据类型 本文以 drizzle orm mysql 0.16.1 版本引入后端数据库ORMEgg.js 官方教程使用 egg-mysql 插件接入 MySQL实现 CRUD、事务与动态数据源Egg.js 官方教程使用 egg mysql 插件接入 MySQL实现 CRUD、事务与动态数据源 本篇教程基于 Egg.js 官方文档体系中的 MySQ后端Web框架上一篇CN-GreenLumaGUI快速免安装的 Steam 解锁图形化工具点 3 下就搞定下一篇K3s节点磁盘空间管理终极指南如何配置自动清理策略创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考