Typegoose 模型和多对多关系

Ale*_*lex 3 mongoose mongodb typescript typegoose

所以我正在使用 NestJs 和 Typegoose 构建一个后端,具有以下模型:

部门

@modelOptions({ schemaOptions: { collection: 'user_department', toJSON: { virtuals: true }, toObject: { virtuals: true }, id: false } })
export class Department {

    @prop({ required: true })
    _id: mongoose.Types.ObjectId;

    @prop({ required: true })
    name: string;

    @prop({ ref: () => User, type: String })
    public supervisors: Ref<User>[];

    members: User[];

    static paginate: PaginateMethod<Department>;
}
Run Code Online (Sandbox Code Playgroud)

用户

@modelOptions({ schemaOptions: { collection: 'user' } })
export class User {

    @prop({ required: true, type: String })
    _id: string;

    @prop({ required: true })
    userName: string;

    @prop({ required: true })
    firstName: string;

    @prop({ required: true })
    lastName: string;

    [...]

    @prop({ ref: () => Department, default: [] })
    memberOfDepartments?: Ref<Department>[];

    static paginate: PaginateMethod<User>;
}
Run Code Online (Sandbox Code Playgroud)

正如您可能猜到的,一个用户可能属于多个部门,一个部门可以有多个成员(用户)。由于部门数量或多或少受到限制(与用户相比),我决定使用一种方式嵌入,如下所述:Two Way Embedding vs. One Way Embedding in MongoDB (Many-To-Many)。这就是 User 持有数组“memberOfDepartments”的原因,但 Department 不保存 Member 数组(因为缺少 @prop)。

第一个问题是,当我请求部门对象时,如何查询它的成员?该查询必须查找department._id 位于数组memberOfDepartments 中的用户。

我在这里尝试了多种东西,比如虚拟填充: https: //typegoose.github.io/typegoose/docs/api/virtuals/#virtual-populate,就像在department.model上这样:

@prop({
    ref: () => User,
    foreignField: 'memberOfDepartments', 
    localField: '_id', // compare this to the foreign document's value defined in "foreignField"
    justOne: false
})
public members: Ref<User>[];
Run Code Online (Sandbox Code Playgroud)

但它不会输出该属性。我的猜测是,这仅适用于一个站点上的一对多...我也尝试过 set/get 但在 DepartmentModel 中使用 UserModel 时遇到问题。

目前我在服务中这样做是“作弊”:

async findDepartmentById(id: string): Promise<Department> {
    const res = await this.departmentModel
        .findById(id)
        .populate({ path: 'supervisors', model: User })
        .lean()
        .exec();

    res.members = await this.userModel.find({ memberOfDepartments: res._id })
        .lean()
        .exec()

    if (!res) {
        throw new HttpException(
            'No Department with the id=' + id + ' found.',
            HttpStatus.NOT_FOUND,
        );
    }
    return res;
}
Run Code Online (Sandbox Code Playgroud)

..但我认为这不是正确的解决方案,因为我的猜测是它属于模型。

第二个问题是,我将如何处理删除一个部门,导致我必须删除对该部门的引用。在用户中?

我知道那里有 mongodb 和 mongoose 的文档,但我只是无法理解如何以“typegoose 方式”完成此操作,因为 typegoose 文档对我来说似乎非常有限。任何提示表示赞赏。

Ale*_*lex 8

所以,这并不容易找到,希望这个答案对其他人有所帮助。我仍然认为有必要记录更多基本内容 - 例如当对象被删除时删除对对象的引用。就像,任何有参考资料的人都需要这个,但在任何文档(typegoose、mongoose、mongodb)中都没有给出完整的示例。

答案1:

@prop({
    ref: () => User,
    foreignField: 'memberOfDepartments', 
    localField: '_id', // compare this to the foreign document's value defined in "foreignField"
    justOne: false
})
public members: Ref<User>[];
Run Code Online (Sandbox Code Playgroud)

正如问题中的那样,这是定义虚拟的正确方法。但我做错了什么,我认为并不是那么明显:我不得不打电话

.populate({ path: 'members', model: User })
Run Code Online (Sandbox Code Playgroud)

明确地如

 const res = await this.departmentModel
            .findById(id)
            .populate({ path: 'supervisors', model: User })
            .populate({ path: 'members', model: User })
            .lean()
            .exec();
Run Code Online (Sandbox Code Playgroud)

如果您不这样做,您将根本看不到属性成员。我对此遇到了问题,因为如果你在像主管这样的引用字段上执行此操作,你至少会得到一个数组 ob objectIds。但如果你不填充虚拟变量,你就根本得不到成员字段。

答案 2: 我的研究得出的结论是,最好的解决方案是使用预挂钩。基本上,您可以定义一个函数,在执行特定操作之前调用该函数(如果您想要在之后,请使用后挂钩)。就我而言,操作是“删除”,因为我想在删除文档本身之前删除引用。您可以使用此装饰器在 typegoose 中定义预挂钩,只需将其放在模型前面即可:

@pre<Department>('deleteOne', function (next) {
    const depId = this.getFilter()["_id"];
    getModelForClass(User).updateMany(
        { 'timeTrackingProfile.memberOfDepartments': depId },
        { $pull: { 'timeTrackingProfile.memberOfDepartments': depId } },
        { multi: true }
    ).exec();
    next();
})
export class Department {
[...]   
}
Run Code Online (Sandbox Code Playgroud)

在我的研究中发现的很多灵魂都使用了“删除”,当您调用 fe Departmentmodel.remove() 时,它会被调用。不要使用它,因为remove() 已被弃用。请改用“deleteOne()”。使用“const depId = this.getFilter()[“_id”];” 您可以访问要在操作中删除的文档的 ID。