MongoDB集合中重复文件名自动添加数字序号的实现方案
解决方案
以下两种方案均完全满足你提出的3项附加要求,适配MongoDB 5.0 + 原生Node.js驱动环境,适合Atlas Serverless使用。
方案一:无需修改现有表结构,适合写入频率较低的场景
核心思路是先拆分待插入文件名的前缀和后缀,通过正则匹配同系列文件,再用聚合直接提取最大序号,全程仅需1次查询+1次写入。
步骤1:文件名拆分工具函数
/** * 拆分文件名前缀和后缀 * @param {string} filename 原始文件名 * @returns {Object} { base: 前缀, ext: 后缀(带点号)} */ function splitFilename(filename) { const lastDotPos = filename.lastIndexOf('.'); if (lastDotPos === -1) { return { base: filename, ext: '' }; } return { base: filename.slice(0, lastDotPos), ext: filename.slice(lastDotPos) }; }
步骤2:序号计算与插入逻辑
async function insertFile(rawFilename) { const { base, ext } = splitFilename(rawFilename); // 正则转义特殊字符,避免匹配错误 const escapedBase = base.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); const escapedExt = ext.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // 构造同系列文件名匹配正则 const seqPattern = ext ? new RegExp(`^${escapedBase}\\((\\d+)\\)${escapedExt}$`) : new RegExp(`^${escapedBase}\\((\\d+)$`); // 聚合查询最大序号 const aggResult = await db.collection('file').aggregate([ { $match: { $or: [ { name: rawFilename }, { name: { $regex: seqPattern } } ] } }, { $addFields: { seq: { $let: { vars: { matchRes: { $regexFind: { input: '$name', regex: seqPattern } } }, in: { $cond: [ { $eq: ['$$matchRes', null] }, 1, // 原始无序号文件默认序号为1 { $toInt: { $arrayElemAt: ['$$matchRes.captures', 0] } } ] } } } } }, { $group: { _id: null, maxSeq: { $max: '$seq' } } } ]).toArray(); // 生成最终文件名 let finalName = rawFilename; if (aggResult.length) { const nextSeq = aggResult[0].maxSeq + 1; finalName = `${base}(${nextSeq})${ext}`; } // 插入文档 await db.collection('file').insertOne({ name: finalName }); return finalName; }
方案优缺点
- 优点:无需修改现有数据结构,兼容存量数据
- 缺点:正则匹配计算序号的性能略低于新增字段方案,适合单系列文件数量不超过1万、写入QPS低于10的场景
方案二:新增冗余字段,高性能适合高频写入场景
核心思路是新增3个冗余字段,将文件名拆分、序号计算的工作提前到写入时完成,配合联合索引实现毫秒级查询,全程仅需1次查询+1次写入,性能最优。
新增字段说明
| 字段名 | 类型 | 说明 |
|---|---|---|
file_base | string | 文件名前缀 |
file_ext | string | 文件名后缀(带点号) |
file_seq | int | 文件序号,原始无序号文件为1,带序号文件对应括号内的数字 |
前置操作:创建联合索引
// 给file_base和file_ext创建联合索引,查询直接命中 await db.collection('file').createIndex({ file_base: 1, file_ext: 1 }); // 给name创建唯一索引,避免并发插入重复 await db.collection('file').createIndex({ name: 1 }, { unique: true });
插入逻辑代码
async function insertFile(rawFilename) { const { base, ext } = splitFilename(rawFilename); let nextSeq = 1; let finalName = rawFilename; try { // 查询同系列最大序号 const maxSeqRes = await db.collection('file').aggregate([ { $match: { file_base: base, file_ext: ext } }, { $group: { _id: null, maxSeq: { $max: '$file_seq' } } } ]).toArray(); if (maxSeqRes.length) { nextSeq = maxSeqRes[0].maxSeq + 1; finalName = `${base}(${nextSeq})${ext}`; } // 插入文档 await db.collection('file').insertOne({ name: finalName, file_base: base, file_ext: ext, file_seq: nextSeq }); } catch (e) { // 唯一键冲突说明并发插入,重试一次即可 if (e.code === 11000) { return insertFile(rawFilename); } throw e; } return finalName; }
方案优缺点
- 优点:查询性能拉满,无正则计算开销,支持高频率写入,完全适配Atlas Serverless的按量付费模式,IO成本极低
- 缺点:需要修改表结构,存量数据需要补全3个冗余字段(仅需一次批量更新即可完成)
扩展:补全最小缺失序号的实现
如果需要补全中间缺失的最小序号(比如你的示例中生成sample(3).txt而非sample(6).txt),只需将方案二的聚合查询修改为如下即可:
const seqListRes = await db.collection('file').aggregate([ { $match: { file_base: base, file_ext: ext } }, { $group: { _id: null, seqs: { $push: '$file_seq' } } } ]).toArray(); let nextSeq = 1; if (seqListRes.length) { const seqSet = new Set(seqListRes[0].seqs); // 从2开始找第一个缺失的序号 for (let i = 2; i <= seqSet.size + 2; i++) { if (!seqSet.has(i)) { nextSeq = i; break; } } }
该方案适合单系列文件数量较少的场景,单系列文件超过1万时建议优先使用最大序号+1的逻辑。
内容的提问来源于stack exchange,提问作者DarkLordCoder
相关产品推荐
相关产品推荐

