Mongoose the stylescript ...?


90

Đang cố gắng triển khai mô hình Mongoose trong Typescript. Google chỉ tiết lộ một cách tiếp cận kết hợp (kết hợp JS và TS). Làm thế nào để triển khai lớp Người dùng, theo cách tiếp cận khá ngây thơ của tôi, nếu không có JS?

Muốn có thể IUserModel mà không cần hành lý.

import {IUser} from './user.ts';
import {Document, Schema, Model} from 'mongoose';

// mixing in a couple of interfaces
interface IUserDocument extends IUser,  Document {}

// mongoose, why oh why '[String]' 
// TODO: investigate out why mongoose needs its own data types
let userSchema: Schema = new Schema({
  userName  : String,
  password  : String,
  firstName : String,
  lastName  : String,
  email     : String,
  activated : Boolean,
  roles     : [String]
});

// interface we want to code to?
export interface IUserModel extends Model<IUserDocument> {/* any custom methods here */}

// stumped here
export class User {
  constructor() {}
}

Userkhông thể là một lớp vì việc tạo một lớp là một hoạt động không đồng bộ. Nó phải trả lại một lời hứa nên bạn phải gọi User.create({...}).then....
Louay Alakkad

1
Cụ thể, được đưa ra trong mã trong OP, bạn có thể vui lòng giải thích tại sao Userkhông thể là một lớp?
Tim McNamara

Thay vào đó, hãy thử github.com/typeorm/typeorm .
Erich

@Erich họ nói rằng typeorm không hoạt động tốt với MongoDB, có lẽ Type ngỗng là một lựa chọn tốt
PayamBeirami

Hãy xem điều này tại npmjs.com/package/@types/mongoose
Harry

Câu trả lời:


130

Đây là cách tôi làm điều đó:

export interface IUser extends mongoose.Document {
  name: string; 
  somethingElse?: number; 
};

export const UserSchema = new mongoose.Schema({
  name: {type:String, required: true},
  somethingElse: Number,
});

const User = mongoose.model<IUser>('User', UserSchema);
export default User;

2
xin lỗi, nhưng 'mongoose' được định nghĩa như thế nào trong TS?
Tim McNamara

13
import * as mongoose from 'mongoose';hoặcimport mongoose = require('mongoose');
Louay Alakkad

1
Một cái gì đó như thế này:import User from '~/models/user'; User.find(/*...*/).then(/*...*/);
Louay Alakkad

3
Dòng cuối cùng (xuất mặc định const Người dùng ...) không hoạt động đối với tôi. Tôi cần phải tách dòng, như được đề xuất trong stackoverflow.com/questions/35821614/…
Sergio

7
Tôi có thể làm let newUser = new User({ iAmNotHere: true })mà không có bất kỳ lỗi nào trong IDE hoặc khi biên dịch. Vậy lý do tạo giao diện là gì?
Lupurus

33

Một giải pháp thay thế khác nếu bạn muốn tách các định nghĩa kiểu của mình và việc triển khai cơ sở dữ liệu.

import {IUser} from './user.ts';
import * as mongoose from 'mongoose';

type UserType = IUser & mongoose.Document;
const User = mongoose.model<UserType>('User', new mongoose.Schema({
    userName  : String,
    password  : String,
    /* etc */
}));

Nguồn cảm hứng từ đây: https://github.com/Appsilon/styleguide/wiki/mongoose-typescript-models


1
Liệu mongoose.Schemađịnh nghĩa ở đây lặp lại trong các lĩnh vực từ IUser? Với điều kiện IUserđược xác định trong một tệp khác , nguy cơ các trường không đồng bộ khi dự án phát triển về độ phức tạp và số lượng nhà phát triển là khá cao.
Dan Dascalescu

Vâng, đây là một lập luận xác đáng đáng xem xét. Tuy nhiên, sử dụng các thử nghiệm tích hợp thành phần có thể giúp giảm thiểu rủi ro. Và lưu ý rằng có những cách tiếp cận và kiến ​​trúc nơi khai báo kiểu và triển khai DB được tách biệt cho dù nó được thực hiện thông qua ORM (như bạn đề xuất) hay thủ công (như trong câu trả lời này). Không có viên đạn bạc nào ... <(°. °)>
Gábor Imre

Một dấu đầu dòng có thể là tạo mã từ định nghĩa GraphQL, cho TypeScript và mongoose.
Dan Dascalescu

23

Xin lỗi vì đã đăng tải nhưng điều này vẫn có thể thú vị đối với ai đó. Tôi nghĩ Typegoose cung cấp cách xác định mô hình hiện đại và thanh lịch hơn

Đây là một ví dụ từ tài liệu:

import { prop, Typegoose, ModelType, InstanceType } from 'typegoose';
import * as mongoose from 'mongoose';

mongoose.connect('mongodb://localhost:27017/test');

class User extends Typegoose {
    @prop()
    name?: string;
}

const UserModel = new User().getModelForClass(User);

// UserModel is a regular Mongoose Model with correct types
(async () => {
    const u = new UserModel({ name: 'JohnDoe' });
    await u.save();
    const user = await UserModel.findOne();

    // prints { _id: 59218f686409d670a97e53e0, name: 'JohnDoe', __v: 0 }
    console.log(user);
})();

Đối với tình huống kết nối hiện có, bạn có thể sử dụng như sau (có thể xảy ra nhiều hơn trong các tình huống thực tế và được khám phá trong tài liệu):

import { prop, Typegoose, ModelType, InstanceType } from 'typegoose';
import * as mongoose from 'mongoose';

const conn = mongoose.createConnection('mongodb://localhost:27017/test');

class User extends Typegoose {
    @prop()
    name?: string;
}

// Notice that the collection name will be 'users':
const UserModel = new User().getModelForClass(User, {existingConnection: conn});

// UserModel is a regular Mongoose Model with correct types
(async () => {
    const u = new UserModel({ name: 'JohnDoe' });
    await u.save();
    const user = await UserModel.findOne();

    // prints { _id: 59218f686409d670a97e53e0, name: 'JohnDoe', __v: 0 }
    console.log(user);
})();

8
Tôi cũng đã đi đến kết luận này, nhưng lo lắng rằng typegoosekhông có đủ hỗ trợ ... kiểm tra thống kê npm của họ, nó chỉ có 3 nghìn lượt tải xuống hàng tuần và rn có gần 100 vấn đề Github đang mở, hầu hết trong số đó không có nhận xét, và một số trong đó trông giống như họ nên đã bị đóng cửa từ lâu rồi
Corbfon

@Corbfon Bạn đã thử chưa? Nếu vậy, phát hiện của bạn là gì? Nếu không, có điều gì khác khiến bạn quyết định không sử dụng nó không? Tôi thường nhìn thấy một số người lo lắng về sự ủng hộ hoàn toàn, nhưng có vẻ như những người thực sự sử dụng nó khá hài lòng với nó
N4ppeL

1
@ N4ppeL tôi sẽ không đi với typegoose- chúng tôi đã kết thúc bằng tay xử lý gõ của chúng tôi, tương tự như bài này , có vẻ như ts-mongoosethể có một số lời hứa (như đề xuất trong câu trả lời sau)
Corbfon

1
Không bao giờ xin lỗi vì "phân bổ". [Như bạn đã biết ...] Thậm chí còn có một huy hiệu (mặc dù được đặt tên là Necromancer ; ^ D) để làm điều này! Khuyến khích đăng tải thông tin và ý tưởng mới!
ruffin

1
@ruffin: Tôi cũng thực sự không hiểu sự kỳ thị đối với việc đăng các giải pháp mới và cập nhật cho các vấn đề.
Dan Dascalescu

16

Cố gắng ts-mongoose. Nó sử dụng các loại điều kiện để thực hiện ánh xạ.

import { createSchema, Type, typedModel } from 'ts-mongoose';

const UserSchema = createSchema({
  username: Type.string(),
  email: Type.string(),
});

const User = typedModel('User', UserSchema);

1
Có vẻ rất hứa hẹn! Cám ơn vì đã chia sẻ! :)
Boriel

1
Chà. Khóa này rất đẹp. Nhìn về phía trước để cung cấp cho nó một thử!
qqilihq

1
Tiết lộ: ts-mongoose dường như được tạo ra bởi bầu trời. Có vẻ là giải pháp bóng bẩy nhất hiện có.
mic


11

Hầu hết các câu trả lời ở đây lặp lại các trường trong lớp / giao diện TypeScript và trong lược đồ mongoose. Không có một nguồn chân lý duy nhất thể hiện rủi ro bảo trì, vì dự án trở nên phức tạp hơn và nhiều nhà phát triển làm việc hơn: các trường có nhiều khả năng không đồng bộ . Điều này đặc biệt tệ khi lớp nằm trong một tệp khác với giản đồ mongoose.

Để giữ cho các trường được đồng bộ hóa, bạn nên xác định chúng một lần. Có một số thư viện làm điều này:

Tôi vẫn chưa hoàn toàn bị thuyết phục bởi bất kỳ ai trong số họ nhưng typegoose dường như được duy trì tích cực và nhà phát triển đã chấp nhận các PR của tôi.

Hãy suy nghĩ trước một bước: khi bạn thêm lược đồ GraphQL vào hỗn hợp, một lớp sao chép mô hình khác sẽ xuất hiện. Một cách để khắc phục sự cố này có thể là tạo mã TypeScript và mongoose từ lược đồ GraphQL.


5

Đây là một cách được đánh máy mạnh để khớp một mô hình đơn giản với một giản đồ mongoose. Trình biên dịch sẽ đảm bảo các định nghĩa được chuyển đến mongoose.Schema khớp với giao diện. Sau khi có lược đồ, bạn có thể sử dụng

thông thường

export type IsRequired<T> =
  undefined extends T
  ? false
  : true;

export type FieldType<T> =
  T extends number ? typeof Number :
  T extends string ? typeof String :
  Object;

export type Field<T> = {
  type: FieldType<T>,
  required: IsRequired<T>,
  enum?: Array<T>
};

export type ModelDefinition<M> = {
  [P in keyof M]-?:
    M[P] extends Array<infer U> ? Array<Field<U>> :
    Field<M[P]>
};

user.ts

import * as mongoose from 'mongoose';
import { ModelDefinition } from "./common";

interface User {
  userName  : string,
  password  : string,
  firstName : string,
  lastName  : string,
  email     : string,
  activated : boolean,
  roles     : Array<string>
}

// The typings above expect the more verbose type definitions,
// but this has the benefit of being able to match required
// and optional fields with the corresponding definition.
// TBD: There may be a way to support both types.
const definition: ModelDefinition<User> = {
  userName  : { type: String, required: true },
  password  : { type: String, required: true },
  firstName : { type: String, required: true },
  lastName  : { type: String, required: true },
  email     : { type: String, required: true },
  activated : { type: Boolean, required: true },
  roles     : [ { type: String, required: true } ]
};

const schema = new mongoose.Schema(
  definition
);

Khi bạn có lược đồ của mình, bạn có thể sử dụng các phương pháp được đề cập trong các câu trả lời khác, chẳng hạn như

const userModel = mongoose.model<User & mongoose.Document>('User', schema);

1
Đây là câu trả lời đúng duy nhất. Không có câu trả lời nào khác thực sự đảm bảo khả năng tương thích kiểu giữa lược đồ và kiểu / giao diện.
Jamie Strauss


1
@DanDascalescu Tôi không nghĩ bạn hiểu cách hoạt động của các loại.
Jamie Strauss

5

Chỉ cần thêm một cách khác ( @types/mongoosephải được cài đặt với npm install --save-dev @types/mongoose)

import { IUser } from './user.ts';
import * as mongoose from 'mongoose';

interface IUserModel extends IUser, mongoose.Document {}

const User = mongoose.model<IUserModel>('User', new mongoose.Schema({
    userName: String,
    password: String,
    // ...
}));

Và sự khác biệt giữa interfacetype, vui lòng đọc câu trả lời này

Cách này có một ưu điểm, bạn có thể thêm các kiểu gõ theo phương thức tĩnh Mongoose:

interface IUserModel extends IUser, mongoose.Document {
  generateJwt: () => string
}

bạn đã xác định ở generateJwtđâu?
rels

1
const User = mongoose.model.... password: String, generateJwt: () => { return someJwt; } }));Về cơ bản, @rels generateJwttrở thành một thuộc tính khác của mô hình.
a11smiles

Bạn sẽ chỉ thêm nó dưới dạng một phương thức theo cách này hay bạn sẽ kết nối nó với thuộc tính phương thức?
user1790300

1
Đây phải là câu trả lời được chấp nhận vì Nó tách rời định nghĩa người dùng và DAL của người dùng. Nếu bạn muốn chuyển từ mongo sang nhà cung cấp db khác, bạn sẽ không phải thay đổi giao diện người dùng.
Rafael del Rio

1
@RafaeldelRio: câu hỏi về cách sử dụng mongoose với TypeScript. Chuyển sang DB khác là trái với mục tiêu này. Và vấn đề với việc tách định nghĩa lược đồ khỏi IUserkhai báo giao diện trong một tệp khácnguy cơ các trường không đồng bộ khi dự án phát triển với số lượng phức tạp và nhà phát triển là khá cao.
Dan Dascalescu

4

Đây là cách các nhân viên tại Microsoft làm điều đó. đây

import mongoose from "mongoose";

export type UserDocument = mongoose.Document & {
    email: string;
    password: string;
    passwordResetToken: string;
    passwordResetExpires: Date;
...
};

const userSchema = new mongoose.Schema({
    email: { type: String, unique: true },
    password: String,
    passwordResetToken: String,
    passwordResetExpires: Date,
...
}, { timestamps: true });

export const User = mongoose.model<UserDocument>("User", userSchema);

Tôi khuyên bạn nên kiểm tra dự án khởi động tuyệt vời này khi bạn thêm TypeScript vào dự án Node của mình.

https://github.com/microsoft/TypeScript-Node-Starter


1
Điều đó trùng lặp mọi trường giữa mongoose và TypeScript, điều này tạo ra rủi ro bảo trì khi mô hình trở nên phức tạp hơn. Các giải pháp thích ts-mongoosetypegoosegiải quyết vấn đề đó, mặc dù phải thừa nhận là có khá nhiều rắc rối về cú pháp.
Dan Dascalescu

2

Với điều này vscode intellisensehoạt động trên cả hai

  • Loại người dùng User.findOne
  • cá thể người dùng u1._id

Mật mã:

// imports
import { ObjectID } from 'mongodb'
import { Document, model, Schema, SchemaDefinition } from 'mongoose'

import { authSchema, IAuthSchema } from './userAuth'

// the model

export interface IUser {
  _id: ObjectID, // !WARNING: No default value in Schema
  auth: IAuthSchema
}

// IUser will act like it is a Schema, it is more common to use this
// For example you can use this type at passport.serialize
export type IUserSchema = IUser & SchemaDefinition
// IUser will act like it is a Document
export type IUserDocument = IUser & Document

export const userSchema = new Schema<IUserSchema>({
  auth: {
    required: true,
    type: authSchema,
  }
})

export default model<IUserDocument>('user', userSchema)


2

Đây là ví dụ từ tài liệu Mongoose, Tạo từ các lớp ES6 Sử dụng loadClass () , được chuyển đổi thành TypeScript:

import { Document, Schema, Model, model } from 'mongoose';
import * as assert from 'assert';

const schema = new Schema<IPerson>({ firstName: String, lastName: String });

export interface IPerson extends Document {
  firstName: string;
  lastName: string;
  fullName: string;
}

class PersonClass extends Model {
  firstName!: string;
  lastName!: string;

  // `fullName` becomes a virtual
  get fullName() {
    return `${this.firstName} ${this.lastName}`;
  }

  set fullName(v) {
    const firstSpace = v.indexOf(' ');
    this.firstName = v.split(' ')[0];
    this.lastName = firstSpace === -1 ? '' : v.substr(firstSpace + 1);
  }

  // `getFullName()` becomes a document method
  getFullName() {
    return `${this.firstName} ${this.lastName}`;
  }

  // `findByFullName()` becomes a static
  static findByFullName(name: string) {
    const firstSpace = name.indexOf(' ');
    const firstName = name.split(' ')[0];
    const lastName = firstSpace === -1 ? '' : name.substr(firstSpace + 1);
    return this.findOne({ firstName, lastName });
  }
}

schema.loadClass(PersonClass);
const Person = model<IPerson>('Person', schema);

(async () => {
  let doc = await Person.create({ firstName: 'Jon', lastName: 'Snow' });
  assert.equal(doc.fullName, 'Jon Snow');
  doc.fullName = 'Jon Stark';
  assert.equal(doc.firstName, 'Jon');
  assert.equal(doc.lastName, 'Stark');

  doc = (<any>Person).findByFullName('Jon Snow');
  assert.equal(doc.fullName, 'Jon Snow');
})();

Đối với findByFullNamephương thức static , tôi không thể tìm cách lấy thông tin kiểu Person, vì vậy tôi phải ép kiểu <any>Personkhi tôi muốn gọi nó. Nếu bạn biết cách khắc phục điều đó, xin vui lòng thêm một bình luận.


Giống như các câu trả lời khác , cách tiếp cận này sao chép các trường giữa giao diện và lược đồ. Điều đó có thể tránh được bằng cách có một nguồn sự thật duy nhất, ví dụ bằng cách sử dụng ts-mongoosehoặc typegoose. Tình hình càng bị trùng lặp khi xác định lược đồ GraphQL.
Dan Dascalescu

Bất kỳ cách nào để xác định giới thiệu với cách tiếp cận này?
Dan Dascalescu

1

Tôi là một người hâm mộ của Plumier, nó có trình trợ giúp mongoose , nhưng nó có thể được sử dụng độc lập mà không cần chính Plumier . Không giống như Typegoose, nó đi theo một con đường khác bằng cách sử dụng thư viện phản chiếu chuyên dụng của Plumier, giúp bạn có thể sử dụng những thứ thú vị.

Đặc trưng

  1. POJO thuần túy (miền không cần kế thừa từ bất kỳ lớp nào, cũng không sử dụng bất kỳ kiểu dữ liệu đặc biệt nào), Mô hình được tạo tự động được suy ra T & Documentđể có thể truy cập các thuộc tính liên quan đến tài liệu.
  2. Các thuộc tính tham số TypeScript được hỗ trợ, rất tốt khi bạn có strict:truecấu hình tsconfig. Và với thuộc tính tham số không yêu cầu decorator trên tất cả các thuộc tính.
  3. Các thuộc tính trường được hỗ trợ như Typegoose
  4. Cấu hình giống như mongoose nên bạn sẽ dễ dàng làm quen với nó.
  5. Kế thừa được hỗ trợ làm cho việc lập trình tự nhiên hơn.
  6. Phân tích mô hình, hiển thị tên mô hình và tên bộ sưu tập thích hợp, cấu hình được áp dụng, v.v.

Sử dụng

import model, {collection} from "@plumier/mongoose"


@collection({ timestamps: true, toJson: { virtuals: true } })
class Domain {
    constructor(
        public createdAt?: Date,
        public updatedAt?: Date,
        @collection.property({ default: false })
        public deleted?: boolean
    ) { }
}

@collection()
class User extends Domain {
    constructor(
        @collection.property({ unique: true })
        public email: string,
        public password: string,
        public firstName: string,
        public lastName: string,
        public dateOfBirth: string,
        public gender: string
    ) { super() }
}

// create mongoose model (can be called multiple time)
const UserModel = model(User)
const user = await UserModel.findById()

1

Đối với bất kỳ ai đang tìm kiếm giải pháp cho các dự án Mongoose hiện có:

Gần đây, chúng tôi đã xây dựng mongoose-tsgen để giải quyết vấn đề này (rất mong một số phản hồi!). Các giải pháp hiện tại như typegoose yêu cầu viết lại toàn bộ lược đồ của chúng tôi và đưa ra nhiều điểm không tương thích khác nhau. mongoose-tsgen là một công cụ CLI đơn giản tạo tệp index.d.ts có chứa các giao diện Typecript cho tất cả các lược đồ Mongoose của bạn; nó yêu cầu ít hoặc không cần cấu hình và tích hợp rất trơn tru với bất kỳ dự án Typecript nào.


1

Nếu bạn muốn đảm bảo rằng lược đồ của mình đáp ứng loại mô hình và ngược lại, giải pháp này cung cấp khả năng nhập tốt hơn những gì @bingles đã đề xuất:

Tệp loại phổ biến: ToSchema.ts(Đừng hoảng sợ! Chỉ cần sao chép và dán nó)

import { Document, Schema, SchemaType, SchemaTypeOpts } from 'mongoose';

type NonOptionalKeys<T> = { [k in keyof T]-?: undefined extends T[k] ? never : k }[keyof T];
type OptionalKeys<T> = Exclude<keyof T, NonOptionalKeys<T>>;
type NoDocument<T> = Exclude<T, keyof Document>;
type ForceNotRequired = Omit<SchemaTypeOpts<any>, 'required'> & { required?: false };
type ForceRequired = Omit<SchemaTypeOpts<any>, 'required'> & { required: SchemaTypeOpts<any>['required'] };

export type ToSchema<T> = Record<NoDocument<NonOptionalKeys<T>>, ForceRequired | Schema | SchemaType> &
   Record<NoDocument<OptionalKeys<T>>, ForceNotRequired | Schema | SchemaType>;

và một mô hình ví dụ:

import { Document, model, Schema } from 'mongoose';
import { ToSchema } from './ToSchema';

export interface IUser extends Document {
   name?: string;
   surname?: string;
   email: string;
   birthDate?: Date;
   lastLogin?: Date;
}

const userSchemaDefinition: ToSchema<IUser> = {
   surname: String,
   lastLogin: Date,
   role: String, // Error, 'role' does not exist
   name: { type: String, required: true, unique: true }, // Error, name is optional! remove 'required'
   email: String, // Error, property 'required' is missing
   // email: {type: String, required: true}, // correct 👍
   // Error, 'birthDate' is not defined
};

const userSchema = new Schema(userSchemaDefinition);

export const User = model<IUser>('User', userSchema);



0

Đây là một ví dụ dựa trên README cho @types/mongoosegói.

Bên cạnh các phần tử đã được bao gồm ở trên, nó cho thấy cách bao gồm các phương thức thông thường và tĩnh:

import { Document, model, Model, Schema } from "mongoose";

interface IUserDocument extends Document {
  name: string;
  method1: () => string;
}
interface IUserModel extends Model<IUserDocument> {
  static1: () => string;
}

var UserSchema = new Schema<IUserDocument & IUserModel>({
  name: String
});

UserSchema.methods.method1 = function() {
  return this.name;
};
UserSchema.statics.static1 = function() {
  return "";
};

var UserModel: IUserModel = model<IUserDocument, IUserModel>(
  "User",
  UserSchema
);
UserModel.static1(); // static methods are available

var user = new UserModel({ name: "Success" });
user.method1();

Nói chung, README này dường như là một nguồn tài nguyên tuyệt vời để tiếp cận các loại với mongoose.


Cách tiếp cận này trùng lặp định nghĩa của mọi trường từ bên IUserDocumenttrong UserSchema, điều này tạo ra rủi ro bảo trì khi mô hình trở nên phức tạp hơn. Các gói thích ts-mongoosetypegoosecố gắng giải quyết vấn đề đó, mặc dù phải thừa nhận là có khá nhiều rắc rối về cú pháp.
Dan Dascalescu
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.