☰
MikroORM 7 生产环境部署全指南:元数据缓存、预编译函数与 Webpack/esbuild 打包
2026/9/28 7:32:51 网站建设 项目流程
  • 后端

【免费下载链接】mikro-orm

TypeScript 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 的实体发现(discovery)机制依赖 TypeScript 源码来推断属性类型,这直接影响了生产环境的部署方式。本文以 deployment.md 为骨架,系统讲解 MikroORM 7.2 在只部署编译产物、无 TS 源码、乃至无eval/new Function的受限运行时(如 Cloudflare Workers)下的六种部署方案,并结合 packages/cli/src/commands/GenerateCacheCommand.ts、packages/cli/src/commands/CompileCommand.ts、packages/core/src/cache/GeneratedCacheAdapter.ts 等源码,给出可复制的配置与打包实践。

为什么部署需要专门处理?

MikroORM 底层使用ts-morph读取所有实体的 TypeScript 源文件,以检测每个属性的类型。正因为"只写类型即可完成运行时校验",实体发现过程天然依赖.ts源文件的存在。

这给部署带来一个直接后果:当你只想部署编译后的 JS 产物、完全不携带 TS 源文件时,实体的发现过程很可能会失败。官方文档提供了多条出路,按"是否需要源码、是否运行在受限运行时"可以分成三类:

  • 彻底移除 TS 源码依赖:生成预构建元数据缓存(cache:generate --combined)、预编译运行函数(compile)、手动填充type/entity属性;
  • 简单粗暴保留源码:把 TS 源文件与编译产物一起部署;
  • 打包为单文件:用 Webpack 或 esbuild 将实体与依赖打成一个 bundle。

下面逐一展开。

方案一:部署预构建元数据缓存(GeneratedCacheAdapter)

从 v6 开始,MikroORM 支持通过 CLI 将生产环境所需的元数据缓存打包成单个 JSON 文件:

npx mikro-orm cache:generate --combined

该命令会在当前目录生成./temp/metadata.json,然后在生产配置中配合GeneratedCacheAdapter使用:

import { GeneratedCacheAdapter, MikroORM } from '@mikro-orm/core'; await MikroORM.init({ metadataCache: { enabled: true, adapter: GeneratedCacheAdapter, options: { data: require('./temp/metadata.json') }, }, // ... });

这样你就可以把@mikro-orm/reflection只保留为开发依赖,构建期用 CLI 生成缓存包,生产构建只依赖这一份 JSON。

自定义缓存输出路径

--combined接受一个可选的路径参数,该路径相对当前目录下的temp文件夹解析。例如:

npx mikro-orm cache:generate --combined="../cache/mikro-orm-metadata.json"

会把文件保存到./cache/mikro-orm-metadata.json。

提示:缓存 bundle 支持静态import引入,在使用打包器(bundler)的场景下非常方便。

源码视角:cache:generate 命令做了什么

查看 GenerateCacheCommand.ts,可以看到该命令的两个关键选项:

  • --ts(布尔):为.ts文件生成开发用缓存;
  • --combined/-c(字符串,别名-c):生成生产用单一 JSON 文件,与GeneratedCacheAdapter配套使用。

当--combined传入空字符串时会回退到默认路径./metadata.json(源码见 GenerateCacheCommand.ts)。命令内部会临时启用FileCacheAdapter并执行一次完整的MetadataDiscovery,再把结果写入缓存文件,最终日志会明确提示是Combined还是普通 JS/TS 缓存、输出到哪个路径。

GeneratedCacheAdapter的实现非常轻量(见 GeneratedCacheAdapter.ts):构造函数把预生成数据转成一个内存Map,get()时会把查询键上的.ts/.js后缀剥掉再命中,set()则直接写内存。也就是说,一旦缓存 bundle 生成,运行期完全不需要再读取任何 TS 文件,也不需要反射提供方参与。

metadataCache 配置项的完整参数

从 Configuration.ts 的类型定义可以看到metadataCache支持的完整选项:

参数类型说明
enabledboolean是否启用元数据缓存,默认取决于元数据提供方的useCache()方法
combinedboolean \| string是否将所有元数据合并进单个缓存文件;true用默认路径,也可给自定义路径字符串
prettyboolean是否美化(pretty print)JSON 缓存文件,默认false
adapterSyncCacheAdapter构造器缓存适配器类;启用缓存但未显式指定时,使用异步MikroORM.init()会自动装配FileCacheAdapter
optionsDictionary传给适配器构造器的选项,默认{ cacheDir: process.cwd() + '/temp' }

生产环境使用GeneratedCacheAdapter时,只需把enabled: true、adapter指向适配器类、options.data指向生成的 JSON 即可。

方案二:预编译实体运行函数(compile)

有些运行时(如 Cloudflare Workers 和各种 edge 运行时)明确禁止new Function/eval。而 MikroORM 在运行期默认使用new Function来 JIT 编译针对每个实体优化的函数(用于 hydration 与实体比较)。解决办法是在构建期把这些函数预生成出来:

npx mikro-orm compile

默认情况下,生成的文件位于你的 ORM 配置文件旁边。可以用--out自定义输出路径:

npx mikro-orm compile --out ./dist/compiled-functions.js

然后在配置中传入生成的函数:

import compiledFunctions from './compiled-functions.js'; export default defineConfig({ compiledFunctions, });

与 GeneratedCacheAdapter 组合:彻底告别 ts-morph 和 new Function

要获得"既无ts-morph、也无new Function"的完整生产部署,把两个方案叠加即可:

import { GeneratedCacheAdapter, defineConfig } from '@mikro-orm/core'; import compiledFunctions from './compiled-functions.js'; import metadata from './temp/metadata.json'; export default defineConfig({ compiledFunctions, metadataCache: { enabled: true, adapter: GeneratedCacheAdapter, options: { data: metadata }, }, });

重要:只要实体定义或驱动配置发生变化,就必须重新生成 compiled functions 文件,否则运行期会使用过期函数。

源码视角:compile 命令如何"捕获"运行函数

CompileCommand.ts 的实现思路很巧妙:它先执行一次完整的MetadataDiscovery,然后通过替换Utils.createFunction全局钩子,把原本要在运行期 JIT 生成的代码字符串逐条捕获下来(见 CompileCommand.ts):

  • 对每个实体元数据依次触发ObjectHydrator的实体 hydrator(full/reference 两种模式)与EntityComparator的比较器、快照生成器、结果映射器、主键 getter/serializer 等;
  • 每条捕获结果以'key': function(params) { ... }的形式写入输出文件;
  • 输出文件同时生成对应的.d.ts类型声明,且按环境输出 ESM(export default)或 CJS(module.exports)两种格式(见 CompileCommand.ts);
  • 若未指定--out,默认路径取 ORM 配置文件所在目录下的compiled-functions.js。

版本一致性校验

compiledFunctions生成文件里带有__version字段。在 Configuration.ts 中,ORM 初始化时会比对__version与当前Utils.getORMVersion(),如果不一致会打印警告,提示"编译函数是用 MikroORM vX 生成的,当前版本是 vY,请用npx mikro-orm compile重新生成"。这从源码层面印证了文档强调的"实体定义或驱动配置变化时必须重新生成"。

方案三:手动填充 type 或 entity 属性

实体发现过程的本质是"嗅探 TS 类型并把值保存为字符串,供后续运行时校验使用"。你可以完全跳过这一过程,手动提供这些值:

@Entity() export class Book { @PrimaryKey({ type: 'number' }) id!: number; @Property({ type: 'string' }) title!: string; @Enum(() => BookStatus) status?: BookStatus; @ManyToOne(() => Author) // or `@ManyToOne({ entity: () => Author })` author1!: Author; // or @ManyToOne({ entity: () => Author }) author2!: Author; } export enum BookStatus { SOLD_OUT = 'sold', ACTIVE = 'active', UPCOMING = 'upcoming', }

需要注意:

  • 标量属性用type明确指定类型字符串(如'number'、'string');
  • 枚举与关系用() => Xxx的 entity 引用形式,@ManyToOne(() => Author)等价于@ManyToOne({ entity: () => Author });
  • 数值枚举(numeric enum)无需手动标注,因为其值是运行时可直接得到的数字。

这种方式尤其适合配合下面的打包方案——因为打包器无法静态分析动态的目录扫描,需要所有实体信息都"硬编码"在代码里。

方案四:直接部署实体源文件(最简单)

大多数场景下,多部署几个文件无关紧要。因此最省事的做法是:把 TS 源文件原样放到编译产物旁边,和开发环境一样部署。这样实体发现照常工作,无需任何额外配置。缺点是产物里会多出源文件,且部署包中仍依赖ts-morph/@mikro-orm/reflection。

方案五:用 Webpack 打包实体与依赖

Webpack 可以把每个实体及其依赖打进单个文件,该文件包含所有必需的模块且没有外部依赖。

打包前的项目改造

Webpack 要求所有必需文件都被"硬编码"在代码里。下面这种动态导入不会生效(Webpack 不知道要把哪个文件打进 bundle,会直接报错):

let dependencyNameInVariable = 'dependency'; const dependency = import(dependencyNameInVariable);

同时需要注意三点:

  1. 必须在MikroORM.init()的entities选项中显式提供实体列表——基于文件夹/文件的发现不被支持(可用下方"动态加载依赖"作为替代方案);
  2. 需要按方案三在所有地方填充type或entity属性;
  3. 禁用元数据缓存(会略微降低启动速度)。

提示:使用ReflectMetadataProvider时缓存默认就是关闭的。

方式 A:手动列出实体
import { Author, Book, BookTag, Publisher, Test } from '../entities.js'; await MikroORM.init({ entities: [Author, Book, BookTag, Publisher, Test], // ... });
方式 B:动态加载依赖

利用 Webpack 的**动态导入(dynamic imports)**特性,只要路径的一部分是已知的,就能把依赖打进来。下面的例子使用require.context——它只在 Webpack 构建期可用,因此同时提供了一个"当环境变量WEBPACK未设置时(例如开发期用tsx或swc运行)也能工作"的替代实现。

这里会从目录../entities导入所有扩展名为.ts的文件:

await MikroORM.init({ // ... entities: await getEntities(), // ... }); async function getEntities(): Promise<any[]> { if (process.env.WEBPACK) { const modules = require.context('../entities', true, /\.ts$/); return modules .keys() .map(r => modules(r)) .flatMap(mod => Object.keys(mod).map(className => mod[className])); } const promises = fs.readdirSync('../entities').map(file => import(`../entities/${file}`)); const modules = await Promise.all(promises); return modules.flatMap(mod => Object.keys(mod).map(className => mod[className])); }

process.env.WEBPACK由下面的EnvironmentPlugin({ WEBPACK: true })注入,从而保证打包期走require.context分支、开发期走原生动态import分支。

Webpack 配置

Webpack 可以不带配置文件运行,但要为 MikroORM 与 Node.js 目标构建 bundle,需要专门的配置。配置文件通常放在项目根目录,名为webpack.config.js。下面是一份针对 MikroORM 的完整推荐配置:

const path = require('path'); const { EnvironmentPlugin, IgnorePlugin } = require('webpack'); const TerserPlugin = require('terser-webpack-plugin'); // Mark our dev dependencies as externals so they don't get included in the webpack bundle. const { devDependencies } = require('./package.json'); const externals = {}; for (const devDependency of Object.keys(devDependencies)) { externals[devDependency] = `commonjs ${devDependency}`; } // And anything MikroORM's packaging can be ignored if it's not on disk. // Later we check these dynamically and tell webpack to ignore the ones we don't have. const optionalModules = new Set([ ...Object.keys(require('@mikro-orm/core/package.json').peerDependencies), ...Object.keys(require('@mikro-orm/core/package.json').devDependencies || {}) ]); module.exports = { entry: path.resolve('app', 'server.ts'), // You can toggle development mode on to better see what's going on in the webpack bundle, // but for anything that is getting deployed, you should use 'production'. // mode: 'development', mode: 'production', optimization: { minimizer: [ new TerserPlugin({ terserOptions: { // We want to minify the bundle, but don't want Terser to change the names of our entity // classes. This can be controlled in a more granular way if needed, (see // https://terser.org/docs/api-reference.html#mangle-options) but the safest default // config is that we simply disable mangling altogether but allow minification to proceed. mangle: false, // Similarly, Terser's compression may at its own discretion change function and class names. // While it only rarely does so, it's safest to also disable changing their names here. // This can be controlled in a more granular way if needed (see // https://terser.org/docs/api-reference.html#compress-options). compress: { keep_classnames: true, keep_fnames: true, }, } }) ] }, target: 'node', module: { rules: [ // Bring in our typescript files. { test: /\.ts$/, exclude: /node_modules/, loader: 'ts-loader', }, // Native modules can be bundled as well. { test: /\.node$/, use: 'node-loader', }, // Some of MikroORM's dependencies use mjs files, so let's set them up here. { test: /\.mjs$/, include: /node_modules/, type: 'javascript/auto', }, ], }, // These are computed above. externals, resolve: { extensions: ['.ts', '.js'] }, plugins: [ // Ignore any of our optional modules if they aren't installed. This ignores database drivers // that we aren't using for example. new EnvironmentPlugin({ WEBPACK: true }), new IgnorePlugin({ checkResource: resource => { const baseResource = resource.split('/', resource[0] === '@' ? 2 : 1).join('/'); if (optionalModules.has(baseResource)) { try { require.resolve(resource); return false; } catch { return true; } } return false; }, }), ], output: { filename: 'server.js', libraryTarget: 'commonjs', path: path.resolve(__dirname, '..', 'output'), }, };

这份配置的几个关键点:

  • externals:把所有devDependencies标记为外部依赖,避免它们被打进 bundle;
  • optionalModules:收集@mikro-orm/core的peerDependencies与devDependencies(覆盖各种数据库驱动),用IgnorePlugin动态检查——模块磁盘上存在就保留,不存在就忽略,从而剔除未使用的数据库驱动;
  • Terser 压缩:mangle: false并保留 class/function 名称,防止压缩器改动实体类名导致 ORM 按名称解析实体时失效;
  • loader 规则:.ts用ts-loader,.node原生模块用node-loader,.mjs按javascript/auto处理;
  • EnvironmentPlugin:注入WEBPACK=true,供上文动态加载实体的分支判断使用。

运行 Webpack

在项目根目录执行webpack(若未全局安装则用npx webpack)。构建过程大概率会抛出一些警告,其中与 MikroORM 相关的报错可以忽略:只要 bundle 正确生成,那些报错指向的代码片段在实际运行时根本不会执行。

方案六:用 esbuild 打包实体与依赖

esbuild同样可以把 MikroORM 的实体与依赖打成单个文件。但由于 esbuild 的打包机制,要让 MikroORM 正常工作,必须妥善处理下面这个关键问题。

排除不必要的依赖(external)

默认情况下,esbuild 会把 MikroORM 的所有包全部打进 bundle,包括全部数据库方言(及其数据库驱动依赖)。这通常不是我们想要的:bundle 会非常大,而绝大多数应用只与一种数据库平台交互。解决方法是把无关依赖通过 esbuild 的external配置排除掉。例如使用postgresql平台时:

external: [ '@mikro-orm/migrations', '@mikro-orm/entity-generator', '@mikro-orm/mariadb', '@mikro-orm/mongodb', '@mikro-orm/mysql', '@mikro-orm/mssql', '@mikro-orm/oracledb', '@mikro-orm/pglite', '@mikro-orm/seeder', '@mikro-orm/sqlite', '@mikro-orm/libsql', '@mikro-orm/sql-js', 'better-sqlite3', 'mysql', 'mysql2', 'oracledb', 'pg-native', 'pg-query-stream', 'sql.js', 'tedious', ]

上面这份列表覆盖了 MikroORM 各可选方言包与底层驱动:@mikro-orm/sqlite/@mikro-orm/libsql/@mikro-orm/sql-js对应 SQLite 与 libSQL 生态,better-sqlite3/sql.js是它们的驱动;mysql/mysql2是 MySQL 与 MariaDB 驱动,oracledb、pg-native、pg-query-stream、tedious分别对应 Oracle、PostgreSQL 原生流式查询与 MSSQL。若你的应用恰好用到其中某一项,则应从 external 列表中移除,让它正常打进 bundle 或作为运行时依赖保留。

如何选择:六种方案适用场景速查

方案是否需要 TS 源码是否支持禁 eval 运行时部署形态适用场景
预构建缓存(cache:generate --combined+GeneratedCacheAdapter)不需要支持JSON 缓存文件 + 编译产物常规 Node.js 生产部署,想移除@mikro-orm/reflection
预编译函数(compile+compiledFunctions)不需要支持compiled-functions.js+ 缓存Cloudflare Workers、edge 运行时
手动填充type/entity不需要视组合方案纯编译产物配合打包器使用,或实体数量少、想彻底去掉发现过程
部署实体源文件需要不支持(依赖 ts-morph)源文件 + 编译产物快速上线、对部署体积不敏感
Webpack 打包不需要(需硬编码实体列表)不支持(默认仍用new Function)单文件server.js希望输出单一可执行文件、无外部依赖
esbuild 打包不需要不支持(默认仍用new Function)单文件 bundle追求构建速度与最小 bundle,可配合 external 剔除方言

需要特别指出:方案五、六(Webpack/esbuild 打包)解决的是"单文件部署形态",若同时要跑在禁eval的运行时上,应把方案二(compile预编译函数)与方案一(GeneratedCacheAdapter)叠加进去,三者在配置上完全兼容——这正是 deployment.md 给出的终极组合:compiledFunctions+metadataCache: { adapter: GeneratedCacheAdapter }。

仓库中tests/features/compiled-functions/、tests/features/reflection/等测试目录以及 tests/Webpack.test.ts、tests/entities-webpack/ 下的示例实体,可以帮你进一步验证上述打包与缓存流程在真实项目中的行为。

  • 后端

【免费下载链接】mikro-orm

TypeScript 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
点击查看免费下载
上一篇:开源突破!WebRL-GLM-4-9B让AI网页代理成功率提升7倍,首次超越GPT-4
下一篇:3分钟掌握光学仿真:OpticsPy让Python变身光学实验室

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询