typegoose / mongoose

mongoDB数据库在nodejs下最佳拍档

Typegoose是一个开源的TypeScript库,它使开发人员可以使用TypeScript类定义Mongoose模型。Mongoose是MongoDB的流行对象文档映射(ODM)库,MongoDB是一个NoSQL文档导向的数据库。Typegoose通过允许开发人员使用TypeScript装饰器来定义属性、方法和模式选项,简化了Mongoose模型的创建。这使得在TypeScript项目中使用MongoDB和Mongoose编写类型安全和可维护的代码变得更加容易。Typegoose可在npm上获取,并由社区贡献者积极维护。

使用npm安装它们

node -v
v18.14.2


npm install -g typescript 
ts-node -v                                                                                                                   
v10.9.1

mkdir typegoose-practice && cd typegoose-practice && npm init  
npm install node @types/node mongoose @typegoose/typegoose ts-node 
const mongoose = require('mongoose');
const DB_URL = 'mongodb://localhost:27017/mydatabase'

mongoose.connect(DB_URL)
.then(() => {
  console.log(`Connected to database: ${mongoose.connection.name}`);
})
.catch((err) => console.error('Error connecting to database', err));

mongoose 连接带密码的MongoDB

要连接带有密码的MongoDB数据库,您需要使用MongoDB驱动程序并提供正确的认证凭据。以下是使用Mongoose连接带有密码的MongoDB数据库的基本步骤

use mydatabase
db.createUser(
  {
    user: "myuser",
    pwd: "mypassword",
    roles: [ { role: "readWrite", db: "mydatabase" } ]
  }
)

const DB_URL = 'mongodb://myuser:mypassword@xiongmingcai.mongodb.rds.aliyuncs.com:27017/mydatabase'

其中username和password是您MongoDB数据库的凭据,xiongmingcai.mongodb.rds.aliyuncs.com是MongoDB数据库的主机名,27017是MongoDB数据库的端口号,mydatabase是要连接的数据库的名称。

创建模型

import {getModelForClass, ModelOptions, prop} from "@typegoose/typegoose";
import {Model} from "mongoose";

@ModelOptions({schemaOptions:{collection:"users"}})
export class UserClass{
    @prop({required:true})
    username:string

}

export const UserModel = getModelForClass(UserClass)

const createUsers = async () => {
    // method 1
    await UserModel.create({username: "Conny"})
    // method 2
    const user = new UserModel({username: "Adrian"})
    await user.save();
}

Subdocument 子文档


@modelOptions({ schemaOptions: { _id: false } })
class Job{
    @prop()
    title:string
    @prop()
    company:string
}

@ModelOptions({schemaOptions:{collection:"users"}})
export class UserClass{
    @prop({required:true})
    username:string

    @prop()
    job:Job //关联子文档
}

添加索引

......
@index({username:1,"job.company":1}) //添加两条索引
@ModelOptions({schemaOptions:{collection:"users"}})
export class UserClass{
    @prop({required:true})
    username:string
......    
    

pre and post hooks

可以使用@pre()装饰器在Typegoose模型中定义一个middleware,在执行更新操作之前自动将title属性转换为大写。


@Pre<PostClass>('save', function (next: () => void) {
    this.title = this.title.toUpperCase();
    next()
})
@modelOptions({schemaOptions: {collection: 'posts', timestamps: true}})
class PostClass {
    @prop({required: true})
    title!: string

在上面的示例中,我们在PostClass模型上使用了@pre(),并指定在执行findOneAndUpdate操作之前需要执行的操作。在这个例子中,我们判断更新操作是否包含title属性,如果包含,则将它转换为大写形式。最后,我们调用next()函数来继续执行更新操作。

通过使用@pre(),我们可以为Typegoose模型添加更加灵活的middleware,实现更加复杂的数据操作。

Typegoose Validation 数据验证

@prop({
        validate:{
            validator:(email:string)=>{
                return validateEmail(email)
            },
            message:"请填写正确的邮箱地址"
        }
    })
    email:String
function validateEmail(email:string) {
    const regex = /^([a-zA-Z0-9_\-.]+)@([a-zA-Z0-9_\-.]+)\.([a-zA-Z]{2,5})$/;
    const isEmail = regex.test(email);
    console.log(isEmail);
    return isEmail;
}
const updateUser = async () => {
    const filter = {username: "Conny"};
    // 方式一
    // const update = {email: "xmc000@qq.com"};
    // const enableValidation = {runValidators:true};
    // await UserModel.findOneAndUpdate(filter,update,enableValidation)
    //方式二
    const user = await UserModel.findOne(filter).exec();
    if (!user) return
    user.email = "1026911109@qq.com"
    await user.save()

}

Typegoose Virtual


const getVirtual = async () => {
    const filter = {username: "Conny"};
    const user = await UserModel.findOne(filter).exec();
    console.log(user?.summary);
}
const strVirtual = async () => {
    const filter = {username: "Conny"};
    const user = await UserModel.findOne(filter).exec();
    if (!user) return
    // @ts-ignore
    user?.summary = "Conny, developer, bighead"
    await user?.save()
}
export class UserClass{
    ......
    get summary(){
        return this.username+", "+this.job.title+", "+this.job.company
    }
    set summary(full){
       const [username,title,company]  = full.split(",")
        this.username = username.trim()
        this.job.title = title.trim()
        this.job.company = company.trim()
    }
    ......
}    

测试

const getVirtual = async () => {
    const filter = {username: "Conny"};
    const user = await UserModel.findOne(filter).exec();
    console.log(user?.summary);
}
const strVirtual = async () => {
    const filter = {username: "Conny"};
    const user = await UserModel.findOne(filter).exec();
    if (!user) return
    // @ts-ignore
    user?.summary = "Conny, developer, bighead"
    await user?.save()
}

Typegoose instance Method

......
export class UserClass{
    @prop({required:true})
    public username!: string;

    @prop()
    password!: string;
    comparePassword(this: DocumentType<UserClass>, password: string) {
      return   this.password == password
    }
    ......    

测试

const instanceMethod = async () => {
    const find = await UserModel.findOne({username: "Conny"}).exec();

    const isPasswordMatch = find?.comparePassword("pw1");
    console.log("isPasswordMatch: "+ isPasswordMatch); // true
}

Typegoose 使用 select 隐藏敏感信息(如密码)

export class UserClass{
    @prop({ select: false }) 
    password!: string;

字段标记为select: false,这表示在默认情况下不会将这些字段包含在查询结果中。

    const find = await UserModel.findOne({username: "Conny"}).select('+password').exec();
 const isPasswordMatch = find?.comparePassword("pw1");
 console.log("isPasswordMatch: "+ isPasswordMatch); // true

在这里,我们使用select修饰符来选择要在查询结果中包含的字段。注意,在这里我们使用+号将password字段标记为要选择的字段。现在,当我们查询用户时,我们将包括这些字段,并且可以使用comparePassword方法来验证密码。

Typegoose instance Method

export class UserClass{
......
    comparePassword(this: DocumentType<UserClass>, password: string) {
      return   this.password == password
    }

......

使用

const instanceMethod = async () => {
    const find = await UserModel.findOne({username: "Conny"}).exec();

    const isPasswordMatch = find?.comparePassword("pw1");
    console.log("isPasswordMatch: "+ isPasswordMatch); // true
}

Typegoose static Method

export class UserClass{
......
    static async getPostByUsername(
        this: ReturnModelType<typeof User>,
        username: string
    ) {
        const user = await UserModel.findOne({ username }).exec()

        if (!user) return

        return await PostModel.find({ author: user.id }).populate("author").exec()
    }

使用


const staticMethod = async () => {
    const res = await UserModel.getPostByUsername("Conny")
    console.log(inspect(res, false,null,true));
}