资讯动态

LoopBack 4 运行时动态创建 Model、Repository 与 Controller 完整指南:以 docs 动态建模文档为主线并结合源码实现

发布时间:2026/10/14 16:05:31 来源:尧图企业网站定制
后端微服务【免费下载链接】loopback-nextLoopBack makes it easy to build modern API applications that require complex integrations.项目地址https://gitcode.com/gh_mirrors/lo/loopback-next点击查看免费下载在典型的 LoopBack 应用中Model、Repository 和 Controller 通常以静态文件形式存在于项目目录中但插件化、多租户或基于数据库 schema 自动生成 API 等场景要求在应用启动后的运行阶段动态注册这三者。本文基于官方文档《Dynamically adding models, repositories, and controllers during runtime》完整讲解ModelDefinition→defineModelClass()→ 动态数据源 →defineCrudRepositoryClass()/defineRepositoryClass()→defineCrudRestController()的完整调用链并结合 define-model-class.ts、define-repository-class.ts、crud-rest.controller.ts 与 repository.mixin.ts 的源码实现说明每一步背后的类生成机制与依赖注入配置细节读完即可在自己的RepositoryMixin应用中运行时挂载一整套可用的 CRUD REST 端点。为什么需要运行时动态创建LoopBack 默认支持在src/下编写静态的 Model 类、Repository 类与 Controller 类。而官方文档指出的典型场景是应用已经启动需要在此基础上“追加”全新的 Model、Repository 和 Controller而不改动代码文件、重新打包。框架为此提供了三类运行时构建能力能力提供的 API所在包动态定义 Model 类ModelDefinition、defineModelClass()loopback/repository动态定义 Repository 类defineCrudRepositoryClass()、defineRepositoryClass()、defineKeyValueRepositoryClass()loopback/repositorydefineCrudRepositoryClass亦从loopback/rest-crud重导出动态定义 CRUD REST ControllerdefineCrudRestController()loopback/rest-crud这些符号分别从 packages/repository/src/index.ts 与 packages/rest-crud/src/index.ts 导出。需要注意的是后文中用到的app.dataSource()与app.repository()方法只在应用使用了RepositoryMixin时才存在即应用类形如class MyApplication extends RepositoryMixin(RestApplication) {}第一步定义 ModelDefinition 对象ModelDefinition是创建动态 Model 的第一步它是对 LoopBack 模型各属性的一种抽象描述。其构造函数可以接收一个模型名字符串或一个ModelDefinitionSyntax对象。从 model.ts 的源码看ModelDefinitionSyntax的核心字段为export interface ModelDefinitionSyntax { name: string; properties?: {[name: string]: PropertyDefinition | PropertyType}; settings?: ModelSettings; relations?: RelationDefinitionMap; jsonSchema?: JsonSchemaWithExtensions; [attribute: string]: any; }构造函数内部会遍历properties对每个属性调用addProperty(name, definitionOrType)注册若未提供settings和relations会分别初始化为空对象/空 Map。文档给出的示例创建一个名为book的模型包含id与title两个属性并在settings中指定了idInjection: false和 MySQL 的库表信息const bookDef new ModelDefinition({ name: book, properties: { id: { type: Number, required: true, length: null, precision: 10, scale: 0, id: 1, mysql: { columnName: id, dataType: int, dataLength: null, dataPrecision: 10, dataScale: 0, nullable: N, }, }, title: { type: String, required: false, length: 512, precision: null, scale: null, mysql: { columnName: title, dataType: varchar, dataLength: 512, dataPrecision: null, dataScale: null, nullable: Y, }, }, }, settings: { idInjection: false, mysql: {schema: test, table: BookStore}, }, });源码层面还有两点值得注意addProperty()对属性定义做了规整——如果传入的定义对象没有type字段会将其整体当作类型处理等价于{type: definitionOrType}因此addProperty(title, {type: string})这类链式写法同样可用当某属性同时声明id: true且generated: true时若显式指定了type而未设置useDefaultIdType构造函数会强制将useDefaultIdType置为false即尊重你显式给出的 ID 类型。第二步用 defineModelClass() 创建 Model 类拿到ModelDefinition后把它传给loopback/repository的辅助函数defineModelClass()。该函数期望两个参数要继承的基类通常是Model或Entity本例为Entity和模型定义对象const BookModel defineModelClasstypeof Entity, {id: number; title?: string}( Entity, bookDef, );如果希望以应用内已有的 Model 作为基类以继承其方法与静态属性只需把该 Model 类作为第一个参数传入import {DynamicModelCtor} from loopback/repository; // 假设 User 是应用中已存在的 Model 类 const StudentModel defineModelClass typeof User, {id: number; university?: string} (User, studentDef);从 define-model-class.ts 的实现可以清楚看到动态类是如何产生的export function defineModelClass BaseCtor extends typeof Model, Props extends object {}, ( base: BaseCtor /* Model or Entity */, definition: ModelDefinition, ): DynamicModelCtorBaseCtor, Props { const modelName definition.name; const defineNamedModelClass new Function( base.name, return class ${modelName} extends ${base.name} {}, ); const modelClass defineNamedModelClass(base) as DynamicModelCtor BaseCtor, Props ; assert.equal(modelClass.name, modelName); // Apply model(definition) to the generated class model(definition)(modelClass); return modelClass; }可以推断出其设计要点函数通过new Function拼接类声明字符串动态生成一个类名等于definition.name的新类并继承传入基类随后用assert.equal(modelClass.name, modelName)校验类名确实正确生成类之后立即对其应用model(definition)装饰器把定义写入装饰器元数据——单元测试 define-model-class.unit.ts 正是用ModelMetadataHelper.getModelMetadata(Book)验证了元数据与传入定义深度相等返回值类型DynamicModelCtorBaseCtor, PropsL82-L90保证了类型安全构造函数接受DataObjectProps同时继承基类的全部静态字段与原型成员。测试用例L54-L82验证了动态Product类可以访问Product.modelName、Product.definition、Product.getIdProperties()并能正常new Product({...})、调用instance.getId()/instance.toJSON()若模型允许自由属性free-form在第二个泛型参数中并入AnyObject即可例如defineModelClasstypeof Entity, AnyObject {id: number}(Entity, definition)测试中AnyObject场景下可以无类型报错地访问instance.name。第三步动态定义并挂载数据源在为模型建立 Repository 之前必须先准备好数据源。如果需要可以运行时创建juggler.DataSource实例参数为数据源名称、connector 与连接串const dsName bookstore-ds; const bookDs new juggler.DataSource({ name: dsName, connector: require(loopback-connector-mongodb), url: mongodb://sysop:moonlocalhost, }); await bookDs.connect(); app.dataSource(bookDs, dsName);文档强调创建后要调用.connect()建立连接并通过app.dataSource()挂载到应用上且该方法仅存在于RepositoryMixin应用。结合 repository.mixin.ts 中dataSource()的实现可以进一步确认两条细节当传入的是juggler.DataSource实例时方法直接执行this.bind(datasources.name).to(dataSource)并打上RepositoryTags.DATASOURCE标签绑定键形如datasources.bookstore-ds不传第二个参数时默认取dataSource.name当传入的是数据源类时则走createBindingFromClass并以SINGLETON作用域注册绑定后的数据源即可在构造函数中注入Mixin 的 JSDoc 示例给出了对应写法constructor(inject(datasources.db) dataSource: DataSourceType) {}此外RepositoryMixin提供的migrateSchema()L262 起会按RepositoryTags.DATASOURCE标签查找所有已注册数据源逐一执行 autoupdate/automigrate因此运行时挂载的数据源同样会被纳入迁移流程——前提是 repository 先被实例化使模型注册并挂接到数据源上单元测试 repository.mixin.unit.ts 验证了app.dataSource()app.repository()之后automigrate收到的模型列表为[Product]。第四步创建 Repository数据源就绪后为BookModel创建 Repository。loopback/rest-crud包导出的辅助函数defineCrudRepositoryClass()会为给定模型创建一个基于DefaultCrudRepository的 Repository 类该函数实际定义在 define-repository-class.ts并由 rest-crud 入口 重新导出以保持向后兼容const BookRepository defineCrudRepositoryClass Book, typeof Book.prototype.id, BookRelations (BookModel);若想使用非DefaultCrudRepository的基类或自定义 Repository改用defineRepositoryClass()并传入第二个参数作为基类。源码注释特别提示TypeScript 无法从构造函数正确推断泛型M和R两个泛型必须显式提供L73-L75。文档给出了两种自定义方式方式一使用自定义的 Repository 基类先编写包含自定义实现的基类再把它作为基类传入class MyRepoBase E extends Entity, IdType, Relations extends object, extends DefaultCrudRepositoryE, IdType, Relations { // 自定义实现 } const BookRepositoryClass defineRepositoryClass typeof BookModel, MyRepoBaseBookModel, typeof BookModel.prototype.id, BookRelations (BookModel, MyRepoBase);这里对基类有明确约束BaseRepositoryClassM, R要求构造函数签名为new (modelClass: M, dataSource: juggler.DataSource)L53-L64。方式二使用 Repository Mixin按“Repository Mixin 类工厂函数”的模式编写自定义 mixin如FindByTitleRepositoryMixin将 mixin 应用到DefaultCrudRepository上把组合结果作为基类const BookRepositoryClass defineRepositoryClass typeof BookModel, DefaultCrudRepository BookModel, typeof BookModel.prototype.id, BookRelations FindByTitleBookModel (BookModel, FindByTitleRepositoryMixin(DefaultCrudRepository));defineRepositoryClass()的内部实现L97-L118同样采用new Function动态拼类生成的类名为模型类名 Repository例如BookRepository构造函数中把ModelCtor与注入的dataSource一起传给基类const repoName modelClass.name Repository; const defineNamedRepo new Function( ModelCtor, BaseRepository, return class ${repoName} extends BaseRepository { constructor(dataSource) { super(ModelCtor, dataSource); } };, );也就是说动态 Repository 类的构造函数只需要数据源一个参数模型类在类定义时已被固化进闭包——这正是ModelRepositoryClassM, RL25-L35接口new (dataSource: juggler.DataSource): R的由来。由于类是在运行时生成的装饰器无法预先写在类上因此必须手动配置依赖注入为构造函数第 0 个参数指定数据源绑定键再把 Repository 类注册到应用inject(datasources.${dsName})(BookRepository, undefined, 0); const repoBinding app.repository(BookRepository);原文示例写作inject(datasources.${dsName.name})其中dsName是字符串变量正确的绑定键应为datasources.${dsName}即上一步app.dataSource(bookDs, dsName)产生的datasources.bookstore-ds。app.repository()同样仅存在于RepositoryMixin应用从 repository.mixin.ts 的实现看它会在repositories命名空间下以TRANSIENT作用域创建类绑定并打上repository标签随后即可通过repositories.BookRepository键或app.getRepository(BookRepository)获取实例。第五步创建并挂载 ControllerRepository 就绪后用loopback/rest-crud的defineCrudRestController()为模型创建控制器。它接受一个 Model 类和一个CrudRestControllerOptions对象basePath必填、readonly可选之后同样需要手动应用inject装饰器配置依赖注入const basePath / bookDef.name; const BookController defineCrudRestController(BookModel, {basePath}); inject(repoBinding.key)(BookController, undefined, 0); app.controller(BookController);其中repoBinding.key就是上一步app.repository()返回的绑定键如repositories.BookRepository它被注入到控制器构造函数的第 0 个参数。app.controller()之后该模型的 CRUD REST 端点即全部可用。从 crud-rest.controller.ts 的源码可以确认动态控制器暴露的完整端点集合options.basePath为挂载前缀方法路径对应仓库操作GET/{basePath}repository.find(filter)GET/{basePath}/{id}repository.findById(id, filter)GET/{basePath}/countrepository.count(where)POST/{basePath}repository.create(data)PATCH/{basePath}repository.updateAll(data, where)PATCH/{basePath}/{id}repository.updateById(id, data)PUT/{basePath}/{id}repository.replaceById(id, data)DELETE/{basePath}/{id}repository.deleteById(id)源码中的几处实现细节值得注意工厂函数内部先定义只读控制器ReadonlyRestControllerImplfind / findById / count再定义CrudRestControllerImpl extends ReadonlyRestControllerImpl追加写操作当options.readonly为 true 时返回只读版本L281-L290最终返回的类通过new Function命名为模型名 Controller本例为BookController文件开头注释解释了这样设计的原因TypeScript 不允许在类表达式中使用装饰器因此工厂必须“定义内部类 返回构造函数”L37-L57POST /的请求体会自动exclude掉模型的 ID 属性L210-L222路径参数{id}的 JSON Schema 则从模型 schema 中的首个 ID 属性推导getIdSchema()L293-L301这些端点行为由验收测试 default-model-crud-rest.acceptance.ts 覆盖例如POST /products返回新实例且自动分配数字id携带id的请求会被拒绝。如果默认的 CRUD 语义不满足需求文档建议复制defineCrudRestController的实现即 crud-rest.controller.ts并按需修改。模型发现从现有数据库 Schema 反推定义许多数据源 connector 提供了从既有数据库 schema 中发现模型定义的接口在动态建模时尤为有用。官方文档给出的完整 API 列表为// 列出数据库表和/或视图 const modelDefs await ds.discoverModelDefinitions({views: true, limit: 20}); // 列出指定表/视图的列 const modelProps await ds.discoverModelProperties(PRODUCT); const modelProps await ds.discoverModelProperties(INVENTORY_VIEW, { owner: STRONGLOOP, }); // 列出指定表的主键 const primaryKeys await ds.discoverPrimaryKeys(INVENTORY); // 列出指定表的外键 const foreignKeys await ds.discoverForeignKeys(INVENTORY); // 列出引用了指定表主键的外键 const exportedForeignKeys ds.discoverExportedForeignKeys(PRODUCT); // 通过发现指定表来创建模型定义 const schema await ds.discoverSchema(table, {owner: STRONGLOOP});这些接口由loopback-datasource-juggler的数据源定义是否可用取决于所选 connector 是否实现了 discover 能力。在 LoopBack 仓库内可以直接看到同一套 API 的实际使用者——CLI 的模型发现工具 model-discoverer.js它先等待ds.connected再调用ds.discoverModelDefinitions(options)获取候选表清单随后对选定的表调用ds.discoverSchema(modelName, options)取回 schema并将schema.options归一到schema.settings。这套“发现 → 生成定义”的流程与本文的动态建模路线正好互补用 discover 系列 API 拿到列与主键信息组装成ModelDefinition即可完全自动化地完成前述五步。小结动态建模的完整调用链把上述步骤串起来LoopBack 4 的运行时动态注册链路为ModelDefinition属性/设置/关系描述 → defineModelClass(Entity, def) // 动态生成 Model 类并写入 model 元数据 → new juggler.DataSource(...) connect() // 动态数据源 → app.dataSource(ds, name) // 绑定到 datasources.name → defineCrudRepositoryClass / defineRepositoryClass(model) → inject(datasources.name)(Repo, undefined, 0) → app.repository(Repo) // 绑定到 repositories.Repo 类名 → defineCrudRestController(model, {basePath}) → inject(repoBinding.key)(Controller, undefined, 0) → app.controller(Controller) // CRUD REST 端点上线适用前提与限制需要牢记应用必须基于RepositoryMixin才能使用app.dataSource()与app.repository()defineRepositoryClass的两个泛型必须显式声明运行时动态生成的类无法预先应用装饰器所有依赖注入都需要通过inject(bindingKey)(target, targetProperty, index)手动补齐而ModelDefinition属性中的 connector 专属配置如示例中的mysql段只对相应 connector 生效。掌握这条链路后无论是插件按需注册资源还是基于discoverSchema从既有数据库自动暴露 REST API都可以在不修改静态代码的前提下完成。赞分享后端微服务【免费下载链接】loopback-nextLoopBack makes it easy to build modern API applications that require complex integrations.项目地址https://gitcode.com/gh_mirrors/lo/loopback-next点击查看免费下载相关推荐如何快速掌握Vue.js Modal动态模态框运行时创建组件的终极指南如何快速掌握Vue.js Modal动态模态框运行时创建组件的终极指南 Vue.js Modal是一款简单易用、高度可定制的Vue.js模态框库能帮助开发者Truffle终极指南一键生成项目依赖关系PDF文档Truffle终极指南一键生成项目依赖关系PDF文档 想要可视化你的Truffle项目依赖关系吗 本文将为你展示如何利用Truffle内置工具快速生成专区块链开发工具Web3LoopBack 4 与 MongoDB 集成验收测试指南loopback/test-repository-mongodb 的运行、配置与源码解析LoopBack 4 与 MongoDB 集成验收测试指南 loopback/test repository mongodb 的运行、配置与源码解析 acc后端微服务创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑