通常写後端都会使用到资料库,透过资料库来储存使用者相关的资料,而资料库有非常多种,本篇将会介绍最热门的 NoSQL 资料库 - MongoDB 如何在 Nest 中进行互动,如果不清楚 MongoDB 或是想要在云端建立免费的 MongoDB 服务,可以参考我去年写的文章。那就废话不多说,赶快开始吧!
node.js 与 MongoDB 沟通最有名的函式库即 mongoose,它是一个采用 schema-based
的 ODM 套件。Nest 对 mongoose
进行了包装,制作了一个 MongooseModule
让习惯用 mongoose
的开发人员可以无痛使用,实在是太贴心啦!
安装方式一样是透过 npm
,不过这里需要特别注意除了安装 Nest 制作的模组外,还需要安装 mongoose
本身:
$ npm install @nestjs/mongoose mongoose
在安装完相关套件後,就可以来实作连线资料库的部分,其方法十分简单,在 AppModule
下汇入 MongooseModule
并使用 forRoot
方法来进行连线,它的效果等同於 mongoose
中的 connect
方法。这里以 app.module.ts
为例,定义一个常数 MONGO
来放置连线相关资讯,并在 MongooseModule.forRoot
中调用 getUrl
方法:
注意:通常不会把敏感资讯写在程序码里面,会将其抽离至环境变数中。
import { Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose';
import { AppController } from './app.controller';
import { AppService } from './app.service';
const MONGO = {
username: '<Example>',
password: encodeURIComponent('<YOUR_PASSWORD>'),
getUrl: function () {
return `mongodb+srv://${this.username}:${this.password}@<YOUR_DB>`
}
};
@Module({
imports: [
MongooseModule.forRoot(MONGO.getUrl())
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
MongooseModule
提供了 forRootAsync
方法,透过这个方法可以把依赖项目注入进来,使 MongooseModule
在建立时可以使用依赖项目来赋予值。运用这个特性将 ConfigModule
引入,并注入 ConfigService
进而取出我们要的环境变数来配置 MongoDB 的来源。
我们先在专案目录下新增 .env
档,并将 MongoDB 的相关配置写进来:
MONGO_USERNAME=YOUR_USERNAME
MONGO_PASSWORD=YOUR_PASSWORD
MONGO_RESOURCE=YOUR_RESOURCE
接着,运用前面学到的命名空间技巧,将 MongoDB 相关的环境变数归类在 mongo
底下,并针对环境变数进行处理,我们在 src/config
资料夹下新增 mongo.config.ts
来实作该工厂函式:
import { registerAs } from '@nestjs/config';
export default registerAs('mongo', () => {
const username = process.env.MONGO_USERNAME;
const password = encodeURIComponent(process.env.MONGO_PASSWORD);
const resource = process.env.MONGO_RESOURCE;
const uri = `mongodb+srv://${username}:${password}@${resource}?retryWrites=true&w=majority`;
return { username, password, resource, uri };
});
修改 app.module.ts
,配置 ConfigModule
以及 MongooseModule
,在 forRootAsync
方法中带入依赖,并用 useFactory
返回从 ConfigService
取到的值:
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { MongooseModule } from '@nestjs/mongoose';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import MongoConfigFactory from './config/mongo.config';
@Module({
imports: [
ConfigModule.forRoot({
load: [MongoConfigFactory]
}),
MongooseModule.forRootAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: async (config: ConfigService) => ({
uri: config.get<string>('mongo.uri')
})
})
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
如此一来,就成功用环境变数去设定 MongooseModule
的配置啦!
在开始存取资料库之前,还是来讲解一下 mongoose
的基本概念,它主要是由两大元素构成:schema
、model
。
MongoDB 最基本的元素为 document
,也就是一笔资料,而很多个 document
所形成的集合为 collection
,其概念如下:
为什麽要特别说 MongoDB 的基本概念呢?因为 schema
与这概念息息相关,每一个 schema
都对应到一个 collection
,它会制定该 collection
下所有 document
的栏位与栏位规则,是最基础的元素。
透过 schema
制定了资料结构,但无法直接透过它来存取资料库,因为它只是制定了规则,真正执行存取的元素为 model
,所有的 model
都是基於 schema
产生的,透过 model
便可以操作该 schema
所控管的 collection
,并且所有建立、修改、查询都会根据 schema
制定的栏位来操作。
在 Nest 要设计 schema
有两种方式,一种是采用 mongoose
原生的做法,另一种则是用 Nest 设计的装饰器,这里会以 Nest 装饰器为主,想了解原生作法的话可以参考我去年的文章。透过 Nest 装饰器设计的 schema
主要是由 @Schema
与 @Prop
所构成:
@Schema
装饰器会将一个 class
定义为 schema
的格式,并且可以接受一个参数,该参数对应到 mongoose
的 schema
选项配置,详细内容可以参考官方文件。
这里我们先简单设计一个名为 Todo
的 class
,并使用 @Schema
装饰器,在 src/common/models
资料夹下建立一个名为 todo.model.ts
的档案:
import { Schema } from '@nestjs/mongoose';
@Schema()
export class Todo {}
@Prop
装饰器定义了 document
的栏位,其使用在 class
中的属性,它拥有基本的型别推断功能,让开发人员在面对简单的型别可以不需特别做指定,但如果是阵列或巢状物件等复杂的型别,则需要在 @Prop
带入参数来指定其型别,而带入的参数其实就是 mongoose
的 SchemaType
,详细内容可以参考官方文件。
这里我们就来修改一下 todo.model.ts
的内容,实作一次 @Prop
的使用方式,总共配置了 title
、description
以及 completed
这三个栏位,其中,title
与 completed
为必填,而 title
最多接受 20
个字,description
最多接受 200
字:
import { Prop, Schema } from '@nestjs/mongoose';
@Schema()
export class Todo {
@Prop({ required: true, maxlength: 20 })
title: string;
@Prop({ maxlength: 200 })
description: string;
@Prop({ required: true })
completed: boolean;
}
我们说 schema
是定义 document
的资料结构,而 model
是基於 schema
所产生出来的,这里我们就可以简单推断出 model
的操作会返回的东西为 document
,mongoose
也很贴心的提供了基本的 Document
型别,但因为 document
会根据 schema
定义的资料结构而有所不同,故我们需要设计一个 type
来让之後 model
可以顺利拿到 schema
定义的栏位。
修改 todo.model.ts
的内容,定义了 TodoDocument
的型别:
import { Prop, Schema } from '@nestjs/mongoose';
import { Document } from 'mongoose';
export type TodoDocument = Todo & Document;
@Schema()
export class Todo {
@Prop({ required: true, maxlength: 20 })
title: string;
@Prop({ maxlength: 200 })
description: string;
@Prop({ required: true })
completed: boolean;
}
如果碰上某个栏位是巢状物件的型别该怎麽处理呢?这时候可以使用一个非常实用的函式来达成该目的,其名为 raw
,它可以让开发人员在不额外建立 class
的情况下,用 mongoose
原生的写法来定义该栏位的型别与规则。
上面的叙述可能有点难理解,我们实作一次会比较清楚,这边在 src/common/models
底下建立一个 user.model.ts
的档案并设计其 schema
,我们定义一个名为 name
的栏位,并在该栏位配置 firstName
、lastName
以及 fullName
的栏位,形成一个巢状物件的型别:
import { Prop, raw, Schema, SchemaFactory } from '@nestjs/mongoose';
import { Document } from 'mongoose';
export type UserDocument = User & Document;
@Schema()
export class User {
@Prop(
raw({
firstName: { type: String },
lastName: { type: String },
fullName: { type: String }
})
)
name: Record<string, any>;
@Prop({ required: true })
email: string;
}
mongoose
可以将多个相关 collection
的资料产生关联,透过 populate
的方法让别的 collection
资料能够被找到且带入。
修改一下 todo.model.ts
的内容,添加一个 owner
的栏位,并透过 mongoose
的 ObjectId
与 User
产生关联:
import { Prop, Schema } from '@nestjs/mongoose';
import { Document, Types } from 'mongoose';
import { User } from './user.model';
export type TodoDocument = Todo & Document;
@Schema()
export class Todo {
@Prop({ required: true, maxlength: 20 })
title: string;
@Prop({ maxlength: 200 })
description: string;
@Prop({ required: true })
completed: boolean;
@Prop({ type: Types.ObjectId, ref: 'User' })
owner: User;
}
在设计好 schema
之後,就要将 schema
透过 SchemaFactory
的 createForClass
方法产生出这个 schema
的实体,这里以 todo.model.ts
与 user.model.ts
为例:
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { Document, Types } from 'mongoose';
import { User } from './user.model';
export type TodoDocument = Todo & Document;
@Schema()
export class Todo {
@Prop({ required: true, maxlength: 20 })
title: string;
@Prop({ maxlength: 200 })
description: string;
@Prop({ required: true })
completed: boolean;
@Prop({ type: Types.ObjectId, ref: 'User' })
owner: User;
}
export const TodoSchema = SchemaFactory.createForClass(Todo);
import { Prop, raw, Schema, SchemaFactory } from '@nestjs/mongoose';
import { Document } from 'mongoose';
export type UserDocument = User & Document;
@Schema()
export class User {
@Prop(
raw({
firstName: { type: String },
lastName: { type: String },
fullName: { type: String }
})
)
name: Record<string, any>;
@Prop({ required: true })
email: string;
}
export const UserSchema = SchemaFactory.createForClass(User);
如此一来,便完成了 schema
的实作,是不是很方便呢!
在完成 schema
以後就要来实作 model
的部分了,这里我们先建立 UserModule
、UserController
以及 UserService
来替我们的 API 做准备:
$ nest generate module features/user
$ nest generate controller features/user
$ nest generate service features/user
MongooseModule
有提供 forFeature
方法来配置 MongooseModule
,并在该作用域下定义需要的 model
,使用方式很简单,给定一个阵列其内容即为 要使用的 schema 与 对应的 collection 名称,通常我们习惯直接使用 schema
的 class
名称作为值,其最终会对应到的 collection
为 名称 + s,举例来说,User
会对应到的 collection
名称即 users
。
我们修改 user.module.ts
,将 MongooseModule
引入,并在 UserModule
的作用域下使用 User
的 model
:
import { Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose';
import { User, UserSchema } from '../../common/models/user.model';
import { UserController } from './user.controller';
import { UserService } from './user.service';
@Module({
imports: [
MongooseModule.forFeature([
{ name: User.name, schema: UserSchema }
])
],
controllers: [UserController],
providers: [UserService]
})
export class UserModule {}
在定义好之後,就可以透过 @InjectModel
来将 User
的 model
注入到 UserService
中,并给定型别 UserDocument
:
import { Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Model } from 'mongoose';
import { User, UserDocument } from '../../common/models/user.model';
@Injectable()
export class UserService {
constructor(
@InjectModel(User.name) private readonly userModel: Model<UserDocument>
) {}
}
修改 user.service.ts
的内容,新增一个 create
方法,并呼叫 userModel
的 create
方法来建立一个使用者到 users
这个 collection
里面:
import { Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Model } from 'mongoose';
import { User, UserDocument } from '../../common/models/user.model';
@Injectable()
export class UserService {
constructor(
@InjectModel(User.name) private readonly userModel: Model<UserDocument>
) {}
create(user: any) {
return this.userModel.create(user);
}
}
修改 user.controller.ts
的内容,设计一个 POST
方法来建立使用者,并且返回 UserDocument
到客户端:
import { Body, Controller, Post } from '@nestjs/common';
import { UserService } from './user.service';
@Controller('users')
export class UserController {
constructor(
private readonly userService: UserService
) {}
@Post()
create(@Body() body: any) {
return this.userService.create(body);
}
}
透过 Postman 建立使用者,成功建立之结果如下图:
修改 user.service.ts
的内容,新增一个 findById
方法,并呼叫 userModel
的 findById
方法来透过 id
取得使用者资料:
import { Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Model } from 'mongoose';
import { User, UserDocument } from '../../common/models/user.model';
@Injectable()
export class UserService {
constructor(
@InjectModel(User.name) private readonly userModel: Model<UserDocument>
) {}
findById(id: string) {
return this.userModel.findById(id);
}
}
修改 user.controller.ts
的内容,设计一个 GET
方法来取得使用者,并且返回 UserDocument
到客户端:
import { Body, Controller, Get, Param } from '@nestjs/common';
import { UserService } from './user.service';
@Controller('users')
export class UserController {
constructor(
private readonly userService: UserService
) {}
@Get(':id')
findById(@Param('id') id: string) {
return this.userService.findById(id);
}
}
透过 Postman 来取得使用者资料,成功取得之结果如下图:
修改 user.service.ts
的内容,新增一个 updateById
方法,并呼叫 userModel
的 findByIdAndUpdate
方法来透过 id
更新使用者资料:
import { Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Model } from 'mongoose';
import { User, UserDocument } from '../../common/models/user.model';
@Injectable()
export class UserService {
constructor(
@InjectModel(User.name) private readonly userModel: Model<UserDocument>
) {}
updateById(id: string, data: any) {
return this.userModel.findByIdAndUpdate(id, data, { new: true });
}
}
注意:上方的
new
参数是让mongoose
回传更新後的结果,预设为false
。
修改 user.controller.ts
的内容,设计一个 PATCH
方法来更新使用者资料,并且返回 UserDocument
到客户端:
import { Body, Controller, Param, Patch } from '@nestjs/common';
import { UserService } from './user.service';
@Controller('users')
export class UserController {
constructor(
private readonly userService: UserService
) {}
@Patch(':id')
updateById(
@Param('id') id: string,
@Body() body: any
) {
return this.userService.updateById(id, body);
}
}
透过 Postman 来更新使用者资料,成功更新之结果如下图:
修改 user.service.ts
的内容,新增一个 removeById
方法,并呼叫 userModel
的 remove
方法来透过 id
删除使用者资料:
import { Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Model } from 'mongoose';
import { User, UserDocument } from '../../common/models/user.model';
@Injectable()
export class UserService {
constructor(
@InjectModel(User.name) private readonly userModel: Model<UserDocument>
) {}
removeById(id: string) {
return this.userModel.remove({ _id: id });
}
}
修改 user.controller.ts
的内容,设计一个 DELETE
方法来删除使用者资料,并且返回删除的相关资讯到客户端:
import { Controller, Delete, Param } from '@nestjs/common';
import { UserService } from './user.service';
@Controller('users')
export class UserController {
constructor(
private readonly userService: UserService
) {}
@Delete(':id')
removeById(@Param('id') id: string) {
return this.userService.removeById(id);
}
}
透过 Postman 来删除使用者资料,成功更新之结果如下图:
mongoose
有提供 hook 让开发人员使用,其属於 schema
的层级,它可以用来实作许多功能,比如说:我希望在储存之前可以在终端机将内容印出来、我希望在储存之前添加时间戳等,都可以透过 hook 来实现。而 hook 的注册需要在 model
建立之前,使用 MongooseModule
的 forFeatureAsync
方法来实作工厂函式,并在工厂函式中完成 hook 的注册即可。
这里以 user.module.ts
为例,我们透过 UserSchema
的 pre
方法来介接 动作发生之前 的 hook,这里以 save
为例,其触发时间点为储存之前:
import { Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose';
import { User, UserDocument, UserSchema } from '../../common/models/user.model';
import { UserController } from './user.controller';
import { UserService } from './user.service';
@Module({
imports: [
MongooseModule.forFeatureAsync([
{
name: User.name,
useFactory: () => {
UserSchema.pre('save', function(this: UserDocument, next) {
console.log(this);
next();
});
return UserSchema;
}
}
])
],
controllers: [UserController],
providers: [UserService]
})
export class UserModule {}
透过 Postman 建立一个使用者,会在终端机看到下方资讯,表示 hook 顺利执行了:
{
"_id": "60db07078d5c2c53e49e6cf0",
"name": {
"firstName": "Hao",
"lastName": "Hsieh",
"fullName": "Hao Hsieh"
},
"email": "[email protected]"
}
资料库可以说是後端不可或缺的一环,而本篇以 MongoDB 为例,并采用 Nest 包装的 mongoose
来进行存取。这里附上今天的懒人包:
mongoose
是 schema-based
的 ODM 套件。schema
用来定义特定 collection
下 document
的资料结构。schema
建立 model
,并透过其存取 MongoDB。
<<: Day 27 CSS3 < 动画 animation>
>>: [Day 23] Reactive Programming - Spring WebFlux(Handler)
我的梦想就是带这一台笔电走遍全世界,「成为一个工程师似乎可以完成这个梦想」,於是在去年底毅然决然的投...
昨天了解各种 Line 的 Message Type 後,今天就运用其中的格式来优化验证码小帮手的互...
小测验 我们在上一章的最开始,示范了元组上的 <*>,其中有一条是这样写的: pure ...
🐄🐄🐄🐄🐄🐄🐄🐄🐄🐄🐄🐄🐄🐄🐄🐄🐄🐄🐄🐄🐄🐄🐄🐄🐄🐄🐄 Day 01 - 认识C++++ 乳牛与程序...
第30天了,来聊聊 vm/虚拟机 ... 如果只是想练习Linux的CLI,一定要在GCP上开ins...