阿里云主机折上折
  • 微信号
Current Site:Index > The use of virtual properties (Virtuals)

The use of virtual properties (Virtuals)

Author:Chuan Chen 阅读数:5969人阅读 分类: MongoDB

The Concept of Virtual Properties

Virtual properties are a special type of property in Mongoose that are not persisted to the MongoDB database but are dynamically computed at runtime. Virtual properties are typically used to combine or transform existing fields in a document or to calculate derived values based on other properties of the document. They provide a flexible way to extend document functionality without actually modifying the database structure.

There are two main types of virtual properties:

  1. Getter virtual properties: Used to compute and return a value
  2. Setter virtual properties: Used to decompose and set values to other fields
const userSchema = new mongoose.Schema({
  firstName: String,
  lastName: String
});

// Define a getter virtual property
userSchema.virtual('fullName').get(function() {
  return `${this.firstName} ${this.lastName}`;
});

// Define a setter virtual property
userSchema.virtual('fullName').set(function(name) {
  const split = name.split(' ');
  this.firstName = split[0];
  this.lastName = split[1];
});

Basic Usage of Virtual Properties

Defining virtual properties is very simple—just call the virtual() method on the schema. Within the getter function of a virtual property, you can access the current document instance via this.

const productSchema = new mongoose.Schema({
  name: String,
  price: Number,
  taxRate: Number
});

// Virtual property to calculate the price including tax
productSchema.virtual('priceWithTax').get(function() {
  return this.price * (1 + this.taxRate / 100);
});

const Product = mongoose.model('Product', productSchema);
const laptop = new Product({ name: 'Laptop', price: 1000, taxRate: 20 });

console.log(laptop.priceWithTax); // Output: 1200

Virtual properties are not included in query results by default unless explicitly specified. You can include virtual properties by setting the toJSON and toObject options:

productSchema.set('toJSON', { virtuals: true });
productSchema.set('toObject', { virtuals: true });

Advanced Usage of Virtual Properties

1. Relationship-Based Virtual Properties

Virtual properties are particularly useful for handling relationships between documents. For example, in a blog system, we can define a virtual property for the post model to retrieve the number of comments:

const postSchema = new mongoose.Schema({
  title: String,
  content: String
});

const commentSchema = new mongoose.Schema({
  content: String,
  post: { type: mongoose.Schema.Types.ObjectId, ref: 'Post' }
});

postSchema.virtual('comments', {
  ref: 'Comment',
  localField: '_id',
  foreignField: 'post'
});

postSchema.virtual('commentCount', {
  ref: 'Comment',
  localField: '_id',
  foreignField: 'post',
  count: true
});

const Post = mongoose.model('Post', postSchema);
const Comment = mongoose.model('Comment', commentSchema);

// Use populate to fetch virtual properties
Post.findOne().populate('comments').populate('commentCount').exec((err, post) => {
  console.log(post.comments); // Array of comments
  console.log(post.commentCount); // Number of comments
});

2. Conditional Virtual Properties

Virtual properties can dynamically compute different values based on other fields in the document:

const orderSchema = new mongoose.Schema({
  items: [{
    product: String,
    quantity: Number,
    price: Number
  }],
  discount: Number
});

orderSchema.virtual('total').get(function() {
  const subtotal = this.items.reduce((sum, item) => sum + (item.price * item.quantity), 0);
  return subtotal - (subtotal * (this.discount || 0) / 100);
});

orderSchema.virtual('hasDiscount').get(function() {
  return this.discount > 0;
});

3. Chaining Virtual Properties

You can define both getter and setter for a single virtual property:

const personSchema = new mongoose.Schema({
  firstName: String,
  lastName: String,
  birthDate: Date
});

personSchema.virtual('age')
  .get(function() {
    const diff = Date.now() - this.birthDate.getTime();
    return Math.floor(diff / (1000 * 60 * 60 * 24 * 365.25));
  })
  .set(function(age) {
    const now = new Date();
    this.birthDate = new Date(now.getFullYear() - age, now.getMonth(), now.getDate());
  });

const person = new Person({ firstName: 'John', lastName: 'Doe' });
person.age = 30; // Setting the virtual property automatically calculates birthDate
console.log(person.birthDate); // Shows the date 30 years ago

Performance Considerations for Virtual Properties

While virtual properties are very useful, there are performance considerations to keep in mind:

  1. Computation Overhead: Complex virtual property calculations may impact performance, especially when processing large numbers of documents.
  2. Query Limitations: Virtual properties cannot be used in query conditions because they don't exist in the database.
  3. Indexing: Virtual properties cannot be indexed since they are not database fields.

For performance-sensitive scenarios, consider:

  • Precomputing and storing frequently used values
  • Using Mongoose middleware to update related fields when saving documents
  • Using aggregation pipelines for complex calculations
// Use pre-save hook to precompute and store virtual property values
personSchema.pre('save', function(next) {
  if (this.isModified('birthDate')) {
    const diff = Date.now() - this.birthDate.getTime();
    this.age = Math.floor(diff / (1000 * 60 * 60 * 24 * 365.25));
  }
  next();
});

Choosing Between Virtual Properties and Instance Methods

Both virtual properties and instance methods can extend document functionality, but they are suited for different scenarios:

Feature Virtual Properties Instance Methods
Syntax Accessed like properties Called like functions
Parameters Cannot accept parameters Can accept parameters
Purpose Compute derived values Perform operations or complex calculations
Caching Can cache results Executes calculation on each call

Example comparison:

// Virtual property implementation
userSchema.virtual('isAdult').get(function() {
  return this.age >= 18;
});

// Instance method implementation
userSchema.methods.isAdult = function(minAge = 18) {
  return this.age >= minAge;
};

// Usage
console.log(user.isAdult); // Virtual property
console.log(user.isAdult(21)); // Instance method, can pass different parameters

Practical Use Cases for Virtual Properties

1. Formatting Output

Virtual properties are often used to format document data for display:

const invoiceSchema = new mongoose.Schema({
  number: Number,
  date: Date,
  amount: Number,
  currency: String
});

invoiceSchema.virtual('formattedAmount').get(function() {
  return new Intl.NumberFormat('en-US', {
    style: 'currency',
    currency: this.currency
  }).format(this.amount);
});

invoiceSchema.virtual('formattedDate').get(function() {
  return this.date.toLocaleDateString('en-US', {
    year: 'numeric',
    month: 'long',
    day: 'numeric'
  });
});

2. Permission Control

Virtual properties can be used to dynamically show or hide data based on user permissions:

const documentSchema = new mongoose.Schema({
  title: String,
  content: String,
  isPrivate: Boolean,
  owner: { type: mongoose.Schema.Types.ObjectId, ref: 'User' }
});

documentSchema.virtual('safeContent').get(function() {
  if (this.isPrivate && !this.userHasAccess) {
    return 'This content is private';
  }
  return this.content;
});

// Set userHasAccess flag in the controller
doc.userHasAccess = checkUserAccess(user, doc);

3. Data Validation

Virtual properties can be used for complex data validation:

const reservationSchema = new mongoose.Schema({
  startDate: Date,
  endDate: Date,
  room: { type: mongoose.Schema.Types.ObjectId, ref: 'Room' }
});

reservationSchema.virtual('duration').get(function() {
  return (this.endDate - this.startDate) / (1000 * 60 * 60 * 24);
});

reservationSchema.virtual('isValid').get(function() {
  return this.duration > 0 && this.duration <= 14; // No more than two weeks
});

// Check before saving
reservationSchema.pre('save', function(next) {
  if (!this.isValid) {
    throw new Error('Invalid reservation duration');
  }
  next();
});

Testing Virtual Properties

Testing virtual properties is similar to testing regular properties, but note that virtual properties are dynamically computed:

describe('User Model', () => {
  it('should compute fullName virtual property', () => {
    const user = new User({ firstName: 'John', lastName: 'Doe' });
    expect(user.fullName).to.equal('John Doe');
  });

  it('should split fullName virtual property', () => {
    const user = new User();
    user.fullName = 'Jane Smith';
    expect(user.firstName).to.equal('Jane');
    expect(user.lastName).to.equal('Smith');
  });

  it('should include virtuals in JSON output', () => {
    const user = new User({ firstName: 'John', lastName: 'Doe' });
    const json = user.toJSON();
    expect(json.fullName).to.equal('John Doe');
  });
});

Limitations and Alternatives to Virtual Properties

Virtual properties are powerful but have some limitations:

  1. Cannot be used in queries: Cannot use virtual properties as conditions in find() or findOne().
  2. Cannot be aggregated: Cannot use virtual properties in aggregation pipelines.
  3. Cannot be indexed: Cannot create indexes for virtual properties.

For these limitations, consider the following alternatives:

  1. Use actual fields: For frequently accessed computed results, store them as actual fields.
  2. Use $expr: In MongoDB 3.6+, you can use $expr for comparisons between fields.
  3. Use aggregation pipelines: For complex calculations, use the aggregation framework.
// Alternative example: Store computed fields
const productSchema = new mongoose.Schema({
  name: String,
  price: Number,
  taxRate: Number,
  priceWithTax: Number
});

productSchema.pre('save', function(next) {
  this.priceWithTax = this.price * (1 + this.taxRate / 100);
  next();
});

本站部分内容来自互联网,一切版权均归源网站或源作者所有。

如果侵犯了你的权益请来信告知我们删除。邮箱:cc@cccx.cn

Front End Chuan

Front End Chuan, Chen Chuan's Code Teahouse 🍵, specializing in exorcising all kinds of stubborn bugs 💻. Daily serving baldness-warning-level development insights 🛠️, with a bonus of one-liners that'll make you laugh for ten years 🐟. Occasionally drops pixel-perfect romance brewed in a coffee cup ☕.