三合一(代码优先):NestJs & GraphQl & Mongoose
问题
解决方案
执行
结论
在Twitter上关注我,很高兴接受您的建议和改进。
我在完成一个个人项目后决定写这篇博文。在开始这个项目时,我思考了如何建立一个可靠的架构,使用NestJs和Mongoose来创建GraphQL API 。
为什么选择 GraphQL?
- 不再过度获取或获取不足;
- 前端产品快速迭代;
- 后端的深入分析;
- 模式和类型系统的好处;
。
为什么选择 NestJs?
- Nest 在这些常见的 Node.js 框架(Express/Fastify)之上提供了更高的抽象级别,并将它们的 API 直接暴露给开发者。这使得开发者可以自由地使用底层平台提供的大量第三方模块。
——菲利普·马宗
。
为什么选择 Mongoose?
- MongooseJS 在 MongoDB 之上提供了一个抽象层,从而无需使用命名集合。
- Mongoose 中的模型执行了大部分设置文档属性默认值和验证数据的工作。
- MongooseJS 中,函数可以附加到模型上。这样可以无缝地集成新功能。
- 查询使用函数链而不是嵌入式助记符,这使得代码更加灵活、可读性更强,因此也更易于维护。
——吉姆·梅德洛克
问题
当我开始构建项目架构时,总会遇到一个问题:数据模型的定义,以及应用程序的不同层如何使用它。就我而言,为应用程序的不同层定义数据模型让我有些恼火:
- 为 GraphQL 定义模式以实现 API 端点;
- 为 Mongoose 定义一个模式来组织数据库的文档;
- 定义一个数据模型以便应用程序映射对象;
解决方案
理想情况下,只需定义一次数据模型,即可用于生成 GraphQL 模式、MongoDB 集合模式以及 NestJs 提供程序使用的类。神奇的是,NestJs 及其插件让我们可以轻松做到这一点😙。
NestJs 插件
NestJS 插件将不同的技术封装在NestJS 模块中,以便于使用和集成到 NestJS 生态系统中。在本例中,我们将使用以下两个插件@nestjs/mongoose
:@nestjs/graphql
这两个插件允许我们以两种方式进行:
- schema-first:首先,定义 Mongoose 和 GraphQL 的模式,然后使用它来生成我们的 typescript 类。
- 代码优先:首先,定义我们的 typescript 类,然后使用它们生成我们的模式 Mongoose/GraphQL。
我使用了代码优先方法,因为它允许我实现单一模型(typescript 类)并使用它来生成 GraphQL 和 Mongoose 👌 的模式。
执行
这是GitHub 上的最终项目源代码。
好了,说了这么多。热热手指,来玩玩魔术吧🧙!
NestJS
首先,使用 创建我们的 NestJs 项目@nestjs/cli
。我们将其命名为three-in-one-project
:
$ npm i -g @nestjs/cli
$ nest new three-in-one-project
这将启动我们的 NestJs 项目:
这里我们感兴趣的是文件夹的内容
src/
:
main.ts
:NestJS 应用程序的入口点,我们在此引导它。app.module.ts
:NestJS 应用的根模块。它实现了controller
AppController
和provider
AppService
。
要为嵌套服务器运行:
$ npm start
为了更好地组织,我们将AppModule
文件放在专门的文件夹中,并更新的导入src/app/
路径:AppModule
main.ts
不要忘记更新导入
AppModule
路径main.ts
模型
我们将创建一个 API 来管理Peron
拥有列表的人员列表,Hobby
为此我们将在app/
文件夹中创建这两个模型:
// person.model.ts
export class Person {
_id: string;
name: string;
hobbies: Hobby[];
}
// hobby.model.ts
export class Hobby {
_id: string;
name: string;
}
猫鼬
安装依赖项
从这两个类Hobby
和中,Person
我们将生成相应的 mongoose 模式HobbySchema
和。为此,我们将安装带有一些其他依赖项的PersonSchema
包:@nestjs/mongoose
$ npm i @nestjs/mongoose mongoose
$ npm i --save-dev @types/mongoose
连接到 MongoDB
为了将我们的后端连接到 mongoDB 数据库,我们将AppModule
导入MongooseModule
:
// app.module.ts
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { MongooseModule } from '@nestjs/mongoose';
@Module({
// "mongodb://localhost:27017/three-in-one-db" is the connection string to the project db
imports: [
MongooseModule.forRoot('mongodb://localhost:27017/three-in-one-db'),
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
为了更好地组织,我们将创建两个子模块PersonModule
,HobbyModule
每个子模块管理相应的模型。我们将使用以下方法@nestjs/cli
快速完成此操作:
$ cd src\app\
$ nest generate module person
$ nest generate module hobby
创建的模块
PersonModule
和HobbyModule
会自动导入AppModule
。
person.model.ts
现在,将每个文件移动hobby.model.ts
到其相应的模块中:
模式生成
现在我们可以开始设置 mongoose 模式的生成。@nestjs/mongoose
为我们提供了一个装饰器来注释我们的 typescript 类,以指示如何生成 mongoose 模式。让我们为我们的类添加一些装饰器:
// person.model.ts
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { Document, Schema as MongooseSchema } from 'mongoose';
import { Hobby } from '../hobby/hobby.model';
@Schema()
export class Person {
_id: MongooseSchema.Types.ObjectId;
@Prop()
name: string;
@Prop()
hobbies: Hobby[];
}
export type PersonDocument = Person & Document;
export const PersonSchema = SchemaFactory.createForClass(Person);
// hobby.model.ts
import { Document, Schema as MongooseSchema } from 'mongoose';
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
@Schema()
export class Hobby {
_id: MongooseSchema.Types.ObjectId;
@Prop()
name: string;
}
export type HobbyDocument = Hobby & Document;
export const HobbySchema = SchemaFactory.createForClass(Hobby);
- 装饰器
@Prop()
在文档中定义一个属性。
- 装饰器
@Schema()
将类标记为模式定义 - Mongoose 文档(例如
PersonDocument
:)表示与 MongoDB 中存储的文档的一对一映射。 MongooseSchema.Types.ObjectId
是一种通常用于唯一标识符的猫鼬类型
最后,我们将模型导入到两个模块中的 MongooseModule 中:
// person.module.ts
import { Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose';
import { Person, PersonSchema } from './person.model';
@Module({
imports: [
MongooseModule.forFeature([{ name: Person.name, schema: PersonSchema }]),
],
})
export class PersonModule {}
// hobby.module.ts
import { Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose';
import { Hobby, HobbySchema } from './hobby.model';
@Module({
imports: [
MongooseModule.forFeature([{ name: Hobby.name, schema: HobbySchema }]),
],
})
export class HobbyModule {}
CRUD 操作
我们将为两个模块创建一个实现 CRUD 操作的层。为每个模块创建一个NestJS 服务来实现该操作。要创建这两个服务,请执行以下命令:
$ cd src\app\person\
$ nest generate service person --flat
$ cd ..\hobby\
$ nest generate service hobby --flat
我们习惯
--flat
不为该服务生成文件夹。
更新服务以添加 CRUD 方法及其输入类:
// person.service.ts
import { Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Model, Schema as MongooseSchema } from 'mongoose';
import { Person, PersonDocument } from './person.model';
import {
CreatePersonInput,
ListPersonInput,
UpdatePersonInput,
} from './person.inputs';
@Injectable()
export class PersonService {
constructor(
@InjectModel(Person.name) private personModel: Model<PersonDocument>,
) {}
create(payload: CreatePersonInput) {
const createdPerson = new this.personModel(payload);
return createdPerson.save();
}
getById(_id: MongooseSchema.Types.ObjectId) {
return this.personModel.findById(_id).exec();
}
list(filters: ListPersonInput) {
return this.personModel.find({ ...filters }).exec();
}
update(payload: UpdatePersonInput) {
return this.personModel
.findByIdAndUpdate(payload._id, payload, { new: true })
.exec();
}
delete(_id: MongooseSchema.Types.ObjectId) {
return this.personModel.findByIdAndDelete(_id).exec();
}
}
// person.inputs.ts
import { Schema as MongooseSchema } from 'mongoose';
import { Hobby } from '../hobby/hobby.model';
export class CreatePersonInput {
name: string;
hobbies: Hobby[];
}
export class ListPersonInput {
_id?: MongooseSchema.Types.ObjectId;
name?: string;
hobbies?: Hobby[];
}
export class UpdatePersonInput {
_id: MongooseSchema.Types.ObjectId;
name?: string;
hobbies?: Hobby[];
}
// hobby.service.ts
import { Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Model, Schema as MongooseSchema } from 'mongoose';
import { Hobby, HobbyDocument } from './hobby.model';
import {
CreateHobbyInput,
ListHobbyInput,
UpdateHobbyInput,
} from './hobby.inputs';
@Injectable()
export class HobbyService {
constructor(
@InjectModel(Hobby.name) private hobbyModel: Model<HobbyDocument>,
) {}
create(payload: CreateHobbyInput) {
const createdHobby = new this.hobbyModel(payload);
return createdHobby.save();
}
getById(_id: MongooseSchema.Types.ObjectId) {
return this.hobbyModel.findById(_id).exec();
}
list(filters: ListHobbyInput) {
return this.hobbyModel.find({ ...filters }).exec();
}
update(payload: UpdateHobbyInput) {
return this.hobbyModel
.findByIdAndUpdate(payload._id, payload, { new: true })
.exec();
}
delete(_id: MongooseSchema.Types.ObjectId) {
return this.hobbyModel.findByIdAndDelete(_id).exec();
}
}
// hobby.inputs.ts
export class CreateHobbyInput {
name: string;
}
export class ListHobbyInput {
_id?: MongooseSchema.Types.ObjectId;
name?: string;
}
export class UpdateHobbyInput {
_id: MongooseSchema.Types.ObjectId;
name?: string;
}
hobbies
注意,类的属性Hobby
是一个对象引用数组Hobby
。因此,我们来做一些调整:
// person.model.ts
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { Document, Schema as MongooseSchema } from 'mongoose';
import { Hobby } from '../hobby/hobby.model';
@Schema()
export class Person {
_id: MongooseSchema.Types.ObjectId;
@Prop()
name: string;
@Prop({ type: [MongooseSchema.Types.ObjectId], ref: Hobby.name })
hobbies: MongooseSchema.Types.ObjectId[];
}
export type PersonDocument = Person & Document;
export const PersonSchema = SchemaFactory.createForClass(Person);
// person.inputs.ts
import { Hobby } from '../hobby/hobby.model';
import { Schema as MongooseSchema } from 'mongoose';
export class CreatePersonInput {
name: string;
hobbies: MongooseSchema.Types.ObjectId[];
}
export class ListPersonInput {
_id?: MongooseSchema.Types.ObjectId;
name?: string;
hobbies?: MongooseSchema.Types.ObjectId[];
}
export class UpdatePersonInput {
_id: MongooseSchema.Types.ObjectId;
name?: string;
hobbies?: MongooseSchema.Types.ObjectId[];
}
GraphQL
我们快完成了,我们只需要实现 graphQL 层。
依赖项安装
$ npm i @nestjs/graphql graphql-tools graphql apollo-server-express
模式生成
至于 mongoose,我们将使用decorators
from@nestjs/graphql
来注释我们的 typescript 类,以指示如何生成 graphQL 模式:
// person.model.ts
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { Field, ObjectType } from '@nestjs/graphql';
import { Document, Schema as MongooseSchema } from 'mongoose';
import { Hobby } from '../hobby/hobby.model';
@ObjectType()
@Schema()
export class Person {
@Field(() => String)
_id: MongooseSchema.Types.ObjectId;
@Field(() => String)
@Prop()
name: string;
@Field(() => [String])
@Prop({ type: [MongooseSchema.Types.ObjectId], ref: Hobby.name })
hobbies: MongooseSchema.Types.ObjectId[];
}
export type PersonDocument = Person & Document;
export const PersonSchema = SchemaFactory.createForClass(Person);
// hobby.model.ts
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { Field, ObjectType } from '@nestjs/graphql';
import { Document, Schema as MongooseSchema } from 'mongoose';
@ObjectType()
@Schema()
export class Hobby {
@Field(() => String)
_id: MongooseSchema.Types.ObjectId;
@Field(() => String)
@Prop()
name: string;
}
export type HobbyDocument = Hobby & Document;
export const HobbySchema = SchemaFactory.createForClass(Hobby);
有关
@nestjs/graphql
装饰器的更多信息ObjectType
和Field
:官方文档
解析器
为了定义我们的 graphQL query
,mutation
我们resolvers
将创建一个NestJS resolver
。我们可以使用 NestJs CLI 来做到这一点:
$ cd src\app\person\
$ nest generate resolver person --flat
$ cd ..\hobby\
$ nest generate resolver hobby --flat
然后,更新生成的文件:
// person.resolver.ts
import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
import { Schema as MongooseSchema } from 'mongoose';
import { Person } from './person.model';
import { PersonService } from './person.service';
import {
CreatePersonInput,
ListPersonInput,
UpdatePersonInput,
} from './person.inputs';
@Resolver(() => Person)
export class PersonResolver {
constructor(private personService: PersonService) {}
@Query(() => Person)
async person(
@Args('_id', { type: () => String }) _id: MongooseSchema.Types.ObjectId,
) {
return this.personService.getById(_id);
}
@Query(() => [Person])
async persons(
@Args('filters', { nullable: true }) filters?: ListPersonInput,
) {
return this.personService.list(filters);
}
@Mutation(() => Person)
async createPerson(@Args('payload') payload: CreatePersonInput) {
return this.personService.create(payload);
}
@Mutation(() => Person)
async updatePerson(@Args('payload') payload: UpdatePersonInput) {
return this.personService.update(payload);
}
@Mutation(() => Person)
async deletePerson(
@Args('_id', { type: () => String }) _id: MongooseSchema.Types.ObjectId,
) {
return this.personService.delete(_id);
}
}
// hobby.resolver.ts
import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
import { Schema as MongooseSchema } from 'mongoose';
import { Hobby } from './hobby.model';
import { HobbyService } from './hobby.service';
import {
CreateHobbyInput,
ListHobbyInput,
UpdateHobbyInput,
} from './hobby.inputs';
@Resolver(() => Hobby)
export class HobbyResolver {
constructor(private hobbyService: HobbyService) {}
@Query(() => Hobby)
async hobby(
@Args('_id', { type: () => String }) _id: MongooseSchema.Types.ObjectId,
) {
return this.hobbyService.getById(_id);
}
@Query(() => [Hobby])
async hobbies(@Args('filters', { nullable: true }) filters?: ListHobbyInput) {
return this.hobbyService.list(filters);
}
@Mutation(() => Hobby)
async createHobby(@Args('payload') payload: CreateHobbyInput) {
return this.hobbyService.create(payload);
}
@Mutation(() => Hobby)
async updateHobby(@Args('payload') payload: UpdateHobbyInput) {
return this.hobbyService.update(payload);
}
@Mutation(() => Hobby)
async deleteHobby(
@Args('_id', { type: () => String }) _id: MongooseSchema.Types.ObjectId,
) {
return this.hobbyService.delete(_id);
}
}
有关
@nestjs/graphql
装饰器Mutation
、Resolver
和的更多信息Query
:官方文档
让我们向输入类添加一些装饰器,以便 GraphQl 识别它们:
// person.inputs.ts
import { Field, InputType } from '@nestjs/graphql';
import { Schema as MongooseSchema } from 'mongoose';
import { Hobby } from '../hobby/hobby.model';
@InputType()
export class CreatePersonInput {
@Field(() => String)
name: string;
@Field(() => [String])
hobbies: MongooseSchema.Types.ObjectId[];
}
@InputType()
export class ListPersonInput {
@Field(() => String, { nullable: true })
_id?: MongooseSchema.Types.ObjectId;
@Field(() => String, { nullable: true })
name?: string;
@Field(() => [String], { nullable: true })
hobbies?: MongooseSchema.Types.ObjectId[];
}
@InputType()
export class UpdatePersonInput {
@Field(() => String)
_id: MongooseSchema.Types.ObjectId;
@Field(() => String, { nullable: true })
name?: string;
@Field(() => [String], { nullable: true })
hobbies?: MongooseSchema.Types.ObjectId[];
}
// hobby.inputs.ts
import { Schema as MongooseSchema } from 'mongoose';
import { Field, InputType } from '@nestjs/graphql';
@InputType()
export class CreateHobbyInput {
@Field(() => String)
name: string;
}
@InputType()
export class ListHobbyInput {
@Field(() => String, { nullable: true })
_id?: MongooseSchema.Types.ObjectId;
@Field(() => String, { nullable: true })
name?: string;
}
@InputType()
export class UpdateHobbyInput {
@Field(() => String)
_id: MongooseSchema.Types.ObjectId;
@Field(() => String, { nullable: true })
name?: string;
}
导入 GraphQLModule
最后,GraphQLModule
导入AppModule
:
import { Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose';
import { GraphQLModule } from '@nestjs/graphql';
import { join } from 'path';
import { PersonModule } from './person/person.module';
import { HobbyModule } from './hobby/hobby.module';
import { AppController } from './app.controller';
import { AppService } from './app.service';
@Module({
imports: [
MongooseModule.forRoot('mongodb://localhost:27017/three-in-one-db'),
GraphQLModule.forRoot({
autoSchemaFile: join(process.cwd(), 'src/schema.gql'),
sortSchema: true,
playground: true,
debug: false,
}),
PersonModule,
HobbyModule,
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
autoSchemaFile
属性值是自动生成的模式的创建路径
sortSchema
生成的模式中的类型将按照它们在包含的模块中定义的顺序排列。要按字典顺序对模式进行排序,请将此属性更改为true
playground
激活graqh-playground
debug
打开/关闭调试模式
GraphQL 游乐场
Playground 是一个图形化、交互式的浏览器内置 GraphQL IDE,默认访问的 URL 与 GraphQL 服务器相同。要访问 Playground,你需要配置并运行一个基本的 GraphQL 服务器。——
NestJs文档
我们已经在上一步中激活了 playground,一旦服务器启动(yarn start
),我们可以通过以下 URL 访问它:http://localhost:3000/graphql:
填充并解析字段
Mongoose 有一个强大的方法称为populate()
,它可以让您引用其他集合中的文档。
填充是指自动将文档中指定的路径替换为其他集合中的文档的过程。我们可以填充单个文档、多个文档、一个普通对象、多个普通对象或查询返回的所有对象。让我们看一些示例。——
Mongoose文档
我们可以使用 Mongoosepopulate
来解析hobbies
以下字段Person
:
// person.resolver.ts
import {
Args,
Mutation,
Parent,
Query,
ResolveField,
Resolver,
} from '@nestjs/graphql';
import { Schema as MongooseSchema } from 'mongoose';
import { Person, PersonDocument } from './person.model';
import { PersonService } from './person.service';
import {
CreatePersonInput,
ListPersonInput,
UpdatePersonInput,
} from './person.inputs';
import { Hobby } from '../hobby/hobby.model';
@Resolver(() => Person)
export class PersonResolver {
constructor(private personService: PersonService) {}
@Query(() => Person)
async person(
@Args('_id', { type: () => String }) _id: MongooseSchema.Types.ObjectId,
) {
return this.personService.getById(_id);
}
@Query(() => [Person])
async persons(
@Args('filters', { nullable: true }) filters?: ListPersonInput,
) {
return this.personService.list(filters);
}
@Mutation(() => Person)
async createPerson(@Args('payload') payload: CreatePersonInput) {
return this.personService.create(payload);
}
@Mutation(() => Person)
async updatePerson(@Args('payload') payload: UpdatePersonInput) {
return this.personService.update(payload);
}
@Mutation(() => Person)
async deletePerson(
@Args('_id', { type: () => String }) _id: MongooseSchema.Types.ObjectId,
) {
return this.personService.delete(_id);
}
@ResolveField()
async hobbies(
@Parent() person: PersonDocument,
@Args('populate') populate: boolean,
) {
if (populate)
await person
.populate({ path: 'hobbies', model: Hobby.name })
.execPopulate();
return person.hobbies;
}
}
// person.model.ts
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { Field, ObjectType } from '@nestjs/graphql';
import { Document, Schema as MongooseSchema } from 'mongoose';
import { Hobby } from '../hobby/hobby.model';
@ObjectType()
@Schema()
export class Person {
@Field(() => String)
_id: MongooseSchema.Types.ObjectId;
@Field(() => String)
@Prop()
name: string;
@Field(() => [Hobby])
@Prop({ type: [MongooseSchema.Types.ObjectId], ref: Hobby.name })
hobbies: MongooseSchema.Types.ObjectId[] | Hobby[];
}
export type PersonDocument = Person & Document;
export const PersonSchema = SchemaFactory.createForClass(Person);
这使我们能够Person
以这种方式发出请求:
结论
作为开发者,我们会将不同的技术模块组合起来,为我们的项目构建一个可行的生态系统🌍。在 JavaScript 生态系统中,其优势(或劣势)在于模块的丰富性和组合的可能性。我希望能够继续撰写这篇博文的续篇,解释如何在 Monorepo 开发环境中集成 GraphQL API,从而在多个应用程序和库之间共享此数据模型。
鏂囩珷鏉ユ簮锛�https://dev.to/lotfi/third-in-one-code-first-nestjs-graphql-mongoose-30ie