在用户管理相关的业务场景中,经常需要实现忽略年份查询生日范围的需求,比如统计生日在10月1日到次年2月1日之间的用户,或者筛选本月过生日的用户。使用Mongoose操作MongoDB时,普通的日期查询会包含年份信息,无法直接满足忽略年份的查询要求,需要借助MongoDB的聚合查询能力来实现。

需求分析与实现思路
要实现忽略年份的生日范围查询,核心是把用户生日中的月份和日期提取出来,转换成可比较的数值,再结合范围条件进行筛选。具体步骤如下:
- 使用聚合管道的
$addFields阶段,提取生日字段的月份和日期,组合成MM-DD格式的字符串或者对应的数值 - 将查询的起始日期和结束日期也转换成相同的MM-DD格式
- 处理跨年份的范围场景,比如查询12月到次年2月的生日,需要拆分条件分别匹配
- 使用
$match阶段完成最终的筛选
数据模型定义
首先定义用户的Mongoose模型,其中包含生日字段,类型为Date:
const mongoose = require('mongoose');
const { Schema } = mongoose;
// 用户模型定义
const userSchema = new Schema({
name: String,
birthday: Date // 存储用户的完整生日日期,包含年月日
});
const User = mongoose.model('User', userSchema);
聚合查询实现
非跨年份的生日范围查询
如果查询的范围在同一年,比如查询生日在3月15日到6月20日之间的用户,实现逻辑如下:
/**
* 查询生日在指定月日范围内的用户,范围不跨年份
* @param {string} start - 起始月日,格式MM-DD,比如03-15
* @param {string} end - 结束月日,格式MM-DD,比如06-20
* @returns {Promise<Array>} 匹配的用户列表
*/
async function queryBirthdayInRange(start, end) {
const result = await User.aggregate([
// 新增字段存储生日的月日字符串
{
$addFields: {
birthdayMD: {
$dateToString: {
format: '%m-%d',
date: '$birthday'
}
}
}
},
// 筛选月日在范围内的文档
{
$match: {
birthdayMD: {
$gte: start,
$lte: end
}
}
},
// 移除临时添加的字段
{
$project: {
birthdayMD: 0
}
}
]);
return result;
}
跨年份的生日范围查询
当查询范围跨年份时,比如查询12月1日到次年2月28日的生日,直接比较MM-DD字符串会失效,需要拆分条件:要么月日大于等于12-01,要么月日小于等于02-28。实现代码如下:
/**
* 查询生日在指定月日范围内的用户,支持跨年份
* @param {string} start - 起始月日,格式MM-DD,比如12-01
* @param {string} end - 结束月日,格式MM-DD,比如02-28
* @returns {Promise<Array>} 匹配的用户列表
*/
async function queryBirthdayCrossYear(start, end) {
// 将起始和结束的月日转换成数值,方便比较
const startNum = parseInt(start.replace('-', ''), 10);
const endNum = parseInt(end.replace('-', ''), 10);
let matchCondition = {};
if (startNum <= endNum) {
// 不跨年份的场景
matchCondition = {
$addFields: {
birthdayMDNum: {
$toInt: {
$replaceAll: {
input: { $dateToString: { format: '%m-%d', date: '$birthday' } },
find: '-',
replacement: ''
}
}
}
},
$match: {
birthdayMDNum: { $gte: startNum, $lte: endNum }
}
};
} else {
// 跨年份的场景,比如12-01到02-28,匹配大于等于12-01或者小于等于02-28
matchCondition = {
$addFields: {
birthdayMDNum: {
$toInt: {
$replaceAll: {
input: { $dateToString: { format: '%m-%d', date: '$birthday' } },
find: '-',
replacement: ''
}
}
}
},
$match: {
$or: [
{ birthdayMDNum: { $gte: startNum } },
{ birthdayMDNum: { $lte: endNum } }
]
}
};
}
const pipeline = [
matchCondition.$addFields ? matchCondition : null,
matchCondition.$match ? matchCondition : null,
// 移除临时字段
{
$project: {
birthdayMDNum: 0
}
}
].filter(Boolean);
const result = await User.aggregate(pipeline);
return result;
}
调用示例
实际使用时的调用方式如下:
// 查询生日在3月15日到6月20日的用户
queryBirthdayInRange('03-15', '06-20').then(users => {
console.log('范围内用户:', users);
});
// 查询生日在12月1日到次年2月28日的用户(跨年份)
queryBirthdayCrossYear('12-01', '02-28').then(users => {
console.log('跨年份范围内用户:', users);
});
注意事项
- 生日字段需要保证存储的是正确的Date类型,如果是字符串类型需要先转换
- 聚合查询中的日期相关操作符依赖MongoDB的版本,建议使用4.0及以上版本
- 如果数据量较大,可以对生日字段建立索引提升查询效率,不过聚合管道中的临时字段无法直接使用索引,需要结合实际场景优化
- 月份和日期的格式需要统一为MM-DD,避免个位数月日导致比较错误