拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

MikroORM 7 项目搭建实战:Fastify 应用、Vitest 测试、Seeder 填充与数据库迁移全流程

MikroORM 7 项目搭建实战:Fastify 应用、Vitest 测试、Seeder 填充与数据库迁移全流程 后端【免费下载链接】mikro-ormTypeScript ORM for Node.js based on Data Mapper, Unit of Work and Identity Map patterns. Supports MongoDB, MySQL, MariaDB, MS SQL Server, PostgreSQL and SQLite/libSQL databases.项目地址https://gitcode.com/gh_mirrors/mi/mikro-orm点击查看免费下载本文是 MikroORM 7 入门指南的第三章实战篇当你已经定义好实体、在本地玩具式验证过之后如何把它升级成一个真正可运行、可测试、可演进的 Web 项目。核心路线是用 Fastify 搭建带独立请求上下文的 HTTP 服务借助RequestContext与 Node.jsAsyncLocalStorage自动隔离每个请求的EntityManagerfork用 Vitest 对接口做端到端测试用mikro-orm/seeder填充测试数据最后用mikro-orm/migrations把 schema 变更纳入版本化管理。读完本文你将掌握 MikroORM 在真实服务端项目中的一整套工程化配置方式包括每个环节的源码级原理。本文以仓库 version-7.2/guide/03-project-setup.md 为主体骨架相关源码证据取自 packages/core、packages/seeder 与 packages/migrations。从实体定义到真实服务为什么需要这一步前面章节你已经定义好了User、Article、Tag、BaseEntity等实体并且能通过em.fork()绕过全局 context 校验来手动操作它们。但这种做法无法支撑一个多请求并发的 Web 服务——每个请求都应该拥有独立、隔离的EntityManager也就是独立的 Identity Map 与 Unit of Work否则不同请求之间会互相污染实体状态。本章要做的事可以归纳为四步用 Fastify 建起应用骨架并利用 MikroORM 自带的RequestContext自动为每个请求创建 EM fork添加第一个分页接口GET /article并用 Vitest 对它做测试引入mikro-orm/seeder为测试库填充数据引入mikro-orm/migrations管理生产库的 schema 演进。Fastify 应用骨架与 RequestContext 自动请求上下文创建 bootstrap 入口在src目录下新建app.ts导出一个bootstrap函数在其中创建 Fastify 实例。之前章节里手动调用em.fork()是为了绕过全局 context 校验而在 Web 服务中借助中间件Fastify 中是 hooks可以自动为每个请求创建独立的 EM forkMikroORM 提供的RequestContext正是为此设计的import { MikroORM, RequestContext } from mikro-orm/core; import { fastify } from fastify; import config from ./mikro-orm.config.js; export async function bootstrap(port 3001) { const orm await MikroORM.init(config); const app fastify(); // register request context hook app.addHook(onRequest, (request, reply, done) { RequestContext.create(orm.em, done); }); // shut down the connection when closing the app app.addHook(onClose, async () { await orm.close(); }); // register routes here // ... const url await app.listen({ port }); return { app, url }; }两个 hook 各司其职onRequest每个请求进来时调用RequestContext.create(orm.em, done)在请求的异步作用域内注入一个全局 EM 的 forkonClose应用关闭时同步关闭数据库连接避免进程悬挂。然后改写server.ts清空之前的玩具代码只负责启动import { bootstrap } from ./app.js; try { const { url } await bootstrap(); console.log(server started at ${url}); } catch (e) { console.error(e); }此时执行npm start你会看到类似下面的启动日志[info] MikroORM version: 7.0.0 [discovery] ORM entity discovery started [discovery] - processing entity User [discovery] - processing entity Article [discovery] - processing entity Tag [discovery] - processing entity BaseEntity [discovery] - entity discovery finished, found 5 entities, took 5 ms [info] MikroORM successfully connected to database sqlite.db server started at http://127.0.0.1:3001服务器已经跑起来了按CTRL C即可停止。RequestContext 到底做了什么源码级拆解文档用一段代码清晰地展示了RequestContext的魔法所在你在业务代码里调用的是全局orm.em.find()但底层实际执行的是请求上下文中的 fork。// we call em.find() on the global EM instance const res await orm.em.find(Book, {}); // but under the hood this resolves to const res await orm.em.getContext().find(Book, {}); // which then resolves to const res await RequestContext.getEntityManager().find(Book, {});这条链路在源码中可以得到完整印证。RequestContext类定义在 packages/core/src/utils/RequestContext.ts它内部持有一个静态的AsyncLocalStorage实例由 AsyncContext.ts 中的createAsyncContext()创建优先使用 Node.js 内置的node:async_hooks模块非 Node 环境则有降级实现RequestContext.create(em, next)会先调用createContext()对传入的 EM可以是单个也可以是数组用于多数据库场景执行em.fork({ useContext: true })创建 fork 并存入Mapstring, EntityManager再通过storage.run(ctx, next)在异步作用域内执行后续回调RequestContext.getEntityManager()通过storage.getStore()取回当前上下文再从 map 中按名称默认default取出对应的 EM fork。而EntityManager侧所有涉及 Identity Map 的方法如em.find()、em.getReference()都会先调用getContext()。该方法的实现位于 packages/core/src/EntityManager.ts#L2963-L2982逻辑是优先使用事务上下文TransactionContext否则通过config.get(context)回调获取当前请求上下文对应的 fork如果拿到的 fork 是全局实例且未开启allowGlobalContext则抛出ValidationError.cannotUseGlobalContext()——这正是前几章你手动 fork 要绕过的校验。AsyncLocalStorage的价值在于它把fork 的创建时机中间件里与fork 的使用位置业务代码任意深度解耦无论中间隔了多少层异步调用只要没有跳出该请求的异步链getEntityManager()都能取回同一个上下文。这也解释了为什么onRequesthook 里传入的done回调必须被RequestContext.create包裹执行。第一个接口GET /article 与 findAndCount接下来添加第一个真实接口GET /article公开分页接口接收limit和offset查询参数返回请求页的条目以及全部可用文章的总数。虽然可以分别用em.count()拿总数但既然要把总数和分页列表一起返回MikroORM 提供了更贴切的em.findAndCount()——一次调用同时返回[items, total]元组app.get(/article, async request { const { limit, offset } request.query as { limit?: number; offset?: number }; const [items, total] await orm.em.findAndCount(ArticleSchema, {}, { limit, offset, }); return { items, total }; });findAndCount的底层实现见 packages/core/src/EntityManager.ts#L1020-L1049它先尝试自动 flush并显式把flushMode改为commit避免重复自动 flush然后用Promise.all并发执行em.find()与em.count()。也就是说你拿到的是同一次查询语义下的列表与计数而计数本身不会把整张表加载进内存。轻量 DI 容器db.ts在写测试之前先做一次面向未来的重构新增src/db.ts作为简单的依赖注入DI容器导出initORM()函数首次调用时初始化 ORM 并缓存到内存后续调用直接返回同一实例。为什么要用一个函数而不是顶层await直接导出因为测试阶段往往需要覆盖部分配置比如换成内存数据库、关闭 debug 日志有了initORM(options?)这样的入口测试代码就可以在初始化前注入自己的选项。import { EntityManager, EntityRepository, MikroORM, Options } from mikro-orm/sqlite; import { UserSchema, type User } from ./modules/user/user.entity.js; import { ArticleSchema, type IArticle } from ./modules/article/article.entity.js; import { TagSchema, type ITag } from ./modules/article/tag.entity.js; import config from ./mikro-orm.config.js; export interface Services { orm: MikroORM; em: EntityManager; article: EntityRepositoryIArticle; user: EntityRepositoryUser; tag: EntityRepositoryITag; } let cache: Services; export function initORM(options?: PartialOptions): Services { if (cache) { return cache; } const orm new MikroORM({ ...config, ...options, }); // save to cache before returning return cache { orm, em: orm.em, article: orm.em.getRepository(ArticleSchema), user: orm.em.getRepository(UserSchema), tag: orm.em.getRepository(TagSchema), }; }关键点在于EntityManager、EntityRepository、MikroORM、Options全部从mikro-orm/sqlite驱动包导入而不是mikro-orm/core。这样这些类型会被自动绑定到SqliteDriver无需手工传泛型。为什么 EntityManager 要从驱动包导入EntityManager与EntityRepository的基类确实定义在mikro-orm/core但那是驱动无关的实现。以QueryBuilder为例它是 SQL 概念不该出现在核心包里而是由 SQL 驱动包提供EntityManager的扩展类SqlEntityManager定义于mikro-orm/sql被所有依赖它的 SQL 驱动包重新导出额外的 SQL 方法如em.createQueryBuilder()就挂在这个扩展类上。出于便利SqlEntityManager也被重新导出为EntityManager别名因此你可以直接写import { EntityManager, EntityRepository } from mikro-orm/sqlite; // or any other driver package在运行时MikroORM 内部始终使用驱动特定的 EM 实现你可以通过console.log(orm.em)验证它会是SqlEntityManager的实例但要让 TypeScript 也明白这一点就得从驱动包导入。MikroORM、defineConfig、Options同理从驱动包导入即可获得驱动类型而无需泛型。EntityRepository 是什么仓库Repository是构建在EntityManager之上的薄层充当扩展点你可以为它添加自定义方法甚至改写现有方法。默认的EntityRepository实现只是把调用转发给底层的EntityManager实例——在 packages/core/src/entity/EntityRepository.ts 中可以看到如find()、findAndCount()等方法都是一行转发return this.getEntityManager().find(this.entityName, ...)。EntityRepository携带了实体类型所以你不需要在每次find或findOne时重复传入实体类。另外要注意不存在flush 某个 repository这回事——repo.flush()只是em.flush()的快捷方式因为 Unit of Work 总是整体 flush而非只 flush 该 repository 代表的单条实体。改造后的app.ts通过initORM()拿到db对象路由直接使用类型化的仓库import { RequestContext } from mikro-orm/core; import { fastify } from fastify; import { initORM } from ./db.js; export async function bootstrap(port 3001) { const db initORM(); const app fastify(); // register request context hook app.addHook(onRequest, (request, reply, done) { RequestContext.create(db.em, done); }); // shut down the connection when closing the app app.addHook(onClose, async () { await db.orm.close(); }); // register routes here app.get(/article, async request { const { limit, offset } request.query as { limit?: number; offset?: number }; const [items, total] await db.article.findAndCount({}, { limit, offset, }); return { items, total }; }); const url await app.listen({ port }); return { app, url }; }用 Vitest 测试第一个接口接口就绪接下来为它写测试。你已经在npm test中有了 vitest现在把测试文件放进test目录并用.test.ts后缀命名即可被 vitest 识别。测试思路是使用 Fastify 的app.inject()模拟 HTTP 请求。但直接调用bootstrap()会连上生产数据库——测试绝不希望这样。因此先创建一个不带.test.ts后缀的工具文件test/utils.ts定义initTestApp用覆盖后的配置初始化 ORM、创建 schema、再启动 Fastify 应用一气呵成。它接收port参数让每个测试用例拥有各自的内存数据库 独立端口从而支持并行测试import { bootstrap } from ../src/app.js; import { initORM } from ../src/db.js; import config from ../src/mikro-orm.config.js; export async function initTestApp(port: number) { // this will create all the ORM services and cache them const { orm } initORM({ // first, include the main config ...config, // no need for debug information, it would only pollute the logs debug: false, // use in-memory database, this way tests can easily be parallelized dbName: :memory:, }); // create the schema so the database can be used await orm.schema.create(); const { app } await bootstrap(port); return app; }dbName: :memory:是 SQLite 的特殊数据库名表示纯内存数据库——这是测试能够并行化的根基。debug: false避免日志污染测试输出。然后是第一个测试用例test/article.test.ts。此时内存库是空的所以文章列表接口会返回空数组import { afterAll, beforeAll, expect, test } from vitest; import { FastifyInstance } from fastify; import { initTestApp } from ./utils.js; let app: FastifyInstance; beforeAll(async () { // use different ports to allow parallel testing app await initTestApp(30001); }); afterAll(async () { // we close only the fastify app - it will close the database connection via onClose hook automatically await app.close(); }); test(list all articles, async () { // mimic the http request via app.inject() const res await app.inject({ method: get, url: /article, }); // assert it was successful response expect(res.statusCode).toBe(200); // with expected shape expect(res.json()).toMatchObject({ items: [], total: 0, }); });注意beforeAll初始化应用、afterAll拆卸应用app.close()会触发onClosehook 进而调用orm.close()。如果没有这一步进程会悬挂不退出。运行npm test预期结果✓ test/article.test.ts (1) Test Files 1 passed (1) Tests 1 passed (1) Start at 15:56:41 Duration 876ms (transform 264ms, setup 0ms, collect 300ms, tests 147ms) PASS Waiting for file changes... press h to show help, press q to quit关于单元测试的忠告有些不需要数据库连接的单元测试可能会想跳过MikroORM.init()阶段但init做的远不止建立连接。最关键的是元数据发现metadata discoveryORM 会检查所有实体定义为各种元数据选项设置默认值主要是命名策略和双向关系的处理。而发现阶段是 propagation实体变更传播正常工作所必需的所以即使不连库也建议保留初始化流程const orm new MikroORM({ // ... });用 Seeder 填充测试数据安装与注册填充测试数据有很多方式最直接的是在测试的beforeAll里手动em.createem.persist。另一种更工程化的方式是使用mikro-orm/seeder包它提供了一系列工具来用不一定是假的数据填充数据库。Seeder 同样适合给生产库造初始数据——比如默认的标签集合、初始管理员账号你可以建立 seeder 层级或逐个调用。先安装并注册扩展npm install mikro-orm/seederimport { defineConfig } from mikro-orm/sqlite; import { SeedManager } from mikro-orm/seeder; export default defineConfig({ // ... extensions: [SeedManager], });注册后SeedManager会通过orm.seeder属性暴露。可以使用的其他扩展还有SchemaGenerator、Migrator和EntityGenerator其中SchemaGenerator以及MongoSchemaGenerator是自动注册的因为它不依赖任何第三方包。用 CLI 生成 seeder 骨架npx mikro-orm seeder:create test这会创建src/seeders目录和TestSeeder.ts文件内含骨架import type { EntityManager } from mikro-orm/core; import { Seeder } from mikro-orm/seeder; export class TestSeeder extends Seeder { async run(em: EntityManager): Promisevoid {} }Seeder抽象类的定义位于 packages/seeder/src/Seeder.tsSeedManager.seed()的执行逻辑见 packages/seeder/src/SeedManager.ts#L79它会依次调用每个 seeder 的run(em)方法。填充数据在run里使用前面章节介绍过的em.create()。它会在返回创建后的实体前自动调用em.persist()所以只需调用em.create()本身即可配合默认的 flush 策略完成持久化export class TestSeeder extends Seeder { async run(em: EntityManager): Promisevoid { em.create(UserSchema, { fullName: Foo Bar, email: foobar.com, password: password123, articles: [ { title: title 1/3, description: desc 1/3, text: text text text 1/3, tags: [{ id: 1, name: foo1 }, { id: 2, name: foo2 }], }, { title: title 2/3, description: desc 2/3, text: text text text 2/3, tags: [{ id: 2, name: foo2 }], }, { title: title 3/3, description: desc 3/3, text: text text text 3/3, tags: [{ id: 2, name: foo2 }, { id: 3, name: foo3 }], }, ], }); } }注意这里嵌套的articles数组展示了em.create()对关系数据的级联处理一个用户携带三篇文章每篇文章又携带标签全部通过一次create调用建立起来。然后在initTestApp中、orm.schema.create()之后调用 seederawait orm.schema.create(); await orm.seeder.seed(TestSeeder);最后更新测试断言——现在接口会返回 3 篇文章expect(res.json()).toMatchObject({ items: [ { author: 1, slug: title-13, title: title 1/3 }, { author: 1, slug: title-23, title: title 2/3 }, { author: 1, slug: title-33, title: title 3/3 }, ], total: 3, });再次运行npm test验证。SchemaGenerator程序化与 CLI 两种用法前文你在测试中用orm.schema.create()重建数据库这里展开讲讲SchemaGenerator。完整文档见 schema-generator。SchemaGenerator负责根据实体元数据生成 SQL 查询——也就是把实体定义翻译成 DDL。更进一步它还能读取当前数据库 schema 并与元数据对比生成让 schema 同步所需的查询。程序化用法// to get the queries const diff await orm.schema.getUpdateSchemaSQL(); console.log(diff); // or to run the queries await orm.schema.update();借助orm.schema.update()你可以轻松复刻 TypeORM 的synchronize: true行为——在 ORM 初始化后或应用 bootstrap 代码里调用即可。但要牢记这种方式可能是破坏性的官方不建议用于生产执行前务必先审查SchemaGenerator生成的查询。CLI 用法要真正执行查询把--dump换成--runnpx mikro-orm schema:create --dump # Dumps create schema SQL npx mikro-orm schema:update --dump # Dumps update schema SQL npx mikro-orm schema:drop --dump # Dumps drop schema SQL你的生产库项目根目录的sqlite.db很可能与实体不同步了之前测试用的都是内存库。先--dump或-d预览生成的查询确认无误后--run或-r执行# first check what gets generated npx mikro-orm schema:update --dump # and when its fine, sync the schema npx mikro-orm schema:update --run如果命令生成了非法查询可以彻底重建先schema:drop --run再重建。SchemaGenerator在原型阶段或测试场景需要大量拥有最新 schema 的数据库、且与生产 schema 形态无关非常好用但直接作用于真实生产库是危险的。安全的演进方案是下文的主角migrations。Migrations把 schema 变更纳入版本管理安装、注册与第一条迁移使用迁移需要先安装mikro-orm/migrationsSQL 驱动或mikro-orm/migrations-mongodbMongoDB并在 ORM 配置里注册Migrator扩展。MikroORM 对迁移有内建支持可以基于当前 schema 差异生成迁移也能管理它们的执行。默认情况下每条迁移在独立事务中执行所有迁移又包裹在一个主事务里——一旦某条失败全部回滚。npm install mikro-orm/migrationsimport { defineConfig } from mikro-orm/sqlite; import { SeedManager } from mikro-orm/seeder; import { Migrator } from mikro-orm/migrations; export default defineConfig({ // ... extensions: [SeedManager, Migrator], });创建第一条迁移npx mikro-orm migration:create如果严格跟着指南走到这里你会看到No changes required, schema is up-to-date原因是你刚才用npx mikro-orm schema:update --run同步过 schema。此时有两个选择先 drop schema 再生成或者使用破坏性更小的方案——初始迁移。Initial migration初始迁移如果 schema 已存在、而你希望改用迁移来管理--initial标志会在保留现有 schema的前提下仅基于实体元数据生成第一条迁移。它只允许在 schema 为空或完全同步的情况下使用。若 schema 已存在生成的迁移会被自动标记为已执行若 schema 不存在则需要像其他迁移一样手动执行npx mikro-orm migration:up。初始迁移只能在没有任何已生成或已执行的迁移时创建。如果你是从零开始、还没有 schema就不必用--initial普通迁移同样能完成工作。npx mikro-orm migration:create --initial这会在src/migrations目录生成包含schema:create全部查询的初始迁移因为你的 schema 已同步迁移会被自动标记为已执行。迁移类的结构生成的迁移是一个继承自mikro-orm/migrations包中Migration抽象类的类import { Migration } from mikro-orm/migrations; export class Migration20220913202829 extends Migration { async up(): Promisevoid { this.addSql(create table tag (id integer not null primary key autoincrement, created_at datetime not null, updated_at datetime not null, name text not null);); // ... } }Migration基类源码见 packages/migrations/src/Migration.ts值得注意的细节up()是必须实现的抽象方法down()默认抛错This migration cannot be reverted需要回滚支持时自行实现addSql(sql)接受的不只是字符串Query类型是string | NativeQueryBuilder | RawQueryFragment即原生查询构建器实例或raw()SQL 片段也可以直接入队你还可以在up()/down()内通过this.execute(...)执行查询它会与迁移的其余部分运行在同一事务中。关于 SQLite 的回滚限制MikroORM 会自动生成 down 迁移出于安全考虑初始迁移除外唯一例外是 SQLite 驱动——因其能力受限。使用其他驱动时非初始迁移都会附带 down 版本。迁移的完整文档见 migrations。用真实 schema 变更演练新增 Comment 实体迁移体系搭好了现在加一个Comment实体来实战检验。它属于 article 模块放入src/modules/article/comment.entity.tsimport { defineEntity, type InferEntity, p } from mikro-orm/core; import { ArticleSchema } from ./article.entity.js; import { UserSchema } from ../user/user.entity.js; import { BaseSchema } from ../common/base.entity.js; export const CommentSchema defineEntity({ name: Comment, extends: BaseSchema, properties: { text: p.string(), article: () p.manyToOne(ArticleSchema).ref(), author: () p.manyToOne(UserSchema).ref(), }, }); export type IComment InferEntitytypeof CommentSchema;并在Article实体中加入 OneToMany 反向侧comments: () p.oneToMany(CommentSchema).mappedBy(article).eager().orphanRemoval(),这里用到两个新的构建器方法.eager()自动填充该关系效果等同于显式populate: [comments].orphanRemoval()一种特殊的级联方式——从该集合中移除的实体会从数据库中被删除而不是仅通过把外键置空来脱离关系。别忘了同步更新 DI 容器加入comment仓库export interface Services { orm: MikroORM; em: EntityManager; user: UserRepository; article: EntityRepositoryIArticle; comment: EntityRepositoryIComment; tag: EntityRepositoryITag; } export function initORM(options?: PartialOptions): Services { // ... return cache { orm, em: orm.em, user: orm.em.getRepository(UserSchema), article: orm.em.getRepository(ArticleSchema), comment: orm.em.getRepository(CommentSchema), tag: orm.em.getRepository(TagSchema), }; }然后通过 CLI 创建并执行迁移顺带尝试其他迁移相关命令# create new migration based on the schema difference npx mikro-orm migration:create # list pending migrations npx mikro-orm migration:pending # run the pending migrations npx mikro-orm migration:up # list executed migrations npx mikro-orm migration:list预期输出如下npx mikro-orm migration:create Migration20220913205718.ts successfully creatednpx mikro-orm migration:pending ┌─────────────────────────┐ │ Name │ ├─────────────────────────┤ │ Migration20220913205718 │ └─────────────────────────┘npx mikro-orm migration:up Processing Migration20220913205718 Applied Migration20220913205718 Successfully migrated up to the latest versionnpx mikro-orm migration:list ┌─────────────────────────┬──────────────────────────┐ │ Name │ Executed at │ ├─────────────────────────┼──────────────────────────┤ │ Migration20220913202829 │ 2022-09-13T18:57:12.000Z │ │ Migration20220913205718 │ 2022-09-13T18:57:27.000Z │ └─────────────────────────┴──────────────────────────┘迁移快照snapshots创建新迁移时会自动把目标 schema 快照存入 migrations 文件夹。之后再次创建迁移时会基于该快照而非当前数据库 schema 计算差异——这意味着即使你在执行 pending 迁移之前就创建新迁移依然能得到正确的 schema diff。快照应当像普通迁移文件一样纳入版本管理也可以通过在 ORM 配置中设置migrations.snapshot: false关闭快照。在 bootstrap 中自动执行迁移最后把迁移执行自动化在应用开始接受连接之前、ORM 初始化之后执行迁移。bootstrap函数增加一个migrate参数export async function bootstrap(port 3001, migrate true) { const db initORM(); if (migrate) { // sync the schema await db.orm.migrator.up(); } // ... }之所以要条件执行是因为迁移只应该跑在生产库上测试库走的是SchemaGeneratorSeeder路线。所以测试工具里要显式传falseexport async function initTestApp(port: number) { const { orm } initORM({ ... }); await orm.schema.create(); await orm.seeder.seed(TestSeeder); const { app } await bootstrap(port, false); // -- here return app; }至此生产环境启动时自动应用迁移测试环境则从零创建 schema 并填充种子数据两条路径互不干扰。⛳ Checkpoint 3本章成果小结本章结束时你拥有了4 个实体User、Article、Tag、Comment、一个带单个GET /article接口并运行在 Fastify 上的 Web 应用、一个基本的端到端测试用例以及完整可用的seeding 与 migrations 体系。值得回顾的工程决策每个请求通过RequestContext内部是AsyncLocalStorage获得独立的 EM forkIdentity Map 与 Unit of Work 在请求间完全隔离DI 容器initORM()缓存了 ORM 实例并暴露类型化的仓库测试时通过PartialOptions覆盖dbName: :memory:与debug: false实现并行测试生产库用migrator.up()自动演进测试库用schema.create()seeder.seed()即时重建涉及up()/down()可回滚、迁移快照、主事务包裹等能力均有对应源码实现支撑Migration.ts、MigrationRunner.ts、SeedManager.ts。本章的完整app.ts形态与后续章节可继续在本指南的后续章节中演进。赞分享后端【免费下载链接】mikro-ormTypeScript ORM for Node.js based on Data Mapper, Unit of Work and Identity Map patterns. Supports MongoDB, MySQL, MariaDB, MS SQL Server, PostgreSQL and SQLite/libSQL databases.项目地址https://gitcode.com/gh_mirrors/mi/mikro-orm点击查看免费下载相关推荐MikroORM 项目搭建实战Fastify Vitest 下的请求上下文、Seeder 与迁移管理MikroORM 项目搭建实战Fastify Vitest 下的请求上下文、Seeder 与迁移管理 本篇技术指南基于 MikroORM 官方入门指南第三后端MikroORM 实战用 Fastify Vitest 搭建项目、测试接口并管理 Schema 与迁移MikroORM 实战用 Fastify Vitest 搭建项目、测试接口并管理 Schema 与迁移 本篇指南承接 MikroORM 的实体定义章节完后端MikroORM 项目实战搭建指南Fastify 服务、RequestContext、测试、Seeder 与迁移Chapter 3: Project SetupMikroORM 项目实战搭建指南Fastify 服务、RequestContext、测试、Seeder 与迁移Chapter 3: Project Set后端上一篇OpenSEO v0.0.9 深度解析域关键词服务端分页过滤、一键导出 Google Sheets 与 URL 驱动的可分享搜索状态下一篇SkillSpector Batch Scan 未来工作路线图从已知限制到优化方向的全景剖析创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表