☰
LoopBack 4 关联模型实战:基于 @loopback/example-todo-list 构建 TodoList 关系 API
2026/10/15 15:27:47 网站建设 项目流程
  • 后端
  • 微服务

【免费下载链接】loopback-next

LoopBack makes it easy to build modern API applications that require complex integrations.

项目地址:https://gitcode.com/gh_mirrors/lo/loopback-next
点击查看免费下载

本篇文章以 LoopBack 4(loopback-next)仓库中的@loopback/example-todo-list示例为核心,完整讲解如何在已有Todo模型之上,通过hasMany/belongsTo/hasOne三种关系(Relation)构建相互关联的 REST API(TodoList ↔ Todo ↔ TodoListImage)。读完本文,你将掌握用lb4CLI 生成关联模型、配置关系仓库(含 inclusion resolver)、编写关系控制器,以及通过include过滤器一次性获取关联数据的完整实战流程。

示例背景:什么是 @loopback/example-todo-list

@loopback/example-todo-list是官方loopback-next仓库中的扩展教程示例,它构建在@loopback/example-todo之上,演示的是真实业务中非常常见的场景:数据集合之间互相关联。一个待办事项(Todo)常常归属于某个清单(TodoList),而一个清单又可以包含多个待办事项——对应关系型数据库中的一对多(1:N)与多对一(N:1)关系。

该示例最终构建出一个包含三个模型、五类 REST 控制器的完整应用:

模型关系说明
TodoListhasManyTodo、hasOneTodoListImage清单,一个清单包含多个待办、一张封面图
TodobelongsToTodoList待办事项,归属于某个清单
TodoListImagebelongsToTodoList清单图片(可选扩展部分),归属于某个清单

示例的完整最终代码位于 examples/todo-list,其中模型定义在 src/models,仓库在 src/repositories,控制器在 src/controllers。本教程对应的官方分步讲解文档位于 docs/site/tutorials/todo-list(模型、仓库、关系、控制器、hasOne 五个步骤)。

环境准备与获取项目

前置条件

  • Node.js:官方 README 中要求 v10 或更高版本;以当前仓库为准,@loopback/example-todo-list的 package.json 中engines字段声明为"node": "22 || 24 || 26",建议使用 Node.js 22 及以上版本运行本示例。
  • LoopBack 4 CLI 工具:全局安装@loopback/cli,本教程中所有lb4命令都由它提供:
npm i -g @loopback/cli

方式一:从 todo 教程继续(推荐)

本教程假设你已熟悉 LoopBack 4 基础(可先阅读 todo 教程)。如果不想从零开始,可以直接用 CLI 拉取@loopback/example-todo,把项目恢复到本教程的起点:

lb4 example todo

注意:本教程不依赖todo 教程的可选地理编码步骤,无论你是否完成该步骤,本教程的内容与步骤都不变。

方式二:直接获取最终成品

如果想直接查看本教程的最终结果,使用lb4 example选择并克隆 todo-list 示例:

lb4 example todo-list cd loopback4-example-todo-list

无论采用哪种方式,最终目录结构都是标准的 LoopBack 4 应用布局:src/application.ts(应用入口,由BootMixin、RepositoryMixin、RestApplication组合而成)、src/sequence.ts(请求处理序列)以及 src/datasources/db.datasource.ts(内存数据源)。

第一步:创建 TodoList 模型

一个待办事项往往会被归入某个清单(checklist),用来衡量更大目标的进度。真实业务中一个实体常常要能通过关系访问另一个实体的数据,因此我们在TodoListApplication中引入TodoList模型,让它与已有Todo模型建立关联。

TodoList模型包含以下属性:

  • 唯一id
  • 标题title
  • 用于区分清单的颜色color

使用lb4 model命令按提示生成模型:

$ lb4 model ? Model class name: TodoList ? Please select the model base class Entity (A persisted model with an ID) ? Allow additional (free-form) properties? No Model TodoList will be created in src/models/todo-list.model.ts Let's add a property to TodoList Enter an empty property name when done ? Enter the property name: id ? Property type: number ? Is id the ID property? Yes ? Is id generated automatically? No ? Is it required?: No ? Default value [leave blank for none]: Let's add another property to TodoList Enter an empty property name when done ? Enter the property name: title ? Property type: string ? Is it required?: Yes ? Default value [leave blank for none]: Let's add another property to TodoList Enter an empty property name when done ? Enter the property name: color ? Property type: string ? Is it required?: No ? Default value [leave blank for none]: Let's add another property to TodoList Enter an empty property name when done ? Enter the property name: create src/models/todo-list.model.ts update src/models/index.ts Model TodoList was created in src/models/

生成后的完整模型文件见 examples/todo-list/src/models/todo-list.model.ts。CLI 为每个属性生成对应的@property()装饰器配置:id标记为id: true且generated: false(由应用自行分配主键),title标记为required: true(必填),color为可选项。

第二步:创建 TodoList 仓库

关联模型仓库(Repository)的一个核心特性是:能够暴露一个工厂函数(factory function),返回"受约束"版本的关联模型仓库——即其所有操作都被限定在某个数据集合范围内。这正是后面hasMany关系得以工作的基础。

在项目目录下运行lb4 repository命令,为TodoList模型选择db数据源创建仓库:

lb4 repository ? Please select the datasource DbDatasource ? Select the model(s) you want to generate a repository TodoList ? Please select the repository base class DefaultCrudRepository (Juggler bridge) create src/repositories/todo-list.repository.ts update src/repositories/index.ts Repository TodoListRepository was created in src/repositories/

自定义方法(Custom Methods)

仓库类中可以添加自定义方法。例如,若想在仓库层按title精确查找某个TodoList,可以加入以下方法(见 todo-list.repository.ts):

export class TodoListRepository extends DefaultCrudRepository< TodoList, typeof TodoList.prototype.id, TodoListRelations > { // 其他代码…… public findByTitle(title: string) { return this.findOne({where: {title}}); } }

从源码结构看,findByTitle复用DefaultCrudRepository提供的findOne能力,只是预先绑定了一个where: {title}过滤条件,这是给仓库扩展业务查询方法的典型模式。

第三步:定义模型关系(hasMany 与 belongsTo)

使用 lb4 relation 命令建立双向关系

接下来用lb4 relation命令为模型添加关系,表示TodoList拥有多个(hasMany)Todo:

$ lb4 relation ? Please select the relation type hasMany ? Please select source model TodoList ? Please select target model Todo ? Foreign key name to define on the target model todoListId ? Source property name for the relation getter (will be the relation name) todos ? Allow TodoList queries to include data from related Todo instances? Yes create src/controllers/todo-list-todo.controller.ts Relation HasMany was created in src/

再为Todo添加反向关系:Todo属于(belongsTo)TodoList:

$ lb4 relation ? Please select the relation type belongsTo ? Please select source model Todo ? Please select target model TodoList ? Foreign key name to define on the source model todoListId ? Relation name todoList ? Allow Todo queries to include data from related TodoList instances? Yes create src/controllers/todo-todo-list.controller.ts Relation BelongsTo was created in src/

本例中外键名与源属性名均使用了默认值(todoListId)。如需自定义,请参考 HasMany-relation.md 中的 Relation Metadata 章节及其他关系文档。

更新样本数据

建立关系后,需要更新 examples/todo-list/data/db.json 中的样本数据来反映关系。先添加两条TodoList样本:

{ "ids": { "Todo": 5, "TodoList": 3 }, "models": { "Todo": { "1": "{\"title\":\"Take over the galaxy\",\"desc\":\"MWAHAHAHAHAHAHAHAHAHAHAHAHAMWAHAHAHAHAHAHAHAHAHAHAHAHA\",\"id\":1}", "2": "{\"title\":\"destroy alderaan\",\"desc\":\"Make sure there are no survivors left!\",\"id\":2}", "3": "{\"title\":\"play space invaders\",\"desc\":\"Become the very best!\",\"id\":3}", "4": "{\"title\":\"crush rebel scum\",\"desc\":\"Every.Last.One.\",\"id\":4}" }, "TodoList": { "1": "{\"title\":\"Sith lord's check list\",\"color\":\"blue\",\"id\":1}", "2": "{\"title\":\"My daily chores\",\"color\":\"red\",\"id\":2}" } } }

然后为各Todo添加todoListId属性,指向其所属清单的id:

{ "ids": { "Todo": 5, "TodoList": 3 }, "models": { "Todo": { "1": "{\"title\":\"Take over the galaxy\",\"desc\":\"MWAHAHAHAHAHAHAHAHAHAHAHAHAMWAHAHAHAHAHAHAHAHAHAHAHAHA\",\"todoListId\":1,\"id\":1}", "2": "{\"title\":\"destroy alderaan\",\"desc\":\"Make sure there are no survivors left!\",\"todoListId\":1,\"id\":2}", "3": "{\"title\":\"play space invaders\",\"desc\":\"Become the very best!\",\"todoListId\":2,\"id\":3}", "4": "{\"title\":\"crush rebel scum\",\"desc\":\"Every.Last.One.\",\"todoListId\":1,\"id\":4}" }, "TodoList": { "1": "{\"title\":\"Sith lord's check list\",\"color\":\"blue\",\"id\":1}", "2": "{\"title\":\"My daily chores\",\"color\":\"red\",\"id\":2}" } } }

注意:该文件由内存数据源(file: './data/db.json')持久化使用,具体配置见 src/datasources/db.datasource.ts 中的config对象。

幕后原理:关系装饰器与 Relations 接口

lb4 relation命令在TodoList模型上添加了@hasMany()装饰器及todos属性——正如装饰器名称所暗示的,它告诉 LoopBack 4:一个清单可以包含多个待办事项(见 todo-list.model.ts):

export class TodoList extends Entity { // ……CLI 定义的属性…… @hasMany(() => Todo) todos: Todo[]; // ……构造函数…… }

belongsTo方向同理(见 todo.model.ts):

export class Todo extends Entity { // ……Todo 模型的属性…… @belongsTo(() => TodoList) todoListId: number; // ……构造函数…… }

同时,模型文件中还会出现TodoRelations接口与TodoWithRelations类型(见 todo.model.ts):

export interface TodoRelations { // 在这里描述导航属性(navigational properties) todoList?: TodoListWithRelations; } export type TodoWithRelations = Todo & TodoRelations;

在TodoRelations接口中,我们把todoList描述为导航属性;同样,todo-list.model.ts 中的TodoListRelations也声明了todos?: TodoWithRelations[]导航属性。

幕后原理:inclusion resolver 与关联数据包含

前面运行lb4 relation时对提示 "Allow Todo queries to include data from related TodoList instances? (Y/n)" 选择了默认的Yes,这一步会为相关关系注册inclusionResolver(包含解析器)。若你的模型支持遍历(traversable)且需要 inclusion 能力,务必选Yes。

以获取每个TodoList关联的Todo为例,TodoListRepository中注册了由HasManyRepositoryFactory自带的 inclusion resolver(见 todo-list.repository.ts):

this.todos = this.createHasManyRepositoryFactoryFor( 'todos', todoRepositoryGetter, ); // 这一行为此关系启用 inclusion this.registerInclusionResolver('todos', this.todos.inclusionResolver);

于是当你查询一个TodoList时,响应会包含todos属性,其中携带关联的Todo列表,例如:

{ "id": 2, "title": "My daily chores", "todos": [ { "id": 3, "title": "play space invaders", "desc": "Become the very best!", "todoListId": 2 } ] }

反向的BelongsToAccessor同样自带 inclusion resolver,注册在TodoRepository上(见 todo.repository.ts):

this.todoList = this.createBelongsToAccessorFor( 'todoList', todoListRepositoryGetter, ); // 这一行为此关系启用 inclusion this.registerInclusionResolver('todoList', this.todoList.inclusionResolver);

第四步:编写控制器

处理关联模型请求的业务逻辑与处理独立模型并无本质区别。我们创建 controllers 来响应 todo-list 与其下 todo 的请求。

创建 TodoList 的 REST CRUD 控制器

运行lb4 controller命令,创建TodoList路由的 RESTful CRUD 控制器:

$ lb4 controller ? Controller class name: TodoList Controller TodoList will be created in src/controllers/todo-list.controller.ts ? What kind of controller would you like to generate? REST Controller with CRUD functions ? What is the name of the model to use with this CRUD repository? TodoList ? What is the name of your CRUD repository? TodoListRepository ? What is the name of ID property? id ? What is the type of your ID? number ? Is the id omitted when creating a new instance? Yes ? What is the base HTTP path name of the CRUD operations? /todo-lists create src/controllers/todo-list.controller.ts update src/controllers/index.ts Controller TodoList was created in src/controllers/

这样就得到了一套开箱即用的 todo-lists 基础 API。完整实现见 examples/todo-list/src/controllers/todo-list.controller.ts,它通过@repository(TodoListRepository)注入仓库,并用@post/@get/@patch/@put/@del装饰器绑定到/todo-lists路由。

启用关联数据的 OpenAPI Schema

为了在响应中携带关联的Todo,需要更新控制器中的schema。先从@loopback/rest导入getModelSchemaRef,然后在responses的content中为列表与单条查询开启includeRelations: true:

@get('/todo-lists', { responses: { '200': { description: 'Array of TodoList model instances', content: { 'application/json': { schema: { type: 'array', items: getModelSchemaRef(TodoList, {includeRelations: true}), }, }, }, }, }, }) async find(/*……*/) {/*……*/} @get('/todo-lists/{id}', { responses: { '200': { description: 'TodoList model instance', content: { 'application/json': { schema: getModelSchemaRef(TodoList, {includeRelations: true}), }, }, }, }, }) async findById(/*……*/) {/*……*/}

TodoController中的列表与单条查询也做同样处理(includeRelations: true),完整代码见 examples/todo-list/src/controllers/todo.controller.ts 中的find/findById方法。

关系控制器(Relation Controllers)

之前运行lb4 relation时,你可能注意到生成了src/controllers/todo-todo-list.controller.ts与src/controllers/todo-list-todo.controller.ts两个文件,它们包含面向关系的 API。关系控制器与普通控制器行为类似,区别在于它操作的是关系属性。例如在 todo-list-todo.controller.ts 中,可以通过/todo-lists/{id}/todos端点访问某清单下的待办事项。

由于src/controllers/todo-todo-list.controller.ts只包含一个方法,可以把它合并到TodoController中并删除原文件(见 todo.controller.ts):

export class TodoController { constructor() {} // …… // 其他控制器方法 @get('/todos/{id}/todo-list', { responses: { '200': { description: 'TodoList belonging to Todo', content: { 'application/json': { schema: {type: 'array', items: getModelSchemaRef(TodoList)}, }, }, }, }, }) async getTodoList( @param.path.number('id') id: typeof Todo.prototype.id, ): Promise<TodoList> { return this.todoRepository.todoList(id); } }

注意这里直接调用了this.todoRepository.todoList(id)——正是第一步中createBelongsToAccessorFor创建的BelongsToAccessor,可见控制器、仓库、关系工厂在调用链上是环环相扣的。

可选扩展:hasOne 关系(TodoListImage)

我们已经知道TodobelongsToTodoList、TodoListhasManyTodo。LoopBack 4 还支持第三种关系hasOne。我们引入TodoListImage,使每个TodoList拥有一个(hasOne)图片;相应地TodoListImage也belongsToTodoList。

创建模型与仓库

与创建TodoList相同,用lb4 model生成TodoListImage(属性为id与必填的value,value用字符串存图片内容;源码注释说明这是因为 Buffer 类型尚不被支持,相关讨论见 todo-list-image.model.ts):

lb4 model ? Model class name: TodoListImage ? Please select the model base class Entity (A persisted model with an ID) ? Allow additional (free-form) properties? No Model TodoListImage will be created in src/models/todo-list-image.model.ts ? Enter the property name: id ? Property type: number ? Is id the ID property? Yes ? Is id generated automatically? No ? Is it required?: No ? Enter the property name: value ? Property type: string ? Is it required?: Yes create src/models/todo-list-image.model.ts update src/models/index.ts

再用lb4 repository创建对应仓库,选择DbDatasource与TodoListImage模型,基类为DefaultCrudRepository (Juggler bridge)。

添加 hasOne 关系

使用lb4 relation添加hasOne关系,与 hasMany 的流程非常相似:

$ lb4 relation ? Please select the relation type hasOne ? Please select source model TodoList ? Please select target model TodoListImage ? Foreign key name to define on the target model todoListId ? Source property name for the relation getter (will be the relation name) image ? Allow TodoList queries to include data from related TodoListImage instances? Yes create src/controllers/todo-list-todo-list-image.controller.ts Relation HasOne was created in src/

再为TodoListImage添加反向belongsTo关系(生成todo-list-image-todo-list.controller.ts)。同样的,src/controllers/todo-list-image-todo-list.controller.ts只有一个方法,可合并进TodoListImageController(见 todo-list-image.controller.ts)后删除原文件。

检查生成的关系代码

TodoList模型中新增了@hasOne装饰的image属性(见 todo-list.model.ts):

@model() export class TodoList extends Entity { // ……其他属性…… @hasOne(() => TodoListImage) image: TodoListImage; // …… } export interface TodoListRelations { todos?: TodoWithRelations[]; // 新增的导航属性 image?: TodoListImageWithRelations; }

TodoListImage模型中则添加了@belongsTo装饰的外键todoListId(见 todo-list-image.model.ts):

@model() export class TodoListImage extends Entity { // ……其他属性…… @belongsTo(() => TodoList) todoListId: number; // …… } export interface TodoListImageRelations { todoList?: TodoListWithRelations; }

提示一:模型 A 对模型 B 存在hasOne关系,并不强制要求模型 B 对模型 A 存在belongsTo关系,两者可以独立使用。

提示二:关于如何自定义@hasOne/@belongsTo装饰器,可参考 HasOne-relation.md 与 BelongsTo-relation.md 中的 Relation Metadata 章节。

对应生成的关系控制器 todo-list-todo-list-image.controller.ts 提供了GET/POST/PATCH/DELETE /todo-lists/{id}/todo-list-image四个端点,内部通过this.todoListRepository.image(id)(HasOneRepositoryFactory)完成受限操作。

运行并验证 API

控制器完成后应用即可重新启动。@loopback/boot会在启动时自动装配所有控制器、仓库与数据源,无需额外配置(见 src/application.ts 中的bootOptions)。

$ npm start Server is running at http://127.0.0.1:3000

使用 API Explorer(默认挂在/explorer,见 application.ts)时,请先清空默认的filter或where对象,以便看到全部数据。

下面是可以尝试的新请求:

  • POST /todo-lists,请求体为{ "title": "grocery list" }。
  • POST /todo-lists/{id}/todos,{id}为上一步返回的 ID,请求体为{ "title": "get eggs", "isComplete": false}。注意响应体中的todoListId属性会自动带上之前的清单 ID——这是HasManyRepositoryFactory自动约束外键的结果。
  • GET /todo-lists/{id}/todos,使用之前的 ID,应能取到刚创建的 todo。
  • GET /todo-lists/{id},使用之前的 ID,并携带过滤器{"include": [{"relation": "todos"}]},应能看到包含todos属性的响应。也可以用下面的 URL 测试该端点(记得把{id}替换为之前的 ID):
http://localhost:3000/todo-lists/{id}?filter[include][][relation]=todos

至此,你就拥有了为关联模型定义 API 的完整能力。

测试验证与工程规范

示例内置了完善的测试套件,可通过npm test运行(package.json 中test脚本为lb-mocha "dist/__tests__/**/*.js")。测试按层级组织在 src/tests下:

  • acceptance(验收测试):todo-list-todo.acceptance.ts 验证"为清单创建 todo 时自动写入todoListId"、"按清单查询其 todos"等端到端行为;todo-list-image.acceptance.ts 验证 hasOne 关系的创建与查询。
  • integration(集成测试):todo-list.repository.integration.ts、todo.repository.integration.ts、todo-list-image.repository.integration.ts验证仓库层 CRUD 与关系工厂。
  • unit(单元测试):controllers/下针对各控制器的纯逻辑测试。

例如 todo-list-todo.acceptance.ts 中的用例清晰地展示了"约束仓库"的核心行为:向POST /todo-lists/{id}/todos提交不含todoListId的 todo,响应中会自动补齐该清单的 ID 并持久化。

小结与延伸阅读

至此,从Todo单模型出发,你已完整走通了 LoopBack 4 关联模型的四层构建链路:模型(@hasMany/@belongsTo/@hasOne装饰器)→ 仓库(HasManyRepositoryFactory/BelongsToAccessor+ inclusion resolver)→ 控制器(关系端点 +includeRelationsSchema)→ 运行时验证(include过滤器)。

如需进一步深入,可在仓库中继续阅读:

  • 关系完整参考:HasMany-relation.md、BelongsTo-relation.md、HasOne-relation.md 与 Relations.md
  • 关系生成器使用说明:Relation-generator.md
  • 本教程完整实现:examples/todo-list(模型、仓库、控制器、数据源、测试齐全)
  • 后端
  • 微服务

【免费下载链接】loopback-next

LoopBack makes it easy to build modern API applications that require complex integrations.

项目地址:https://gitcode.com/gh_mirrors/lo/loopback-next
点击查看免费下载

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

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

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

立即咨询