阿里云主机折上折
  • 微信号
您当前的位置:网站首页 > 自定义类型与扩展

自定义类型与扩展

作者:陈川 阅读数:10733人阅读 分类: MongoDB

自定义类型与扩展

Mongoose 提供了灵活的自定义类型机制,允许开发者根据需求创建专属的数据类型。这种能力在处理特殊数据结构或业务逻辑时特别有用,能够突破内置类型的限制。

基础类型扩展

最简单的自定义类型方式是通过继承 mongoose.SchemaType 类。下面示例创建一个简单的邮政编码类型:

const mongoose = require('mongoose');
const Schema = mongoose.Schema;
const SchemaType = mongoose.SchemaType;

class ZipCode extends SchemaType {
  constructor(key, options) {
    super(key, options, 'ZipCode');
  }

  cast(val) {
    if (!/^\d{6}$/.test(val)) {
      throw new Error('ZipCode must be 6 digits');
    }
    return val;
  }
}

// 注册类型
mongoose.Schema.Types.ZipCode = ZipCode;

// 使用示例
const addressSchema = new Schema({
  zip: { type: ZipCode }
});

复杂自定义类型

对于需要处理嵌套数据的场景,可以创建更复杂的自定义类型。下面实现一个支持自动格式化的电话号码类型:

class PhoneNumber extends SchemaType {
  constructor(key, options) {
    super(key, options, 'PhoneNumber');
    this.countryCode = options?.countryCode || '86';
  }

  cast(val) {
    if (typeof val !== 'string') {
      throw new Error('PhoneNumber must be a string');
    }
    
    // 移除所有非数字字符
    const cleaned = val.replace(/\D/g, '');
    
    // 检查长度
    if (cleaned.length < 7 || cleaned.length > 15) {
      throw new Error('Invalid phone number length');
    }
    
    // 自动添加国家代码
    return cleaned.startsWith(this.countryCode) 
      ? `+${cleaned}` 
      : `+${this.countryCode}${cleaned}`;
  }
}

// 使用示例
const contactSchema = new Schema({
  phone: { 
    type: PhoneNumber,
    countryCode: '1'  // 北美区号
  }
});

类型验证扩展

自定义类型可以集成复杂的验证逻辑。下面创建一个带验证的RGB颜色类型:

class RGBColor extends SchemaType {
  constructor(key, options) {
    super(key, options, 'RGBColor');
  }

  cast(val) {
    if (typeof val === 'string') {
      // 处理 #RRGGBB 格式
      if (/^#([0-9a-f]{3}){1,2}$/i.test(val)) {
        return val.toLowerCase();
      }
      // 处理 rgb(r,g,b) 格式
      const rgbMatch = val.match(/^rgb\((\d+),\s*(\d+),\s*(\d+)\)$/);
      if (rgbMatch) {
        const toHex = c => parseInt(c).toString(16).padStart(2, '0');
        return `#${toHex(rgbMatch[1])}${toHex(rgbMatch[2])}${toHex(rgbMatch[3])}`;
      }
    }
    throw new Error('Invalid RGB color format');
  }
}

// 使用示例
const themeSchema = new Schema({
  primaryColor: { type: RGBColor }
});

混合类型扩展

Mongoose 的 Mixed 类型可以进一步扩展,创建具有特定行为的动态类型:

class DynamicConfig extends mongoose.Schema.Types.Mixed {
  constructor(key, options) {
    super(key, options);
    this.allowedKeys = options?.allowedKeys || [];
  }

  cast(val) {
    if (typeof val !== 'object') {
      throw new Error('DynamicConfig must be an object');
    }
    
    // 过滤不允许的键
    return Object.keys(val).reduce((acc, key) => {
      if (this.allowedKeys.includes(key)) {
        acc[key] = val[key];
      }
      return acc;
    }, {});
  }
}

// 使用示例
const appSchema = new Schema({
  config: {
    type: DynamicConfig,
    allowedKeys: ['theme', 'layout', 'features']
  }
});

继承内置类型

直接继承内置类型可以复用现有功能:

class TruncatedString extends mongoose.Schema.Types.String {
  constructor(key, options) {
    super(key, options);
    this.maxLength = options?.maxLength || 255;
  }

  cast(val) {
    const strVal = super.cast(val);
    return strVal.length > this.maxLength 
      ? strVal.substring(0, this.maxLength)
      : strVal;
  }
}

// 使用示例
const postSchema = new Schema({
  title: { 
    type: TruncatedString,
    maxLength: 100 
  },
  content: {
    type: TruncatedString,
    maxLength: 1000
  }
});

类型转换器

实现类型转换器可以在保存前自动转换数据格式:

class Timestamp extends mongoose.Schema.Types.Date {
  constructor(key, options) {
    super(key, options);
    this.precision = options?.precision || 'millisecond';
  }

  cast(val) {
    const date = super.cast(val);
    
    switch (this.precision) {
      case 'second':
        return new Date(Math.floor(date.getTime() / 1000) * 1000);
      case 'minute':
        return new Date(Math.floor(date.getTime() / 60000) * 60000);
      case 'hour':
        return new Date(Math.floor(date.getTime() / 3600000) * 3600000);
      default:
        return date;
    }
  }
}

// 使用示例
const eventSchema = new Schema({
  createdAt: { 
    type: Timestamp,
    precision: 'minute',
    default: Date.now
  }
});

虚拟类型扩展

虚拟类型也可以自定义,实现复杂计算逻辑:

class GeometricMean extends mongoose.Schema.Types.Virtual {
  constructor(key, options) {
    super(key, options);
    this.fields = options?.fields || [];
  }

  get() {
    return function() {
      const values = this.fields.map(f => this[f]);
      if (values.some(v => v === undefined || v === null)) return null;
      
      const product = values.reduce((acc, val) => acc * val, 1);
      return Math.pow(product, 1 / values.length);
    };
  }
}

// 使用示例
const statsSchema = new Schema({
  values: [Number]
});

statsSchema.virtual('gmean', new GeometricMean(null, {
  fields: ['values']
}));

数组类型扩展

自定义数组元素类型可以实现特定元素的数组:

class UniqueStringArray extends mongoose.Schema.Types.Array {
  constructor(key, options) {
    super(key, options);
    this.maxLength = options?.maxLength || 100;
  }

  cast(val) {
    const arr = super.cast(val);
    
    // 去重
    const uniqueArr = [...new Set(arr)];
    
    // 截断
    return uniqueArr.length > this.maxLength
      ? uniqueArr.slice(0, this.maxLength)
      : uniqueArr;
  }
}

// 使用示例
const userSchema = new Schema({
  tags: {
    type: UniqueStringArray,
    maxLength: 10
  }
});

查询条件扩展

自定义类型可以扩展查询条件处理:

class CaseInsensitiveString extends mongoose.Schema.Types.String {
  constructor(key, options) {
    super(key, options);
  }

  cast(val) {
    return super.cast(val)?.toLowerCase();
  }

  checkRequired(val) {
    return super.checkRequired(val) && val.trim().length > 0;
  }
}

// 使用时会自动转换查询条件
const productSchema = new Schema({
  name: { type: CaseInsensitiveString, required: true }
});

// 查询时不需要考虑大小写
Product.find({ name: 'TEST' }); // 会匹配 "test", "Test" 等

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

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

前端川

前端川,陈川的代码茶馆🍵,专治各种不服的Bug退散符💻,日常贩卖秃头警告级的开发心得🛠️,附赠一行代码笑十年的摸鱼宝典🐟,偶尔掉落咖啡杯里泡开的像素级浪漫☕。‌