ARTICLE DETAIL

资讯详情

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

MikroORM 项目搭建实战:Fastify + Vitest + RequestContext + 迁移与种子数据完整指南

MikroORM 项目搭建实战:Fastify + Vitest + RequestContext + 迁移与种子数据完整指南 后端【免费下载链接】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 官方文档《Chapter 3: Project Setup》展开面向已经完成实体定义User/Article/Tag 等的开发者系统讲解如何将 MikroORM 接入 Fastify Web 服务器、使用 Vitest 进行接口测试并深入剖析RequestContext请求上下文、EntityRepository仓储模式、SchemaGenerator模式同步、Migrator数据库迁移以及Seeder种子数据填充的完整落地流程。读完本文你将掌握一套可复制的、具备单元测试与迁移能力的 MikroORM Fastify 项目骨架。一、为什么需要“项目级”装配前两章中你只是围绕实体定义做简单验证从这一章开始要构建真实可运行的服务。核心诉求有三为每个 HTTP 请求提供独立的EntityManager上下文Identity Map 的隔离避免跨请求状态污染为接口提供可自动化验证的测试通道内存数据库 并行测试为 schema 演进提供安全手段SchemaGenerator 快速原型 Migrator 生产级迁移。本章选用 Fastify 作为 Web 服务器、Vitest 作为测试框架二者都是 Node.js 生态中轻量、高性能且与 TypeScript 配合良好的选择。二、用 Fastify 搭起服务骨架app.ts与server.ts2.1RequestContext请求级 EntityManager 的核心机制前面章节里你通过手动fork()全局EntityManager来绕过全局上下文校验。而在 Web 服务器场景下MikroORM 提供了官方助手类RequestContext它能在每个请求内自动创建并持有独立的 EM fork。核心原理源码级RequestContext内部使用 Node.js 核心模块AsyncLocalStorage见 packages/core/src/utils/AsyncContext.ts来跟踪异步调用链上的上下文。RequestContext.create(em, done)会为当前EntityManager执行em.fork({ useContext: true })并将 fork 结果存入AsyncLocalStorage的 store见 packages/core/src/utils/RequestContext.ts。EntityManager本身对RequestContext是“感知”的所有涉及 Identity Map 的方法如em.find()、em.getReference()在执行前都会调用em.getContext()。该方法优先返回TransactionContext中的 EM其次通过config.get(context)回调即RequestContext.getEntityManager()解析当前异步上下文中的 fork见 packages/core/src/EntityManager.ts。因此你在业务代码里写的const res await orm.em.find(Book, {});实际解析为const res await orm.em.getContext().find(Book, {}); // 进一步解析为 const res await RequestContext.getEntityManager().find(Book, {});RequestContext.getEntityManager()从AsyncLocalStorage的 store 中取出当前请求对应的 fork见 packages/core/src/utils/RequestContext.ts。这样EM fork 的创建在中间件/hook 中与使用在任意深度的业务代码中被彻底解耦。从源码结构看RequestContext还提供了enter()适用于没有next回调的框架中间件如 Elysia以及currentRequestContext()等方法create()也支持传入EntityManager[]数组来同时管理多个命名上下文。2.2 编写app.ts在src目录新建app.ts导出一个bootstrap函数在其中初始化 ORM 并创建 Fastify 实例注册onRequest钩子来为每个请求创建上下文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(); // 为每个请求创建独立的 EM 上下文 app.addHook(onRequest, (request, reply, done) { RequestContext.create(orm.em, done); }); // 应用关闭时同步关闭数据库连接 app.addHook(onClose, async () { await orm.close(); }); // 在此注册路由 // ... const url await app.listen({ port }); return { app, url }; }注意RequestContext.create(orm.em, done)中的done是 Fastify 钩子的回调——AsyncLocalStorage.run(ctx, next)会在这个回调内执行后续的请求处理链路从而让整个请求的生命周期都处于该上下文中。2.3 编写server.ts启动入口用以下内容替换之前实验用的server.tsimport { 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即可停止服务。三、第一个接口GET /article与findAndCount3.1 为什么用findAndCount而不是countfind接口需求是返回文章分页列表并同时给出文章总数。虽然可以先用em.count()再em.find()但 MikroORM 提供了更贴合的em.findAndCount()——它正是为“分页列表 总数”场景设计。从 packages/core/src/EntityManager.ts 的源码看findAndCount内部会先执行一次tryFlush确保未提交的变更进入数据库再计数然后通过Promise.all([em.find(...), em.count(...)])并行执行查询并显式将flushMode设为commit以避免重复自动 flush。返回类型为[实体数组, 总数]的元组。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 }; });limit/offset直接透传给查询选项即可完成分页。四、简易 DI 容器db.ts与initORM()在写测试之前先做一次面向未来的重构把 ORM 初始化收敛到一个“简易依赖注入DI容器”中。4.1 为什么不用顶层export const orm await MikroORM.init(...)虽然借助 top-level await 可以直接初始化并导出实例但后续做测试时往往需要覆盖部分配置如切换内存数据库、关闭 debug 日志。封装一个initORM(options?)函数既能缓存实例避免重复初始化又能在首次初始化前注入覆盖项。4.2 从驱动包导入类型化导出注意EntityManager、EntityRepository、MikroORM、Options都应从驱动包如mikro-orm/sqlite导入而不是从mikro-orm/core。原因是mikro-orm/core只提供驱动无关的基类而 SQL 驱动包会导出扩展后的SqlEntityManager提供createQueryBuilder()等 SQL 专属方法并以EntityManager别名再导出从而让 TypeScript 自动获得与SqliteDriver绑定的类型无需手写泛型。从源码结构看SqlEntityManager定义于mikro-orm/sql包并在每个依赖它的 SQL 驱动包中被再导出。运行时orm.em实际就是SqlEntityManager实例你可以用console.log(orm.em)验证。EntityRepository/SqlEntityRepository同理。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, }); // 先缓存再返回保证后续调用拿到同一实例 return cache { orm, em: orm.em, article: orm.em.getRepository(ArticleSchema), user: orm.em.getRepository(UserSchema), tag: orm.em.getRepository(TagSchema), }; }4.3 改造app.ts使用 DI 容器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(); app.addHook(onRequest, (request, reply, done) { RequestContext.create(db.em, done); }); app.addHook(onClose, async () { await db.orm.close(); }); 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 }; }五、什么是EntityRepository仓储是EntityManager之上的一层“薄封装”它充当扩展点可以添加自定义方法甚至改写现有方法默认实现只是把调用转发给底层EntityManager它携带实体类型因此调用find/findOne时无需重复传入实体类不存在“flush 一个仓储”——repo.flush()只是em.flush()的快捷方式MikroORM 始终 flush 整个 Unit of Work而不是某个仓储代表的单个实体。在 DI 容器中一次性取出article/user/tag等仓储后续业务代码即可直接db.article.findAndCount(...)。六、用 Vitest 测试接口6.1 测试策略app.inject() 内存数据库Fastify 提供了app.inject()方法可以在不真正监听端口的情况下模拟 HTTP 请求非常适合集成测试。但直接复用生产数据库显然不合适因此测试要走“独立实例”路线每个测试用例使用独立的 SQLite 内存数据库dbName: :memory:每个测试用例运行在独立的端口上从而支持并行执行通过beforeAll初始化、afterAll关闭避免进程悬挂。6.2 编写测试工具test/utils.ts在test目录创建utils.ts不带.test.ts后缀避免被 Vitest 当作测试文件导出initTestApp一次性完成“覆盖配置初始化 ORM → 创建 schema → 启动 Fastify”。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) { // 创建并缓存所有 ORM 服务 const { orm } initORM({ // 先继承主配置 ...config, // 关闭调试输出避免污染测试日志 debug: false, // 使用内存数据库便于测试并行化 dbName: :memory:, }); // 创建 schema 以便数据库可用 await orm.schema.create(); const { app } await bootstrap(port); return app; }6.3 编写测试用例test/article.test.tsimport { afterAll, beforeAll, expect, test } from vitest; import { FastifyInstance } from fastify; import { initTestApp } from ./utils.js; let app: FastifyInstance; beforeAll(async () { // 使用不同端口以支持并行测试 app await initTestApp(30001); }); afterAll(async () { // 只关闭 fastify app —— 它会通过 onClose 钩子自动关闭数据库连接 await app.close(); }); test(list all articles, async () { // 通过 app.inject() 模拟 http 请求 const res await app.inject({ method: get, url: /article, }); // 断言响应成功 expect(res.statusCode).toBe(200); // 断言响应结构符合预期 expect(res.json()).toMatchObject({ items: [], total: 0, }); });目前内存库为空因此/article返回空数组与total: 0稍后接入 Seeder 后会填充数据。beforeAll/afterAll的配对很重要app.close()会触发onClose钩子进而调用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 quit6.4 单元测试的注意事项即使某些单元测试不需要连接数据库也不要轻易跳过MikroORM初始化。init阶段除了建立连接更重要的是元数据发现discoveryORM 会检查所有实体定义为各类元数据选项设置默认值主要是命名策略与双向关系的补全。而发现阶段是传播propagation机制能够工作的前提详见 docs/versioned_docs/version-7.1/guide/../propagation.md。因此即便只是new MikroORM({...})而不连库也要保留这一初始化过程。七、用 Seeder 填充测试数据7.1 安装与注册npm install mikro-orm/seeder然后在 ORM 配置中注册SeedManager扩展之后即可通过orm.seeder使用import { defineConfig } from mikro-orm/sqlite; import { SeedManager } from mikro-orm/seeder; export default defineConfig({ // ... extensions: [SeedManager], });除SeedManager外MikroORM 还提供SchemaGenerator、Migrator、EntityGenerator等扩展。SchemaGenerator以及MongoSchemaGenerator因不依赖任何第三方包会被自动注册无需手动加入extensions。从 packages/seeder/src/SeedManager.ts 源码可见SeedManager.register(orm)会把扩展注册为mikro-orm/seeder构造时它会对em执行一次fork()并强制设置persistOnCreate: true——这正是下面em.create()无需手动persist的原因。7.2 生成 Seeder 骨架npx mikro-orm seeder:create test该命令会在src/seeders目录生成TestSeeder.ts初始骨架如下该模板由SeedManager.generate生成见 packages/seeder/src/SeedManager.tsimport type { EntityManager } from mikro-orm/core; import { Seeder } from mikro-orm/seeder; export class TestSeeder extends Seeder { async run(em: EntityManager): Promisevoid {} }7.3 填充数据前面章节提到过em.create()它会先调用em.persist(entity)再返回创建的实体所以只需调用一次即可完成“创建 入队持久化”。填充 3 篇文章及其标签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 }], }, ], }); } }从 packages/seeder/src/SeedManager.ts 源码可见seed()会依次实例化 Seeder、执行其run(em)、随后em.flush()并em.clear()——即每个 Seeder 运行完自动落库并清空上下文。Seeder 同样适用于生产库初始化如默认标签集、初始管理员可构建 Seeder 层级或逐个调用。7.4 在测试初始化中执行 Seeder修改test/utils.ts在orm.schema.create()之后执行种子await orm.schema.create(); await orm.seeder.seed(TestSeeder);7.5 更新测试断言现在内存库中有 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从实体元数据到 DDL8.1 职责与用法SchemaGenerator负责基于实体元数据生成 SQL即把实体定义翻译成 DDL并且能够读取当前数据库 schema 并与元数据比对产出使二者同步所需的查询语句。它既可编程调用也可通过 CLI 使用。编程方式// 仅获取查询语句不执行 const diff await orm.schema.getUpdateSchemaSQL(); console.log(diff); // 直接执行同步查询 await orm.schema.update();orm.schema.update()可以实现类似 TypeORMsynchronize: true的行为——在应用初始化/bootstrap 后立即调用即可。但请牢记这种方式可能是破坏性的强烈不建议在生产环境使用执行前务必先检查SchemaGenerator生成的查询内容。CLI 方式用--run替换--dump即真正执行npx mikro-orm schema:create --dump # 输出创建 schema 的 SQL npx mikro-orm schema:update --dump # 输出更新 schema 的 SQL npx mikro-orm schema:drop --dump # 输出删除 schema 的 SQL8.2 同步生产库示例测试中一直使用内存库项目根目录的sqlite.db生产库可能已与实体不同步。先--dump或-d查看将要执行的 SQL确认无误后再--run或-r真正执行# 先检查生成的 SQL npx mikro-orm schema:update --dump # 确认无误后同步 schema npx mikro-orm schema:update --run若生成的查询有问题可以先用schema:drop --run清空后重新创建 schema。SchemaGenerator在原型验证和测试场景需要大量“最新 schema”的数据库中非常顺手但对真实生产库有风险——这正是下一节 Migrations 的用武之地。九、Migrations生产级的 schema 演进9.1 安装与注册npm install mikro-orm/migrations对 MongoDB 请改用mikro-orm/migrations-mongodb包。注册Migrator扩展import { defineConfig } from mikro-orm/sqlite; import { SeedManager } from mikro-orm/seeder; import { Migrator } from mikro-orm/migrations; export default defineConfig({ // ... extensions: [SeedManager, Migrator], });Migrator类在 packages/migrations/src/Migrator.ts 中定义它基于AbstractMigrator实现内部持有SqlSchemaGenerator用于比对差异并根据options.emit选择TSMigrationGenerator或JSMigrationGenerator生成迁移文件。MikroORM 对迁移的内置支持包括根据当前 schema 差异生成迁移管理迁移执行默认情况下每个迁移在独立事务中执行全部迁移再被包裹进一个主事务——任一迁移失败则整体回滚。9.2 创建首个迁移npx mikro-orm migration:create如果此前刚执行过schema:update --runschema 已是最新会看到No changes required, schema is up-to-date此时有两种选择先schema:drop清库或采用**初始迁移initial migration**这一破坏性更小的方案。9.3 初始迁移--initial--initial标志适用于“schema 已存在但想开始使用迁移”的场景它保留现有 schema仅基于实体元数据生成首个迁移。使用前提是 schema 为空或完全最新若 schema 已存在生成的迁移会被自动标记为已执行否则需要手动migration:up执行。初始迁移只有在没有任何已生成或已执行的迁移时才能创建。如果是全新项目、尚无 schema则无需--initial普通迁移即可。npx mikro-orm migration:create --initial这会在src/migrations目录生成初始迁移内容来自schema:create的查询由于 schema 已同步迁移会自动标记为已执行。9.4 迁移类结构生成的迁移继承自mikro-orm/migrations的抽象类Migrationimport { 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);); // ... } }要点默认只生成up()如需回滚可实现down()默认实现直接抛错自动生成 down 迁移除 SQLite 驱动受限于其能力外其他驱动会自动生成 down 迁移——但初始迁移除外出于安全考虑在up()/down()中可通过this.execute(...)执行查询它会与迁移的其余部分处于同一事务this.addSql()也接受原生 QueryBuilder 实例或raw()SQL 片段。更多迁移细节见 docs/versioned_docs/version-7.1/guide/../migrations。9.5 加一个实体来验证迁移Comment在src/modules/article/comment.entity.ts新增Comment实体使用defineEntity与p属性构建器import { 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()一种特殊级联——从该集合中移除的实体将被从数据库删除而不是仅仅脱离关系把外键置空。别忘了把comment仓储加进 DI 容器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), }; }9.6 迁移相关 CLI 命令全流程# 基于 schema 差异创建新迁移 npx mikro-orm migration:create # 列出待执行迁移 npx mikro-orm migration:pending # 执行待执行的迁移 npx mikro-orm migration:up # 列出已执行的迁移 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 │ └─────────────────────────┴──────────────────────────┘9.7 迁移快照Migration snapshots创建新迁移时MikroORM 会自动把目标 schema 快照保存到迁移目录。之后再次创建迁移时会基于该快照计算差异而非当前数据库 schema——这意味着在尚未执行 pending 迁移的情况下再次创建迁移仍能得到正确的 schema diff。快照应与普通迁移文件一样纳入版本控制。如需关闭快照功能可在 ORM 配置中设置migrations.snapshot: false。9.8 应用启动时自动执行迁移在生产环境希望在应用开始接收连接前完成迁移因此把它放进bootstrap放在 ORM 初始化之后export async function bootstrap(port 3001, migrate true) { const db initORM(); if (migrate) { // 同步 schema await db.orm.migrator.up(); } // ... }必须条件化执行生产库走Migrator而测试库直接使用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); // -- 测试不执行迁移 return app; }十、Checkpoint 3阶段性成果至此你已完成4 个实体User、Article、Tag以及新增的Comment一个可运行的 Web 应用带GET /article分页接口一个基础测试用例通过app.inject()验证接口响应迁移与种子数据基础设施Migrator负责 schema 演进Seeder负责数据填充。注测试中使用的:memory:是 SQLite 提供的内存数据库特性通过特殊库名启用每个测试用例独立、互不干扰天然支持并行执行。关键经验回顾关注点方案依据请求级上下文隔离RequestContext FastifyonRequest钩子packages/core/src/utils/RequestContext.ts分页 计数em.findAndCount()内部并行findcountpackages/core/src/EntityManager.ts类型化 EM/仓储从驱动包导入EntityManager/EntityRepositorySqlEntityManager别名mikro-orm/sqlite导出测试隔离dbName: :memory: 独立端口 app.inject()test/utils.tsschema 快速同步orm.schema.create()/update()/drop()仅限原型/测试本章 CLI 示例生产 schema 演进Migrator扩展 migration:create/up/pending/listpackages/migrations/src/Migrator.ts数据填充SeedManager扩展 orm.seeder.seed()packages/seeder/src/SeedManager.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 服务、RequestContext、测试、Seeder 与迁移Chapter 3: Project SetupMikroORM 项目实战搭建指南Fastify 服务、RequestContext、测试、Seeder 与迁移Chapter 3: Project Set后端MikroORM 实战用 Fastify Vitest 搭建项目、测试接口并管理 Schema 与迁移MikroORM 实战用 Fastify Vitest 搭建项目、测试接口并管理 Schema 与迁移 本篇指南承接 MikroORM 的实体定义章节完后端MikroORM 项目搭建实战Fastify Vitest 下的请求上下文、Seeder 与迁移管理MikroORM 项目搭建实战Fastify Vitest 下的请求上下文、Seeder 与迁移管理 本篇技术指南基于 MikroORM 官方入门指南第三后端上一篇Calibre NoTrans 插件中文路径传到设备不再变拼音下一篇Kubernetes SIG-Apps 2016 年 6 月会议纪要解读Stacksmith 容器镜像持续交付、SIG 应用生态调研与 PetSet 有状态工作负载演进创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表