使用MongoDB存储用户体重历史记录的最佳实现方案是什么?
体重历史记录需求的NestJS + Mongoose最佳实现方案
架构选型
根据你的业务场景,有两种成熟的实现方案,可根据实际使用情况选择:
- 方案1:用户文档内嵌体重历史数组(推荐90%以上普通场景使用)
优势:单文档查询无需联表,写入性能高,普通用户一生的体重记录也不会超过万级,完全不会触及MongoDB单文档16MB的上限
劣势:仅不适用于分钟级高频上报体重的特殊场景(如医疗监测设备自动上报) - 方案2:独立集合存储体重历史记录
优势:文档体积可控,适合超高频率的体重上报场景
劣势:需要事务保证数据一致性,查询用户数据需要跨表操作
方案1(内嵌数组)实现步骤
1. 定义Mongoose Schema
将原有的单体重字段拆分为最新体重冗余字段+历史记录数组,冗余字段用于直接展示最新体重,无需遍历历史数组:
// user.schema.ts import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; import { Document } from 'mongoose'; // 体重记录子文档,不需要单独_id @Schema({ _id: false }) class WeightRecord { @Prop({ required: true, type: Number }) value: number; // 体重数值 @Prop({ required: true, type: Date, default: Date.now }) recordedAt: Date; // 记录时间 @Prop({ type: String }) note?: string; // 可选备注,如“空腹”“餐后” } @Schema({ timestamps: true }) export class User extends Document { // 原有用户字段(如昵称、手机号等自行保留) @Prop({ required: true, unique: true }) username: string; // 最新体重冗余字段,提升查询效率 @Prop({ type: Number }) currentWeight?: number; // 历史体重数组,默认按插入顺序倒序存储,最新记录在最前 @Prop({ type: [WeightRecord], default: [] }) weightHistory: WeightRecord[]; } export const UserSchema = SchemaFactory.createForClass(User); // 可选:加复合索引避免同一用户同一时间重复提交记录 UserSchema.index({ _id: 1, 'weightHistory.recordedAt': 1 }, { unique: true });
2. 编写体重更新与查询逻辑
用MongoDB原子操作符完成单请求更新,避免并发问题,无需先查后改:
// user.service.ts import { Injectable } from '@nestjs/common'; import { InjectModel } from '@nestjs/mongoose'; import { Model } from 'mongoose'; import { User } from './user.schema'; import { UpdateWeightDto } from './dto/update-weight.dto'; @Injectable() export class UserService { constructor(@InjectModel(User.name) private userModel: Model<User>) {} // 更新体重,同时写入历史记录 async updateWeight(userId: string, updateWeightDto: UpdateWeightDto) { const { value, recordedAt, note } = updateWeightDto; return this.userModel.findByIdAndUpdate( userId, { // 更新最新体重 $set: { currentWeight: value }, // 插入历史记录到数组头部 $push: { weightHistory: { $each: [{ value, recordedAt: recordedAt || new Date(), note }], $position: 0 } } }, { new: true } // 返回更新后的文档 ); } // 分页查询体重历史 async getWeightHistory(userId: string, page = 1, limit = 30) { const skip = (page - 1) * limit; const user = await this.userModel.findById(userId, { weightHistory: { $slice: [skip, limit] }, currentWeight: 1 }); return user?.weightHistory || []; } }
对应DTO定义参考:
// dto/update-weight.dto.ts import { IsNumber, IsOptional, IsDateString, IsString } from 'class-validator'; export class UpdateWeightDto { @IsNumber() value: number; @IsOptional() @IsDateString() recordedAt?: string; @IsOptional() @IsString() note?: string; }
方案2(独立集合)实现步骤(仅适合高频上报场景)
如果是医疗设备等分钟级上报体重的场景,单用户年记录量超过5万条,可采用该方案:
1. 定义独立集合Schema
// weight-history.schema.ts import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; import { Document, Types } from 'mongoose'; @Schema({ timestamps: true }) export class WeightHistory extends Document { @Prop({ required: true, type: Types.ObjectId, ref: 'User' }) userId: Types.ObjectId; @Prop({ required: true, type: Number }) value: number; @Prop({ required: true, type: Date, default: Date.now }) recordedAt: Date; @Prop({ type: String }) note?: string; } export const WeightHistorySchema = SchemaFactory.createForClass(WeightHistory); // 加复合索引大幅提升用户历史查询速度 WeightHistorySchema.index({ userId: 1, recordedAt: -1 });
2. 写入逻辑(用事务保证数据一致性)
// weight-history.service.ts import { Injectable } from '@nestjs/common'; import { InjectModel } from '@nestjs/mongoose'; import { Model, ClientSession } from 'mongoose'; import { WeightHistory } from './weight-history.schema'; import { User } from '../user/user.schema'; import { UpdateWeightDto } from './dto/update-weight.dto'; @Injectable() export class WeightHistoryService { constructor( @InjectModel(WeightHistory.name) private weightHistoryModel: Model<WeightHistory>, @InjectModel(User.name) private userModel: Model<User> ) {} async addWeightRecord(userId: string, dto: UpdateWeightDto) { const session: ClientSession = await this.weightHistoryModel.startSession(); session.startTransaction(); try { // 新增历史记录 const [record] = await this.weightHistoryModel.create( [{ userId, ...dto, recordedAt: dto.recordedAt || new Date() }], { session } ); // 更新用户最新体重 await this.userModel.findByIdAndUpdate(userId, { currentWeight: dto.value }, { session }); await session.commitTransaction(); return record; } catch (err) { await session.abortTransaction(); throw err; } finally { session.endSession(); } } }
内容的提问来源于stack exchange,提问作者BrunoFred
相关产品推荐
相关产品推荐

