【问题标题】:Mongoose Subdocuments in Nest.jsNest.js 中的 Mongoose 子文档
【发布时间】:2020-10-23 12:57:48
【问题描述】:

我正在将我的应用程序从 express.js 移动到 Nest.js,并且我找不到在另一个中引用一个 mongoose Schema 的方法,而不使用使用 mongoose.Schema({... })。

让我们使用文档中的示例,这样我可以澄清我的问题:

@Schema()
  export class Cat extends Document {
  @Prop()
  name: string;
}

export const CatSchema = SchemaFactory.createForClass(Cat);

现在,我想要的是这样的:

@Schema()
export class Owner extends Document {
  @Prop({type: [Cat], required: true})
  cats: Cat[];
}

export const OwnerSchema = SchemaFactory.createForClass(Owner);

当我以这种方式定义架构时,我会得到一个错误,如下所示:无效的架构配置:Cat is not a valid 在数组中输入cats

那么,使用这种更面向对象的方法来定义架构,在另一个架构中引用一个架构的正确方法是什么?

【问题讨论】:

    标签: reference schema nestjs subdocument


    【解决方案1】:

    我钻研了源码,了解了SchemaFactory.createForClass方法是如何转换Schema类的。

    那么它是如何工作的?

    1。看看下面这个例子:

    @Schema()
    export class Cat extends Document {
      @Prop()
      name: string;
    }
    export const catSchema = SchemaFactory.createForClass(Cat);
    

    基本上,当你做SchemaFactory.createForClass(Cat)

    Nest 会将类语法转换为 Mongoose 模式语法,所以最终转换的结果是这样的:

    const schema = new mongoose.Schema({
        name: { type: String } // Notice that `String` is now uppercase.
    });
    

    2。转换是如何进行的?

    看看这个文件:mongoose/prop.decorator.ts at master · nestjs/mongoose · GitHub

    export function Prop(options?: PropOptions): PropertyDecorator {
      return (target: object, propertyKey: string | symbol) => {
        options = (options || {}) as mongoose.SchemaTypeOpts<unknown>;
    
        const isRawDefinition = options[RAW_OBJECT_DEFINITION];
        if (!options.type && !Array.isArray(options) && !isRawDefinition) {
          const type = Reflect.getMetadata(TYPE_METADATA_KEY, target, propertyKey);
    
          if (type === Array) {
            options.type = [];
          } else if (type && type !== Object) {
            options.type = type;
          }
        }
    
        TypeMetadataStorage.addPropertyMetadata({
          target: target.constructor,
          propertyKey: propertyKey as string,
          options,
        });
      };
    }
    

    在这里你可以看到 Prop() 装饰器在幕后做了什么。 当你这样做时:

    @Prop()
    name: string;
    

    Prop 函数将被调用,在这种情况下没有参数。

    const type = Reflect.getMetadata(TYPE_METADATA_KEY, target, propertyKey);
    

    使用Reflect API,我们可以获取您在执行name: string 时使用的数据类型。 type 变量的值现在设置为 String。请注意,它不是stringReflect API 将始终返回数据类型的构造函数版本,因此:

    • number 将被序列化为 Number
    • string 将被序列化为 String
    • boolean 将被序列化为 Boolean
    • 等等

    TypeMetadataStorage.addPropertyMetadata 然后会将下面的对象存储到存储中。

    {
        target: User,
        propertyKey: ‘name’,
        options: { type: String }
    }
    

    我们来看看:mongoose/type-metadata.storage.ts at master · nestjs/mongoose · GitHub

    export class TypeMetadataStorageHost {
      private schemas = new Array<SchemaMetadata>();
      private properties = new Array<PropertyMetadata>();
    
      addPropertyMetadata(metadata: PropertyMetadata) {
        this.properties.push(metadata);
      }
    }
    

    所以基本上该对象将存储在TypeMetadataStorageHost 中的properties 变量中。 TypeMetadataStorageHost 是一个可以存储大量此类对象的单例。

    3。架构生成

    要了解 SchemaFactory.createForClass(Cat) 如何生成 Mongoose 架构,请查看以下内容:mongoose/schema.factory.ts at master · nestjs/mongoose · GitHub

    export class SchemaFactory {
      static createForClass(target: Type<unknown>) {
        const schemaDefinition = DefinitionsFactory.createForClass(target);
        const schemaMetadata = TypeMetadataStorage.getSchemaMetadataByTarget(
          target,
        );
        return new mongoose.Schema(
          schemaDefinition,
          schemaMetadata && schemaMetadata.options,
        );
      }
    }
    

    最重要的部分是: const schemaDefinition = DefinitionsFactory.createForClass(target);。请注意,这里的目标是您的 Cat 类。

    你可以在这里看到方法定义:mongoose/definitions.factory.ts at master · nestjs/mongoose · GitHub

    export class DefinitionsFactory {
      static createForClass(target: Type<unknown>): mongoose.SchemaDefinition {
        let schemaDefinition: mongoose.SchemaDefinition = {};
    
      schemaMetadata.properties?.forEach((item) => {
        const options = this.inspectTypeDefinition(item.options as any);
        schemaDefinition = {
        [item.propertyKey]: options as any,
          …schemaDefinition,
        };
      });
    
        return schemaDefinition;
    }
    

    schemaMetadata.properties 包含您在执行TypeMetadataStorage.addPropertyMetadata 时存储的对象:

    [
        {
            target: User,
            propertyKey: ‘name’,
            options: { type: String }
        }
    ]
    

    forEach 将产生:

    {
        name: { type: String }
    }
    

    最后,它将作为mongoose.Schema构造函数mongoose/schema.factory.ts at master · nestjs/mongoose · GitHub的参数:

    return new mongoose.Schema(
        schemaDefinition,
        schemaMetadata && schemaMetadata.options,
    );
    

    4。所以回答这个问题:

    你应该把什么作为Prop() 参数?

    还记得 Nest 何时使用 forEach 生成 Mongoose Schema 吗?

    schemaMetadata.properties?.forEach((item) => {
      const options = this.inspectTypeDefinition(item.options as any);
      schemaDefinition = {
        [item.propertyKey]: options as any,
        …schemaDefinition,
      };
    });
    

    要获得options,它使用inspectTypeDefinition 方法。你可以看到下面的定义:

    private static inspectTypeDefinition(options: mongoose.SchemaTypeOpts<unknown> | Function): PropOptions {
      if (typeof options === 'function') {
        if (this.isPrimitive(options)) {
          return options;
        } else if (this.isMongooseSchemaType(options)) {
          return options;
        }
        return this.createForClass(options as Type<unknown>);   
      } else if (typeof options.type === 'function') {
        options.type = this.inspectTypeDefinition(options.type);
        return options;
      } else if (Array.isArray(options)) {
        return options.length > 0
          ? [this.inspectTypeDefinition(options[0])]
          : options;
      }
      return options;
    }
    

    在这里你可以得出结论:

    1. 如果optionsfunction,例如StringSchemaType,它将直接返回并用作Mongoose 选项。
    2. 如果optionsArray,它将返回该数组的第一个索引并将其包装在一个数组中。
    3. 如果options不是Arrayfunction,例如,如果它只是一个普通的object{ type: String, required: true },它将直接返回并用作Mongoose选项。李>

    回答

    所以要添加从CatOwner 的引用,您可以这样做:

    import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
    import { Document, Schema as MongooseSchema } from 'mongoose';
    import { Owner } from './owner.schema.ts';
    
    @Schema()
    export class Cat extends Document {
      @Prop()
      name: string;
    
      @Prop({ type: MongooseSchema.Types.ObjectId, ref: Owner.name })
      owner: Owner;
    }
    
    export const catSchema = SchemaFactory.createForClass(Cat);
    

    至于如何将Owner的引用添加到Cat,我们可以这样做:

    @Prop([{ type: MongooseSchema.Types.ObjectId, ref: Cat.name }])
    

    更新

    回答评论部分的问题:

    如何将架构嵌入到另一个架构中?

    如果你正确地阅读了答案,你应该有足够的知识来做到这一点。但如果您不这样做,这就是 TLDR 的答案。

    请注意,我强烈建议您在前往此处之前阅读完整的答案。

    image-variant.schema.ts

    import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
    
    @Schema()
    export class ImageVariant {
      @Prop()
      url: string;
    
      @Prop()
      width: number;
    
      @Prop()
      height: number;
    
      @Prop()
      size: number;
    }
    
    export const imageVariantSchema = SchemaFactory.createForClass(ImageVariant);
    
    

    image.schema.ts

    import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
    import { Document } from 'mongoose';
    import { imageVariantSchema, ImageVariant } from './imagevariant.schema';
    
    @Schema()
    export class Image extends Document {
      @Prop({ type: imageVariantSchema })
      large: ImageVariant;
    
      @Prop({ type: imageVariantSchema })
      medium: ImageVariant;
    
      @Prop({ type: imageVariantSchema })
      small: ImageVariant;
    }
    
    export const imageSchema = SchemaFactory.createForClass(Image);
    

    【讨论】:

    • 像魅力一样工作!很好的解释,非常感谢!
    • 这并没有回答这个问题。如何使用装饰器创建嵌套模式?
    • @Sinandro 而不是 @Prop({type: [Cat]}) 你写 @Prop([{ type: MongooseSchema.Types.ObjectId, ref: Cat.name }]) 。你能举例说明你的意思吗?
    • @Sinandro 问题是“在另一个模式中引用一个模式”而不是“在另一个模式中嵌入模式”。你的问题是一个不同的问题。请查看inspectTypeDefinition 方法中的else if (typeof options.type === 'function')。这就是你想要的答案。
    • @Yaron 它不会保存整个文档。只要你在@Prop装饰器中指定ref,它将被保存为关系引用,因此它只会保存id。此功能并非来自 @nestjs/mongoose 库,而是来自 Mongoose。
    【解决方案2】:
    import { Prop, raw, Schema, SchemaFactory } from '@nestjs/mongoose';
    import * as mongoose from 'mongoose';
    import { Education } from '../../education/schemas';
    import { RECORD_STATUS } from '../../common/common.constants';
    import { Employment } from '../../employment/schemas';
    import {
        JOB_SEARCH_STATUS,
        LANGUAGE_PROFICIENCY
    } from '../user-profile.constants';
    
    const externalLinks = {
        linkedInUrl: { type: String },
        githubUrl: { type: String },
        twitterUrl: { type: String },
        blogUrl: { type: String },
        websiteUrl: { type: String },
        stackoverflowUrl: { type: String }
    };
    
    const address = {
        line1: { type: String, required: true },
        line2: { type: String },
        zipCode: { type: String },
        cityId: { type: Number },
        countryId: { type: Number }
    };
    
    const language = {
        name: { type: String, require: true },
        code: { type: String, required: true },
        proficiency: { type: String, required: true, enum: LANGUAGE_PROFICIENCY }
    };
    
    const options = {
        timestamps: true,
    };
    
    export type UserProfileDocument = UserProfile & mongoose.Document;
    
    @Schema(options)
    export class UserProfile {
    
        _id: string;
    
        @Prop()
        firstName: string;
    
        @Prop()
        lastName: string;
    
        @Prop()
        headline: string;
    
        @Prop({
            unique: true,
            trim: true,
            lowercase: true
        })
        email: string;
    
        @Prop()
        phoneNumber: string
    
        @Prop(raw({
            jobSearchStatus: { type: String, enum: JOB_SEARCH_STATUS, required: true }
        }))
        jobPreferences: Record<string, any>;
    
        @Prop(raw(externalLinks))
        externalLinks: Record<string, any>;
    
        @Prop([String])
        skills: string[];
    
        @Prop(raw({ type: address, required: false }))
        address: Record<string, any>;
    
        @Prop()
        birthDate: Date;
    
        @Prop({ type: [{ type: mongoose.Schema.Types.ObjectId, ref: 'Employment' }] })
        employments: Employment[];
    
        @Prop({ type: [{ type: mongoose.Schema.Types.ObjectId, ref: 'Education' }] })
        educations: Education[];
    
        @Prop(raw([language]))
        languages: Record<string, any>[];
    
        @Prop()
        timeZone: string;
    
        @Prop()
        createdAt: Date;
    
        @Prop()
        updatedAt: Date;
    
        @Prop({
            enum: RECORD_STATUS,
            required: true,
            default: RECORD_STATUS.Active
        })
        recordStatus: string;
    }
    
    export const UserProfileSchema = SchemaFactory.createForClass(UserProfile);
    

    【讨论】:

      【解决方案3】:

      为子文档创建SchemaFactory.createForClass,并在文档中引用其类型。

          import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
          
          @Schema()
          export class SubDocument {
           @Prop()
            name: string;
          
            @Prop()
            description: number;
          }
          
          const subDocumentSchema = SchemaFactory.createForClass(SubDocument);
          
          @Schema()
          export class Document {
            @Prop()
            name: string;
          
            @Prop({ type: subDocumentSchema })
            subDocument: SubDocument;
          }
          
          export const documentSchema = SchemaFactory.createForClass(Document);
      

      【讨论】:

        猜你喜欢
        • 2020-07-16
        • 1970-01-01
        • 1970-01-01
        • 2017-05-20
        • 2013-05-21
        • 2017-09-17
        • 1970-01-01
        • 2014-08-16
        • 1970-01-01
        相关资源
        最近更新 更多