## Mongo
`Nest`支持兩種與 [MongoDB](http://www.mongodb.org/) 數據庫集成的方式。既使用內置的[TypeORM](https://github.com/typeorm/typeorm) 提供的 MongoDB 連接器,或使用最流行的 MongoDB 對象建模工具 [Mongoose](http://mongoosejs.com/)。在本章后續描述中我們使用專用的`@nestjs/mongoose`包。
首先,我們需要安裝所有必需的依賴項:
```bash
$ npm install --save @nestjs/mongoose mongoose
```
安裝過程完成后,我們可以將其 `MongooseModule` 導入到根目錄 `AppModule` 中。
> app.module.ts
```typescript
import { Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose';
@Module({
imports: [MongooseModule.forRoot('mongodb://localhost/nest')],
})
export class AppModule {}
```
該 `forRoot()` 和 [mongoose](http://mongoosejs.com/) 包中的 `mongoose.connect()` 一樣的參數對象。[參見](https://mongoosejs.com/docs/connections.html)。
### 模型注入
在`Mongoose`中,一切都源于 [Scheme](http://mongoosejs.com/docs/guide.html),每個 `Schema` 都會映射到 `MongoDB` 的一個集合,并定義集合內文檔的結構。`Schema` 被用來定義模型,而模型負責從底層創建和讀取 `MongoDB` 的文檔。
`Schema` 可以用 `NestJS` 內置的裝飾器來創建,或者也可以自己動手使用 `Mongoose`的常規方式。使用裝飾器來創建 `Schema` 會極大大減少引用并且提高代碼的可讀性。
我們先定義`CatSchema`:
> schemas/cat.schema.ts
```typescript
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { Document } from 'mongoose';
export type CatDocument = Cat & Document;
@Schema()
export class Cat extends Document {
@Prop()
name: string;
@Prop()
age: number;
@Prop()
breed: string;
}
export const CatSchema = SchemaFactory.createForClass(Cat);
```
> 注意你也可以通過使用 `DefinitionsFactory` 類(可以從 `@nestjs/mongoose` 導入)來生成一個原始 `Schema` ,這將允許你根據被提供的元數據手動修改生成的 `Schema` 定義。這對于某些很難用裝飾器體現所有的極端例子非常有用。
`@Schema` 裝飾器標記一個類作為`Schema` 定義,它將我們的 `Cat` 類映射到 `MongoDB` 同名復數的集合 `Cats`,這個裝飾器接受一個可選的 `Schema` 對象。將它想象為那個你通常會傳遞給 `mongoose.Schema` 類的構造函數的第二個參數(例如, `new mongoose.Schema(_, options))`)。
更多可用的 `Schema` 選項可以 [看這里](https://mongoosejs.com/docs/guide.html#options)。
`@Prop` 裝飾器在文檔中定義了一個屬性。舉個例子,在上面的 `Schema` 定義中,我們定義了三個屬性,分別是:`name` ,`age` 和 `breed`。得益于 `TypeScript` 的元數據(還有反射),這些屬性的 [`Schema類型`](https://mongoosejs.com/docs/schematypes.html)會被自動推斷。然而在更復雜的場景下,有些類型例如對象和嵌套數組無法正確推斷類型,所以我們要向下面一樣顯式的指出。
```typescript
@Prop([String])
tags: string[];
```
另外的 `@Prop` 裝飾器接受一個可選的參數,通過這個,你可以指示這個屬性是否是必須的,是否需要默認值,或者是標記它作為一個常量,下面是例子:
```typescript
@Prop({ required: true })
name: string;
```
最后的,原始 `Schema` 定義也可以被傳遞給裝飾器。這也非常有用,舉個例子,一個屬性體現為一個嵌套對象而不是一個定義的類。要使用這個,需要從像下面一樣從 `@nestjs/mongoose` 包導入 `raw()`。
```typescript
@Prop(raw({
firstName: { type: String },
lastName: { type: String }
}))
details: Record<string, any>;
```
或者,如果你不喜歡使用裝飾器,你可以使用 `mongoose.Schema` 手動定義一個 `Schema`。下面是例子:
> schemas/cat.schema.ts
```typescript
import * as mongoose from 'mongoose';
export const CatSchema = new mongoose.Schema({
name: String,
age: Number,
breed: String,
});
```
該 `cat.schema` 文件在 `cats` 目錄下。這個目錄包含了和 `CatsModule`模塊有關的所有文件。你可以決定在哪里保存`Schema`文件,但我們推薦在他們的**域**中就近創建,即在相應的模塊目錄中。
我們來看看`CatsModule`:
> cats.module.ts
```typescript
import { Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose';
import { CatsController } from './cats.controller';
import { CatsService } from './cats.service';
import { Cat, CatSchema } from './schemas/cat.schema';
@Module({
imports: [MongooseModule.forFeature([{ name: Cat.name, schema: CatSchema }])],
controllers: [CatsController],
providers: [CatsService],
})
export class CatsModule {}
```
`MongooseModule`提供了`forFeature()`方法來配置模塊,包括定義哪些模型應該注冊在當前范圍中。如果你還想在另外的模塊中使用這個模型,將`MongooseModule`添加到`CatsModule`的`exports`部分并在其他模塊中導入`CatsModule`。
注冊`Schema`后,可以使用 `@InjectModel()` 裝飾器將 `Cat` 模型注入到 `CatsService` 中:
> cats.service.ts
```typescript
import { Model } from 'mongoose';
import { Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Cat, CatDocument } from './schemas/cat.schema';
import { CreateCatDto } from './dto/create-cat.dto';
@Injectable()
export class CatsService {
constructor(@InjectModel('Cat') private catModel: Model<CatDocument>) {}
async create(createCatDto: CreateCatDto): Promise<Cat> {
const createdCat = new this.catModel(createCatDto);
return createdCat.save();
}
async findAll(): Promise<Cat[]> {
return this.catModel.find().exec();
}
}
```
### 連接
有時你可能需要連接原生的[Mongoose 連接](https://mongoosejs.com/docs/api.html#Connection)對象,你可能在連接對象中想使用某個原生的 API。你可以使用如下的`@InjectConnection()`裝飾器來注入 Mongoose 連接。
```typescript
import { Injectable } from '@nestjs/common';
import { InjectConnection } from '@nestjs/mongoose';
import { Connection } from 'mongoose';
@Injectable()
export class CatsService {
constructor(@InjectConnection() private connection: Connection) {}
}
```
### 多數據庫
有的項目需要多數據庫連接,可以在這個模塊中實現。要使用多連接,首先要創建連接,在這種情況下,*連接*必須要有名稱。
> app.module.ts
```typescript
import { Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose';
@Module({
imports: [
MongooseModule.forRoot('mongodb://localhost/test', {
connectionName: 'cats',
}),
MongooseModule.forRoot('mongodb://localhost/users', {
connectionName: 'users',
}),
],
})
export class AppModule {}
```
> 你不能在沒有名稱的情況下使用多連接,也不能對多連接使用同一個名稱,否則會被覆蓋掉。
在設置中,要告訴`MongooseModule.forFeature()`方法應該使用哪個連接。
```typescript
@Module({
imports: [MongooseModule.forFeature([{ name: 'Cat', schema: CatSchema }], 'cats')],
})
export class AppModule {}
```
也可以向一個給定的連接中注入`Connection`。
```typescript
import { Injectable } from '@nestjs/common';
import { InjectConnection } from '@nestjs/mongoose';
import { Connection } from 'mongoose';
@Injectable()
export class CatsService {
constructor(@InjectConnection('cats') private connection: Connection) {}
}
```
### 鉤子(中間件)
中間件(也被稱作預處理(pre)和后處理(post)鉤子)是在執行異步函數時傳遞控制的函數。中間件是針對`Schema`層級的,在寫插件([源碼](https://mongoosejs.com/docs/middleware.html))時非常有用。在 Mongoose 編譯完模型后使用`pre()`或`post()`不會起作用。要在模型注冊前注冊一個鉤子,可以在使用一個工廠提供者(例如 `useFactory`)是使用`MongooseModule`中的`forFeatureAsync()`方法。使用這一技術,你可以訪問一個 Schema 對象,然后使用`pre()`或`post()`方法來在那個 schema 中注冊一個鉤子。示例如下:
```typescript
@Module({
imports: [
MongooseModule.forFeatureAsync([
{
name: 'Cat',
useFactory: () => {
const schema = CatsSchema;
schema.pre('save', () => console.log('Hello from pre save'));
return schema;
},
},
]),
],
})
export class AppModule {}
```
和其他[工廠提供者](https://docs.nestjs.com/fundamentals/custom-providers#factory-providers-usefactory)一樣,我們的工廠函數是異步的,可以通過`inject`注入依賴。
```typescript
@Module({
imports: [
MongooseModule.forFeatureAsync([
{
name: 'Cat',
imports: [ConfigModule],
useFactory: (configService: ConfigService) => {
const schema = CatsSchema;
schema.pre('save', () => console.log(`${configService.get<string>('APP_NAME')}: Hello from pre save`));
return schema;
},
inject: [ConfigService],
},
]),
],
})
export class AppModule {}
```
### 插件
要向給定的 schema 中注冊[插件](https://mongoosejs.com/docs/plugins.html),可以使用`forFeatureAsync()`方法。
```typescript
@Module({
imports: [
MongooseModule.forFeatureAsync([
{
name: 'Cat',
useFactory: () => {
const schema = CatsSchema;
schema.plugin(require('mongoose-autopopulate'));
return schema;
},
},
]),
],
})
export class AppModule {}
```
要向所有 schema 中立即注冊一個插件,調用`Connection`對象中的`.plugin()`方法。你可以在所有模型創建前訪問連接。使用`connectionFactory`來實現:
> app.module.ts
```typescript
import { Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose';
@Module({
imports: [
MongooseModule.forRoot('mongodb://localhost/test', {
connectionFactory: (connection) => {
connection.plugin(require('mongoose-autopopulate'));
return connection;
},
}),
],
})
export class AppModule {}
```
#### 鑒別器[#](#discriminators)
[鑒別](https://mongoosejs.com/docs/discriminators.html)器是一種模式繼承機制。它們使您能夠在同一個基礎 MongoDB 集合之上擁有多個具有重疊模式的模型。
假設您想在單個集合中跟蹤不同類型的事件。每個事件都會有一個時間戳。
>event.schema.ts
~~~typescript
@Schema({ discriminatorKey: 'kind' })
export class Event {
@Prop({
type: String,
required: true,
enum: [ClickedLinkEvent.name, SignUpEvent.name],
})
kind: string;
@Prop({ type: Date, required: true })
time: Date;
}
export const EventSchema = SchemaFactory.createForClass(Event);
~~~
> **提示**:`mongoose` 區分不同判別器模型的方式是`__t`通過默認的“判別器鍵”。`Mongoose` 添加了一個字符串路徑,該路徑調用`__t`到您的模式中,用于跟蹤該文檔是哪個鑒別器的實例。您也可以使用該`discriminatorKey`選項來定義區分路徑。
`SignedUpEvent`和`ClickedLinkEvent`實例將存儲在與通用事件相同的集合中。
現在,讓我們定義`ClickedLinkEvent`類,如下所示:
>click-link-event.schema.ts
~~~typescript
@Schema()
export class ClickedLinkEvent {
kind: string;
time: Date;
@Prop({ type: String, required: true })
url: string;
}
export const ClickedLinkEventSchema = SchemaFactory.createForClass(ClickedLinkEvent);
~~~
和`SignUpEvent`類:
>sign-up-event.schema.ts
~~~typescript
@Schema()
export class SignUpEvent {
kind: string;
time: Date;
@Prop({ type: String, required: true })
user: string;
}
export const SignUpEventSchema = SchemaFactory.createForClass(SignUpEvent);
~~~
有了這個,使用`discriminators`選項為給定的模式注冊一個鑒別器。它適用于`MongooseModule.forFeature`和`MongooseModule.forFeatureAsync`:
>event.module.ts
~~~typescript
import { Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose';
@Module({
imports: [
MongooseModule.forFeature([
{
name: Event.name,
schema: EventSchema,
discriminators: [
{ name: ClickedLinkEvent.name, schema: ClickedLinkEventSchema },
{ name: SignUpEvent.name, schema: SignUpEventSchema },
],
},
]),
]
})
export class EventsModule {}
~~~
### 測試
在單元測試我們的應用程序時,我們通常希望避免任何數據庫連接,使我們的測試套件獨立并盡可能快地執行它們。但是我們的類可能依賴于從連接實例中提取的模型。如何處理這些類呢?解決方案是創建模擬模型。
為了簡化這一過程,`@nestjs/mongoose` 包公開了一個 `getModelToken()` 函數,該函數根據一個 `token` 名稱返回一個準備好的`[注入token](https://docs.nestjs.com/fundamentals/custom-providers#di-fundamentals)`。使用此 `token`,你可以輕松地使用任何標準[自定義提供者](https://docs.nestjs.com/fundamentals/custom-providers)技術,包括 `useClass`、`useValue` 和 `useFactory`。例如:
```typescript
@Module({
providers: [
CatsService,
{
provide: getModelToken('Cat'),
useValue: catModel,
},
],
})
export class CatsModule {}
```
在本例中,每當任何使用者使用 `@InjectModel()` 裝飾器注入模型時,都會提供一個硬編碼的 `Model<Cat>` (對象實例)。
### 異步配置
通常,您可能希望異步傳遞模塊選項,而不是事先傳遞它們。在這種情況下,使用 `forRootAsync()` 方法,`Nest`提供了幾種處理異步數據的方法。
第一種可能的方法是使用工廠函數:
```typescript
MongooseModule.forRootAsync({
useFactory: () => ({
uri: 'mongodb://localhost/nest',
}),
});
```
與其他工廠提供程序一樣,我們的工廠函數可以是異步的,并且可以通過注入注入依賴。
```typescript
MongooseModule.forRootAsync({
imports: [ConfigModule],
useFactory: async (configService: ConfigService) => ({
uri: configService.getString('MONGODB_URI'),
}),
inject: [ConfigService],
});
```
或者,您可以使用類而不是工廠來配置 `MongooseModule`,如下所示:
```typescript
MongooseModule.forRootAsync({
useClass: MongooseConfigService,
});
```
上面的構造在 `MongooseModule`中實例化了 `MongooseConfigService`,使用它來創建所需的 `options` 對象。注意,在本例中,`MongooseConfigService` 必須實現 `MongooseOptionsFactory` 接口,如下所示。 `MongooseModule` 將在提供的類的實例化對象上調用 `createMongooseOptions()` 方法。
```typescript
@Injectable()
class MongooseConfigService implements MongooseOptionsFactory {
createMongooseOptions(): MongooseModuleOptions {
return {
uri: 'mongodb://localhost/nest',
};
}
}
```
為了防止 `MongooseConfigService` 內部創建 `MongooseModule` 并使用從不同模塊導入的提供程序,您可以使用 `useExisting` 語法。
```typescript
MongooseModule.forRootAsync({
imports: [ConfigModule],
useExisting: ConfigService,
});
```
### 例子
一個可用的示例見[這里](https://github.com/nestjs/nest/tree/master/sample/06-mongoose)。
- 介紹
- 概述
- 第一步
- 控制器
- 提供者
- 模塊
- 中間件
- 異常過濾器
- 管道
- 守衛
- 攔截器
- 自定義裝飾器
- 基礎知識
- 自定義提供者
- 異步提供者
- 動態模塊
- 注入作用域
- 循環依賴
- 模塊參考
- 懶加載模塊
- 應用上下文
- 生命周期事件
- 跨平臺
- 測試
- 技術
- 數據庫
- Mongo
- 配置
- 驗證
- 緩存
- 序列化
- 版本控制
- 定時任務
- 隊列
- 日志
- Cookies
- 事件
- 壓縮
- 文件上傳
- 流式處理文件
- HTTP模塊
- Session(會話)
- MVC
- 性能(Fastify)
- 服務器端事件發送
- 安全
- 認證(Authentication)
- 授權(Authorization)
- 加密和散列
- Helmet
- CORS(跨域請求)
- CSRF保護
- 限速
- GraphQL
- 快速開始
- 解析器(resolvers)
- 變更(Mutations)
- 訂閱(Subscriptions)
- 標量(Scalars)
- 指令(directives)
- 接口(Interfaces)
- 聯合類型
- 枚舉(Enums)
- 字段中間件
- 映射類型
- 插件
- 復雜性
- 擴展
- CLI插件
- 生成SDL
- 其他功能
- 聯合服務
- 遷移指南
- Websocket
- 網關
- 異常過濾器
- 管道
- 守衛
- 攔截器
- 適配器
- 微服務
- 概述
- Redis
- MQTT
- NATS
- RabbitMQ
- Kafka
- gRPC
- 自定義傳輸器
- 異常過濾器
- 管道
- 守衛
- 攔截器
- 獨立應用
- Cli
- 概述
- 工作空間
- 庫
- 用法
- 腳本
- Openapi
- 介紹
- 類型和參數
- 操作
- 安全
- 映射類型
- 裝飾器
- CLI插件
- 其他特性
- 遷移指南
- 秘籍
- CRUD 生成器
- 熱重載
- MikroORM
- TypeORM
- Mongoose
- 序列化
- 路由模塊
- Swagger
- 健康檢查
- CQRS
- 文檔
- Prisma
- 靜態服務
- Nest Commander
- 問答
- Serverless
- HTTP 適配器
- 全局路由前綴
- 混合應用
- HTTPS 和多服務器
- 請求生命周期
- 常見錯誤
- 實例
- 遷移指南
- 發現
- 誰在使用Nest?