面对Schema free的MongoDB,如何规范你的schema

本文深入探讨了MongoDB文档模型带来的优势,包括json形式存储、高效读写性能以及灵活的schemafree特性。重点介绍了Variety工具,用于检测表字段类型、分布,提供报表以直观分析现有表结构,帮助发现数据模型隐患。同时阐述了MongoDB的DocumentValidation功能及其限制,强调了在SchemaFree场景下保持字段一致性的重要性。

作为近年最为火热的文档型数据库,MongoDB受到了越来越多人的关注,但是由于国内的MongoDB相关技术分享屈指可数,不少朋友都在抱怨无从下手。本期【DBA+社群】联合发起人周李洋为大家分享面对Schema free 的MongoDB时,如何规范你的schema。将从实际应用的角度,并覆盖调优,troubleshooting等方面来进行MongoDB的干货分享,希望能对大家带来帮助。

目录:

  • MongoDB文档模型带来的好处

  • Variety

  • 我们为什么要用Variety呢?

  • Document Validation

  • validation的限制

如果希望了解更多MongoDB基础的信息,还请大家Google下。我们知道MongoDB是一个文档型数据库,scheme free 是其非常重要的特性,但是在生产中我们应该怎么如合理利用这个特性,合理处理MongoDB的schema呢?

一、MongoDB文档模型带来的好处

大家都知道MongoDB是文档型数据库,是Schema Free的。 那么MongoDB的文档模型能给我们带来哪些好处呢,在这简单列举几个:

1、json形式

在MongoDB中,开发人员可以直接将一个json数据存储进MongoDB,这对于开发人员来说是非常友好额;

2、读写性能高

在关系型数据库中,我们经常会进行join、子查询等关联性需求,这时候往往会带来较多的随机IO,而在MongoDB中,我们可以通过合理的数据模型设计来将很多的关联需求通过内嵌、反范式的方式实现,减少了随机IO;

3、schema free

MongoDB的数据模型是灵活的,无需为了Online DDL而操心,不同的document也可以有不同的结构。

在这,我们不深入探究如何对于MongoDB 的Schema进行设计、建模,有关这部分内容,推荐大家可以阅读TJ在开源中国的年终盛典会上分享《MongoDB 进阶模式设计》,以及《Retail Reference Architecture Part 1 to 4 》。

在此我们将主要针对进行了初步建模、并正式上线服务后的schema进行巡检与检测的方式来进行讨论。

二、Variety

Variety是一个开源的,非常使用的,检测mongodb表字段类型、分布的一个开源工具。 正如其github readme中第一句所说"**Meet Variety, a Schema Analyzer for MongoDB**"。

Variety能够帮助我们检测我们MongoDB表中的字段类型、分布,并生产报表,可以让我们非常直观的对现有表结构、字段类型进行分析,并找出数据模型中的隐患。

下面我们通过例子来进行讲解:

首先,建立一个表

我们来看看通过variety获得的结果

test是我们的db名,users是表名。我们可以看到,针对我们之前插入的5条数据,variety跑出的结果是:

所有的document都含有_id,和name字段,60%的document含有bio字段,40%的document含有birthday和pets字段,且pets字段有2个类型的数据(4个array的,1个string的),20%的document含有someBinData和SomeWeirdLegacyKey字段。

然而生产环境中由于我们的数据量较大,比如一个表有10亿条数据,全部进行扫描会耗时较长,可能我们仅希望对1000条数据进行分析,这时候就可以使用limit来限定。

由于MongoDB的可以通过内嵌来减少联合查询的需求,可以通过反范式来减少随机IO,所以很可能会有嵌套出现在我们的document中。有的时候嵌套的层数太多了,影响我们的统计信息,怎么办,我们可以通过maxDepth来限制。请参考下面的例子:

又或者我们希望指定统计的条件,比如希望caredAbout为true的,可以这样做:

```

$ mongo test --eval "var collection = 'users', query = {'caredAbout':true}" variety.js

```

又或者是希望进行排序:

```

$ mongo test --eval "var collection = 'users', sort = { updated_at : -1 }" variety.js

```

同时我们也可以指定分析结果的format:

```

$ mongo test --quiet --eval "var collection = 'users', outputFormat='json'" variety.js

```

一般在生产中, 我们不会在primary上进行分析, 我们可以在一个priority为0,且为hidden的secondary上进行分析,这时候需要指定slaveOK:

```

$ mongo secondary.replicaset.member:31337/somedb --eval "var collection = 'users', slaveOk = true" variety.js

```

又或者说我们希望将分析结果存在mongo中:

```

$ mongo test --quiet --eval "var collection = 'users', persistResults=true" variety.js

```

并且指定存储的详细信息:

- resultsDatabase 分析结果所存储的db名

- resultsCollection 分析结果所存储的collection名

- resultsUser 分析结果存储的实例的user

- resultsPass 分析结果所存储的实例的password

```

mongo test --quiet --eval "var collection = 'users', persistResults=true, resultsDatabase='db.example.com/variety' variety.js

```

三、我们为什么要用Variety呢?

尽管我们MongoDB是Schema Free的,但是绝大多数情况下, 我们都希望字段类型统一。

不一致的字段类型可能会为我们的数据带来误差,试想一下,如果某个字段的字段类型不统一,而我们却不知情,这时候很可能会发现业务查询有数据丢失,数据不准确。

并且在生产环境中,应用的版本在不断迭代,需求不断增多,字段也随之变化,如果在没有规范化的上线流程检查过后,数据库中可能还会存在部分数据的字段确实,比如有的document有a字段,有的却没有,variety也可以帮助我们发现这些问题。

四、Document Validation

MongoDB 3.2推出了很多给力的功能,在这不得不提及Document Validation,Document Validation的出现我想也是MongoDB官方想表达”schema free but you may need some rules”吧,哈哈,纯属臆测。

简单介绍下Document Validation:

我们可以为我们schema free的mongodb collection做一些限制。当然这并不是意味着MongoDB变成了关系型数据库,个人觉得这反而更好的突出了MongoDB Schema free的特性。在正确的地方、需要的地方schema free,在适当的地方要有限制。

假设我们要新建一个表contacts,要有如下约束:

phone字段为string类型或者email字段要匹配”@mongodb.com”结尾,或者status为”Unknown”或者"Incomplete"

对已经建立了的表,我们可以通过如下方式来做限定:

这里可以看到,多了一个validationLevel参数,我们可以在设置validation的时候指定我们的validationLevel级别:

- 默认级别是strict,对该collection已有的和以后新增的document都进行validation验证;

- 可以设置为moderate,仅对已经存在的document进行validation限定;

同时还有validationAction参数来指定当有不符合validation规则的数据进行update或者insert的时候, 我们mongodb实例如何进行处理。

- 默认级别为error,mongodb将拒绝这些不符合validation规则的insert和update。

- 可以设置为warn,mongodb会在日志中记录,但是允许这类insert和update操作。日志中如:

五、validation的限制

- validation不能对admin、local和config库中的collection进行设置;

- 不能对system.*这类collections进行validation设置;


<think>我们设计一个虚拟资源会员系统的MongoDB集合结构,需要包含以下核心模块:1.用户系统(包含会员状态)2.资源(文章/下载资源)管理3.支付订单4.登录认证(含微信绑定)5.主题皮肤配置(可选,也可前端存储)每个集合的字段设计如下(使用TypeScript接口表示):###1.用户集合(User)```typescriptinterfaceUser{_id:string;//MongoDB自动生成的IDusername?:string;//用户名(可选,用于密码登录)password?:string;//加密后的密码(可选)email?:string;//邮箱(可选)phone?:string;//手机号(可选)wechatOpenId?:string;//微信开放平台唯一标识(用于微信登录)unionId?:string;//微信UnionID(多应用统一标识)isMember:boolean;//是否是会员memberExpire?:Date;//会员过期时间(非会员可为空)avatar:string;//头像URLname:string;//昵称或真实姓名createdAt:Date;//创建时间updatedAt:Date;//更新时间paymentHistory:string[];//关联的支付订单ID数组resources:string[];//用户拥有的资源ID数组(用户上传的资源)themePreference?:string;//主题偏好(可选)}```###2.资源集合(Resource)```typescriptinterfaceResource{_id:string;title:string;//资源标题description:string;//简介coverImage:string;//封面图URLcontent:string;//资源内容(文章内容或下载链接)isFree:boolean;//是否免费price:number;//非会员价格(如果是免费资源,此字段可忽略)category:string;//资源分类(如:电子书、视频、模板等)tags:string[];//标签author:string;//作者(可以是用户ID或字符串)createdAt:Date;updatedAt:Date;downloadCount:number;//下载次数统计viewCount:number;//浏览次数统计}```###3.支付订单集合(PaymentOrder)```typescriptenumPaymentStatus{Pending='pending',//等待支付Success='success',//支付成功Failed='failed',//支付失败Closed='closed'//订单关闭}enumPaymentMethod{Wechat='wechat',Alipay='alipay'}interfacePaymentOrder{_id:string;userId:string;//关联的用户IDamount:number;//支付金额(单位:元)method:PaymentMethod;//支付方式status:PaymentStatus;//支付状态orderTime:Date;//订单创建时间successTime?:Date;//支付成功时间memberDuration:number;//会员时长(单位:月)qrCode?:string;//支付二维码URL(如果是扫码支付)tradeNo?:string;//第三方支付平台交易号(支付成功后更新)outTradeNo:string;//商户订单号(自己系统的订单号)}```###4.微信登录临时凭证集合(WechatLoginTemp)```typescriptinterfaceWechatLoginTemp{_id:string;state:string;//随机状态码(防CSRF)openid?:string;//微信返回的openid(登录成功后写入)unionid?:string;//微信unionid(如果有)code?:string;//微信临时code(用于换取access_token)accessToken?:string;//微信access_token(如果需要)expireAt:Date;//过期时间(用于自动删除过期记录)userId?:string;//登录成功后关联的用户ID(如果已绑定)}```###5.短信/邮箱验证码集合(VerificationCode)```typescriptinterfaceVerificationCode{_id:string;type:'phone'|'email';//验证类型target:string;//手机号或邮箱code:string;//验证码expireAt:Date;//过期时间used:boolean;//是否已使用createdAt:Date;}```###6.主题皮肤集合(Theme)[可选,如果后端需要管理主题]```typescriptinterfaceTheme{_id:string;name:string;//主题名称identifier:string;//主题标识(如:light,dark,blue)variables:Record<string,string>;//CSS变量键值对isDefault:boolean;//是否是默认主题}```##字段说明和索引建议###索引设计1.**User集合**:-`{email:1}`,唯一索引(如果邮箱必填则设为唯一)-`{phone:1}`,唯一索引(如果手机号必填则设为唯一)-`{wechatOpenId:1}`,唯一索引(用于微信登录)-`{isMember:1,memberExpire:1}`(用于会员状态查询)2.**Resource集合**:-`{category:1,createdAt:-1}`(按分类和时间排序)-`{tags:1}`(标签查询)-`{isFree:1}`(免费资源查询)3.**PaymentOrder集合**:-`{userId:1,orderTime:-1}`(用户订单查询)-`{outTradeNo:1}`,唯一索引(商户订单号唯一)-`{tradeNo:1}`,唯一索引(第三方交易号唯一)4.**WechatLoginTemp集合**:-`{state:1}`,唯一索引(用于登录回调验证)-`{expireAt:1}`,设置TTL索引(自动删除过期记录)5.**VerificationCode集合**:-`{target:1,type:1,createdAt:-1}`(用于验证码查询)-`{expireAt:1}`,设置TTL索引(自动删除过期验证码)##数据关系说明1.**用户和资源**:-一个用户可以拥有多个资源(上传资源),通过`User.resources`数组字段关联资源ID。-资源集合中的`author`字段关联用户ID(或直接存储作者名字)。2.**用户和支付订单**:-一个用户可以有多个支付订单,通过`PaymentOrder.userId`关联用户ID。-用户集合中的`paymentHistory`存储订单ID数组(可选,也可以不冗余,通过查询获取)。3.**微信登录临时凭证**:-用于存储微信登录过程中的临时状态,成功后会关联到用户ID(`userId`字段)。##会员状态维护-当支付订单状态变为`success`时,系统需要更新相应用户的会员状态:```typescript//伪代码:支付成功回调处理asyncfunctionhandlePaymentSuccess(orderId:string){constorder=awaitPaymentOrder.findById(orderId);if(!order||order.status!==PaymentStatus.Success)return;constuser=awaitUser.findById(order.userId);if(!user)return;//计算新的会员过期时间constcurrentExpire=user.memberExpire||newDate();constnewExpire=newDate(currentExpire);newExpire.setMonth(newExpire.getMonth()+order.memberDuration);//更新用户user.isMember=true;user.memberExpire=newExpire;awaituser.save();}```##安全性考虑1.**密码存储**:使用bcrypt等算法加密存储。2.**敏感信息**:支付订单中的金额、会员信息等关键字段应避免被篡改,需有签名验证。3.**验证码**:设置合理的过期时间(如5分钟)和使用次数限制(一次验证码只能使用一次)。##总结以上设计涵盖了虚拟资源会员系统的核心数据模型,包括:-用户信息(含会员状态)-资源管理-支付订单-微信登录凭证管理-验证码管理-主题皮肤(可选)每个集合的字段设计满足功能需求,并考虑了索引优化和数据关系。在实际应用中,可根据具体业务需求调整字段。§§相关问题§§1.如何设计MongoDB索引以优化虚拟资源会员系统的查询性能?2.在会员系统中,如何处理并发支付订单导致的会员时长计算问题?3.如何保证用户资源下载链接的安全性和时效性?4.在微信登录流程中,如何防止CSRF攻击?5.如何实现会员到期自动降级的定时任务?</think># Nuxt 3 虚拟资源会员系统 MongoDB 集合 Schema 设计 我将设计一个完整的 MongoDB 集合结构,支持 Nuxt 3 + TypeScript + ElementPlus + Pinia + TailwindCSS + JWT 的虚拟资源会员系统。 ## 核心集合设计 ### 1. 用户集合 (User) ```typescript interface User { _id: ObjectId; username: string; // 用户名 email: string; // 邮箱 phone?: string; // 手机号(可选) password: string; // 加密后的密码 isMember: boolean; // 是否是会员 memberExpire?: Date; // 会员过期时间 memberType?: 'monthly' | 'quarterly' | 'yearly'; // 会员类型 wechatOpenId?: string; // 微信OpenID(用于扫码登录) avatar: string; // 头像URL createdAt: Date; // 创建时间 updatedAt: Date; // 更新时间 lastLogin: Date; // 最后登录时间 refreshToken?: string; // 刷新令牌(用于双token刷新) themePreference?: 'light' | 'dark' | 'blue' | 'green'; // 主题偏好 resources: ObjectId[]; // 用户拥有的资源ID列表 paymentHistory: ObjectId[]; // 支付历史记录ID列表 } ``` ### 2. 资源集合 (Resource) ```typescript interface Resource { _id: ObjectId; title: string; // 资源标题 description: string; // 资源描述 content: string; // 资源内容(文章/下载链接) coverImage: string; // 封面图片URL category: string; // 资源分类 tags: string[]; // 资源标签 isFree: boolean; // 是否免费 price?: number; // 非会员价格(如果是付费资源) author: ObjectId; // 作者ID(关联User集合) createdAt: Date; // 创建时间 updatedAt: Date; // 更新时间 downloadCount: number; // 下载次数 viewCount: number; // 浏览次数 requiresMembership: boolean; // 是否需要会员 } ``` ### 3. 支付订单集合 (Payment) ```typescript enum PaymentMethod { WECHAT = 'wechat', ALIPAY = 'alipay' } enum PaymentStatus { PENDING = 'pending', SUCCESS = 'success', FAILED = 'failed', CLOSED = 'closed' } interface Payment { _id: ObjectId; userId: ObjectId; // 用户ID amount: number; // 支付金额 method: PaymentMethod; // 支付方式 status: PaymentStatus; // 支付状态 orderTime: Date; // 订单创建时间 successTime?: Date; // 支付成功时间 qrCodeUrl?: string; // 支付二维码URL tradeNo?: string; // 第三方支付平台交易号 outTradeNo: string; // 商户订单号(系统唯一) memberDuration: number; // 会员时长(单位:月) description: string; // 订单描述 } ``` ### 4. 微信登录凭证集合 (WechatLogin) ```typescript interface WechatLogin { _id: ObjectId; state: string; // 随机状态码(防CSRF) openid?: string; // 微信返回的openid unionid?: string; // 微信unionid code?: string; // 微信临时code accessToken?: string; // 微信access_token expireAt: Date; // 过期时间 userId?: ObjectId; // 关联的用户ID createdAt: Date; // 创建时间 } ``` ### 5. 验证码集合 (VerificationCode) ```typescript interface VerificationCode { _id: ObjectId; type: 'phone' | 'email'; // 验证类型 target: string; // 手机号或邮箱 code: string; // 验证码 expireAt: Date; // 过期时间 used: boolean; // 是否已使用 createdAt: Date; // 创建时间 } ``` ## 索引设计 ```typescript // 用户集合索引 db.users.createIndex({ email: 1 }, { unique: true }); db.users.createIndex({ phone: 1 }, { unique: true, sparse: true }); db.users.createIndex({ wechatOpenId: 1 }, { unique: true, sparse: true }); db.users.createIndex({ isMember: 1, memberExpire: 1 }); // 资源集合索引 db.resources.createIndex({ title: "text", description: "text" }); db.resources.createIndex({ category: 1 }); db.resources.createIndex({ requiresMembership: 1 }); db.resources.createIndex({ createdAt: -1 }); // 支付订单集合索引 db.payments.createIndex({ userId: 1 }); db.payments.createIndex({ outTradeNo: 1 }, { unique: true }); db.payments.createIndex({ tradeNo: 1 }, { unique: true, sparse: true }); db.payments.createIndex({ status: 1, orderTime: -1 }); // 微信登录凭证集合索引 db.wechatLogins.createIndex({ state: 1 }, { unique: true }); db.wechatLogins.createIndex({ expireAt: 1 }, { expireAfterSeconds: 0 }); // 验证码集合索引 db.verificationCodes.createIndex({ target: 1, type: 1 }); db.verificationCodes.createIndex({ expireAt: 1 }, { expireAfterSeconds: 0 }); ``` ## 数据关系说明 1. **用户与资源**: - 用户可以通过 `resources` 字段拥有多个资源 - 资源通过 `author` 字段关联到创建者 - 关系:一对多(用户 ↔ 资源) 2. **用户与支付**: - 用户通过 `paymentHistory` 字段关联多个支付订单 - 支付订单通过 `userId` 字段关联用户 - 关系:一对多(用户 ↔ 支付订单) 3. **用户与微信登录**: - 用户通过微信登录凭证的 `userId` 关联 - 关系:一对一(用户 ↔ 微信登录凭证) 4. **支付与会员状态**: - 支付成功后,系统根据 `memberDuration` 更新用户的 `memberExpire` 和 `isMember` 状态 - 关系:支付状态变更触发用户状态更新 ## 会员状态维护 当支付订单状态变为 `SUCCESS` 时,系统需要更新相应用户的会员状态: ```typescript // 支付成功回调处理 async function handlePaymentSuccess(paymentId: ObjectId) { const payment = await Payment.findById(paymentId); if (!payment || payment.status !== PaymentStatus.SUCCESS) return; const user = await User.findById(payment.userId); if (!user) return; // 计算新的会员过期时间 const currentExpire = user.memberExpire || new Date(); const newExpire = new Date(currentExpire); newExpire.setMonth(newExpire.getMonth() + payment.memberDuration); // 更新用户状态 user.isMember = true; user.memberExpire = newExpire; user.memberType = payment.memberDuration === 1 ? 'monthly' : payment.memberDuration === 3 ? 'quarterly' : 'yearly'; await user.save(); } ``` ## 安全性设计 1. **密码存储**: ```typescript // 用户创建/更新密码时 import bcrypt from 'bcryptjs'; const salt = bcrypt.genSaltSync(10); user.password = bcrypt.hashSync(password, salt); ``` 2. **JWT 令牌生成**: ```typescript // 登录成功后生成双token import jwt from 'jsonwebtoken'; const accessToken = jwt.sign( { userId: user._id, email: user.email }, process.env.JWT_SECRET, { expiresIn: '15m' } ); const refreshToken = jwt.sign( { userId: user._id }, process.env.JWT_REFRESH_SECRET, { expiresIn: '7d' } ); // 存储refreshToken到用户记录 user.refreshToken = refreshToken; await user.save(); ``` 3. **支付回调验证**: ```typescript // 支付回调处理 app.post('/api/payment/callback', async (req, res) => { const signature = req.headers['x-signature']; const body = req.body; // 验证签名 if (!verifySignature(body, signature, process.env.PAYMENT_SECRET)) { return res.status(403).json({ error: 'Invalid signature' }); } // 处理支付结果 const payment = await Payment.findOne({ outTradeNo: body.outTradeNo }); if (!payment) return res.status(404).json({ error: 'Order not found' }); payment.status = body.status === 'success' ? PaymentStatus.SUCCESS : PaymentStatus.FAILED; payment.tradeNo = body.tradeNo; if (payment.status === PaymentStatus.SUCCESS) { payment.successTime = new Date(); await handlePaymentSuccess(payment._id); } await payment.save(); res.json({ success: true }); }); ``` ## 虚拟资源访问控制 ```typescript // 获取资源内容(考虑会员状态) async function getResourceContent(resourceId: ObjectId, userId?: ObjectId) { const resource = await Resource.findById(resourceId); if (!resource) return null; // 免费资源直接返回 if (resource.isFree) return resource.content; // 检查用户会员状态 if (!userId) return null; const user = await User.findById(userId); if (!user || !user.isMember || (user.memberExpire && user.memberExpire < new Date())) { return null; // 非会员或会员已过期 } return resource.content; } ``` ## 总结 该 MongoDB 集合设计涵盖了虚拟资源会员系统的核心需求: 1. **用户管理**:包含会员状态、登录凭证和个性化设置 2. **资源管理**:支持付费/免费资源,包含访问控制字段 3. **支付系统**:支持微信和支付宝支付,记录完整交易信息 4. **认证系统**:支持多种登录方式,包含双token刷新机制 5. **验证码系统**:支持手机和邮箱验证码登录 所有集合都包含必要的索引优化,确保系统在高并发场景下的性能。安全性方面考虑了密码加密、JWT签名验证和支付回调验证,防止常见的安全威胁。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值