|
1 | 1 | # monSQLize |
2 | | -monSQLize is a universal query adapter that converts various query languages (e.g., SQL for MySQL, PostgreSQL) into MongoDB syntax. It dynamically translates familiar query styles into MongoDB-compatible operations, reducing complexity, saving effort, and improving efficiency, so developers can focus on application logic. |
| 2 | + |
| 3 | +一个面向多数据库的统一(Mongo 风格)读 API。目前支持 MongoDB。目标是在不同后端之间平滑切换,同时保持熟悉的查询形态与选项。 |
| 4 | + |
| 5 | +## 目录 |
| 6 | +- [状态](#status) |
| 7 | +- [安装](#install) |
| 8 | +- [快速开始](#quick-start) |
| 9 | +- [缓存与失效](#cache) |
| 10 | + - [缓存配置](#缓存配置) |
| 11 | + - [缓存行为与细节](#缓存行为与细节) |
| 12 | + - [统计与可观测性](#统计与可观测性) |
| 13 | + - [缓存操作方法](#缓存操作方法) |
| 14 | + - [invalidate(op) 用法](#invalidate) |
| 15 | + - [进阶:手动失效](#cache-advanced) |
| 16 | +- [跨库访问注意事项](#cross-db) |
| 17 | +- [说明](#notes) |
| 18 | + |
| 19 | +<a id='status'></a> |
| 20 | +## 状态(总览) |
| 21 | + |
| 22 | +> 说明:本表统一四列(分类 | 能力 | 状态 | 备注),状态使用标记:✅ 已实现、❌ 未实现、🗺️ 计划中。 |
| 23 | +
|
| 24 | +| 分类 | 能力 | 状态 | 备注 | |
| 25 | +|---|---|---|---| |
| 26 | +| 数据库类型 | MongoDB | ✅ 已实现 | 当前唯一已实现适配器 | |
| 27 | +| 数据库类型 | PostgreSQL | 🗺️ 计划中 | 未实现 | |
| 28 | +| 数据库类型 | MySQL | 🗺️ 计划中 | 未实现 | |
| 29 | +| 数据库类型 | SQLite | 🗺️ 计划中 | 未实现 | |
| 30 | +| 数据库类型 | 查询运算符映射(operators) | 🗺️ 计划中 | 预研草案,尚未实现跨库翻译 | |
| 31 | +| 数据模型/Schema | Schema 能力 | ❌ 未实现 | 由上层应用自行约束 | |
| 32 | +| 读 API(Read) | findOne | ✅ 已实现 | 支持 projection、sort、cache、maxTimeMS | |
| 33 | +| 读 API(Read) | find | ✅ 已实现 | 支持 limit/skip 普通分页;未传 limit 使用全局 findLimit(默认 10);limit=0 表示不限制 | |
| 34 | +| 读 API(Read) | 深分页(游标/主键) | ❌ 未实现 | 计划中 | |
| 35 | +| 读 API(Read) | 链表/聚合驱动分页 | ❌ 未实现 | 计划中 | |
| 36 | +| 读 API(Read) | count | ✅ 已实现 | 统计匹配文档数 | |
| 37 | +| 读 API(Read) | stream(find 流式返回) | ❌ 未实现 | 计划中 | |
| 38 | +| 读 API(Read) | 聚合(aggregate/或 find 支持聚合) | ❌ 未实现 | 后续可能透传或翻译 | |
| 39 | +| 缓存与失效 | 内置内存缓存 | ✅ 已实现 | 读穿、TTL(毫秒)、LRU、惰性过期、并发去重 | |
| 40 | +| 缓存与失效 | 稳定序列化键 | ✅ 已实现 | 支持常见 BSON;keys()/delPattern();统计(enableStats 可选) | |
| 41 | +| 缓存与失效 | 命名空间与精准失效 | ✅ 已实现 | collection.invalidate(op?);getNamespace() | |
| 42 | +| 缓存与失效 | 多层缓存(本地+远端) | 🗺️ 计划中 | 未实现 | |
| 43 | +| 跨库访问 | 跨库读与失效 | ✅ 已实现 | db('<目标库>').collection('<集合>') 支持 find/findOne/count/invalidate | |
| 44 | +| 超时与慢日志 | 全局默认值 | ✅ 已实现 | maxTimeMS、findLimit 构造时设定,单次可覆盖 | |
| 45 | +| 超时与慢日志 | 慢查询日志 | ✅ 已实现 | slowQueryMs(默认 500ms);日志包含安全字段与查询形状(无敏感值) | |
| 46 | +| 类型与接口 | TypeScript 类型声明 | ✅ 已实现 | index.d.ts;含 CacheLike、Find/Count、getNamespace、getDefaults | |
| 47 | +| 类型与接口 | getDefaults() | ✅ 已实现 | 返回当前实例默认配置视图 | |
| 48 | +| 类型与接口 | 模块格式 | 🗺️ 计划中 | 目前 CJS;ESM 条件导出未实现 | |
| 49 | +| 连接与运维 | connect/close | ✅ 已实现 | 连接与关闭 | |
| 50 | +| 连接与运维 | 健康检查/事件钩子 | 🗺️ 计划中 | 未实现 | |
| 51 | +| 写相关辅助 | createCollection/createView/dropCollection | ✅ 已实现 | Mongo 适配器功能 | |
| 52 | +| 写相关辅助 | 写后读缓存一致性 | 手动 | 不自动失效,建议写后调用 collection.invalidate(op?) | |
| 53 | +| 其他 | 安全默认 | ✅ 已实现 | find 未指定 limit 使用全局 findLimit;limit=0 表示不限制 | |
| 54 | +| 其他 | 命名空间 instanceId | ✅ 已实现 | 可显式指定或自动生成;scope 支持 database/connection | |
| 55 | + |
| 56 | +<a id='install'></a> |
| 57 | +## 安装 |
| 58 | +``` |
| 59 | +npm i monsqlize |
| 60 | +``` |
| 61 | + |
| 62 | +<a id='quick-start'></a> |
| 63 | +## 快速开始(含默认配置与自动 instanceId) |
| 64 | +```js |
| 65 | +const MonSQLize = require('monsqlize'); |
| 66 | +(async () => { |
| 67 | + const { db, collection } = await (new MonSQLize({ |
| 68 | + type: 'mongodb', |
| 69 | + databaseName: 'example', |
| 70 | + config: { uri: 'mongodb://localhost:27017' }, |
| 71 | + maxTimeMS: 3000, //全局默认配置(本实例的默认 maxTimeMS) |
| 72 | + findLimit:10, // 分成查询每页数量,默认:10 |
| 73 | + }).connect()); |
| 74 | + |
| 75 | + // 单次查询可覆盖 maxTimeMS;cache 为毫秒 |
| 76 | + const one = await collection('test').findOne({ query: {}, cache: 5000, maxTimeMS: 1500 }); |
| 77 | + console.log(one); |
| 78 | + |
| 79 | + // find 的安全默认:未传 limit 时使用全局 findLimit(默认 10);传 0 表示不限制 |
| 80 | + const list = await collection('test').find({ query: {} }); // 等效 limit=10 |
| 81 | + const all = await collection('test').find({ query: {}, limit: 0 }); // 不限制 |
| 82 | + |
| 83 | + // 写后失效(可选 op:'find' | 'findOne' | 'count') |
| 84 | + await collection('test').invalidate(); // 失效该集合的全部读缓存 |
| 85 | + await collection('test').invalidate('find'); // 仅失效 find 的缓存 |
| 86 | + |
| 87 | + // —— 跨库访问(Cross-DB)—— |
| 88 | + // 1) 访问其他数据库下的集合 |
| 89 | + const docOther = await db('analytics').collection('events').findOne({ |
| 90 | + query: { type: 'click' }, |
| 91 | + cache: 3000, // 可选缓存(毫秒) |
| 92 | + maxTimeMS: 1500 // 单次查询的超时覆盖 |
| 93 | + }); |
| 94 | + console.log('analytics.events ->', docOther); |
| 95 | + |
| 96 | + // 2) 在同一调用中进行多次跨库查询(顺序执行) |
| 97 | + const [u1, u2] = [ |
| 98 | + await db('users_db').collection('users').findOne({ query: { name: 'Alice' }, cache: 2000 }), |
| 99 | + await db('users_db').collection('users').findOne({ query: { name: 'Bob' } }) |
| 100 | + ]; |
| 101 | + console.log(u1, u2); |
| 102 | +})(); |
| 103 | +``` |
| 104 | + |
| 105 | +<a id='cache'></a> |
| 106 | +## 缓存与失效 |
| 107 | + |
| 108 | +- 默认提供内存缓存(LRU + 惰性过期),也可传入自定义缓存实现(需实现标准接口:get/set/del/delPattern/keys 等)。 |
| 109 | +- 读穿(read-through)策略:当 options.cache>0 时开启缓存;0 或未传则直连数据库。 |
| 110 | +- TTL 单位为毫秒;允许缓存 null(仅将 undefined 视为未命中)。 |
| 111 | +- 键采用稳定序列化,确保同一查询结构产生相同键(含常见 BSON 类型)。 |
| 112 | + |
| 113 | +### 缓存配置 |
| 114 | +- 方式一:传入“配置对象”,自动创建内置内存缓存 |
| 115 | +```js |
| 116 | +const { db, collection } = await new MonSQLize({ |
| 117 | + type: 'mongodb', |
| 118 | + databaseName: 'example', |
| 119 | + config: { uri: 'mongodb://localhost:27017' }, |
| 120 | + // 缓存配置(创建默认内存缓存实例) |
| 121 | + cache: { |
| 122 | + maxSize: 100000, // 最大键数量(默认 100000) |
| 123 | + maxMemory: 0, // 最大内存占用(字节);0 表示不限制 |
| 124 | + enableStats: true, // 是否启用命中率等统计(默认 true) |
| 125 | + }, |
| 126 | + // 全局查询默认值 |
| 127 | + maxTimeMS: 3000, |
| 128 | + findLimit: 10, |
| 129 | +}).connect(); |
| 130 | +``` |
| 131 | + |
| 132 | +- 方式二:注入自定义缓存实例(需实现 CacheLike 接口) |
| 133 | +```ts |
| 134 | +// TypeScript 接口(简化),见 index.d.ts 的 CacheLike |
| 135 | +interface CacheLike { |
| 136 | + get(key: string): Promise<any>; |
| 137 | + set(key: string, val: any, ttl?: number): Promise<void>; |
| 138 | + del(key: string): Promise<boolean>; |
| 139 | + exists(key: string): Promise<boolean>; |
| 140 | + getMany(keys: string[]): Promise<Record<string, any>>; |
| 141 | + setMany(obj: Record<string, any>, ttl?: number): Promise<boolean>; |
| 142 | + delMany(keys: string[]): Promise<number>; |
| 143 | + delPattern(pattern: string): Promise<number>; |
| 144 | + clear(): void; |
| 145 | + keys(pattern?: string): string[]; |
| 146 | + getStats?(): any; |
| 147 | +} |
| 148 | +``` |
| 149 | +```js |
| 150 | +// 注入自定义实现(例如封装 Redis/Memcached/本地 LRU 等),只要方法签名一致即可 |
| 151 | +// 假设已获得自定义缓存实例 |
| 152 | +const customCache = getCustomCache(); |
| 153 | +const msq = new MonSQLize({ |
| 154 | + type: 'mongodb', |
| 155 | + databaseName: 'example', |
| 156 | + config: { uri: 'mongodb://localhost:27017' }, |
| 157 | + cache: customCache, |
| 158 | +}); |
| 159 | +await msq.connect(); |
| 160 | +``` |
| 161 | + |
| 162 | +- 每次查询是否使用缓存由“查询级 options.cache”决定: |
| 163 | + - >0:使用读穿缓存,单位毫秒(TTL) |
| 164 | + - 0 或未传:绕过缓存,直连数据库 |
| 165 | + |
| 166 | +### 缓存行为与细节 |
| 167 | +- 读穿(read-through):首次未命中会执行实际查询,结果写入缓存;命中直接返回缓存。 |
| 168 | +- 并发去重:相同键的并发请求共享同一 Promise,避免对后端的 thundering herd(参见 lib/cache.js 中 __inflight 逻辑)。 |
| 169 | +- LRU 淘汰:超出 maxSize 或 maxMemory 时,从最久未使用的键开始淘汰。 |
| 170 | +- TTL 精度:以毫秒为单位;采用“惰性过期”,即在读取/扫描时才判断过期、并清理键。 |
| 171 | +- 值语义:允许缓存 null;仅将 undefined 视为未命中(便于明确区分“确无数据”和“未缓存”)。 |
| 172 | +- 键生成:采用稳定序列化(stableStringify),对象键排序、数组保序;内置支持常见 BSON 类型(ObjectId、Decimal128、Long、UUID、Binary)。 |
| 173 | +- 内存估算:内置缓存对 value 进行粗略 size 估算(JSON.stringify 长度等),仅用于淘汰策略,并非精确内存计量。 |
| 174 | + |
| 175 | +### 统计与可观测性 |
| 176 | +- 获取底层缓存实例,并查看统计/键: |
| 177 | +```js |
| 178 | +const msq = new MonSQLize({ /* ... */ }); |
| 179 | +await msq.connect(); |
| 180 | +const cache = msq.getCache(); |
| 181 | + |
| 182 | +// 命中率与基本统计(需 enableStats=true) |
| 183 | +const stats = cache.getStats && cache.getStats(); |
| 184 | +console.log('cache stats:', stats); |
| 185 | + |
| 186 | +// 列出当前所有键(或按简单通配过滤) |
| 187 | +console.log(cache.keys()); // 全部键 |
| 188 | +console.log(cache.keys('*users*')); // 仅包含 users 的键 |
| 189 | +``` |
| 190 | +- 手动清理: |
| 191 | +```js |
| 192 | +cache.clear(); // 清空全部缓存(谨慎使用) |
| 193 | +await cache.delPattern('*x*'); // 通配删除,复杂场景建议使用更强的外部缓存 |
| 194 | +``` |
| 195 | + |
| 196 | +<a id='缓存操作方法'></a> |
| 197 | +### 缓存操作方法 |
| 198 | +- 以下方法由内置内存缓存与自定义 CacheLike 实现共同支持。先通过实例方法 `getCache()` 获取缓存对象: |
| 199 | +```js |
| 200 | +const msq = new MonSQLize({ /* ... */ }); |
| 201 | +await msq.connect(); |
| 202 | +const cache = msq.getCache(); |
| 203 | +``` |
| 204 | + |
| 205 | +- 方法速览: |
| 206 | + - `get(key: string): Promise<any>` 获取键值(未命中返回 undefined;允许缓存 null)。 |
| 207 | + - `set(key: string, val: any, ttl?: number): Promise<void>` 设置键值与可选 TTL(毫秒)。 |
| 208 | + - `del(key: string): Promise<boolean>` 删除单个键。 |
| 209 | + - `exists(key: string): Promise<boolean>` 判断键是否存在且未过期。 |
| 210 | + - `getMany(keys: string[]): Promise<Record<string, any>>` 批量获取。 |
| 211 | + - `setMany(obj: Record<string, any>, ttl?: number): Promise<boolean>` 批量设置。 |
| 212 | + - `delMany(keys: string[]): Promise<number>` 批量删除,返回删除数量。 |
| 213 | + - `delPattern(pattern: string): Promise<number>` 按通配模式删除(内存实现为 O(N) 扫描)。 |
| 214 | + - `clear(): void` 清空所有键(谨慎使用)。 |
| 215 | + - `keys(pattern?: string): string[]` 列出键(可选通配模式)。 |
| 216 | + - `getStats?(): any` 返回命中率、淘汰数、内存估算等统计(若启用)。 |
| 217 | + |
| 218 | +- 使用示例: |
| 219 | +```js |
| 220 | +// 单键 set/get |
| 221 | +await cache.set('foo', { a: 1 }, 5000); // TTL 5s |
| 222 | +const foo = await cache.get('foo'); // => { a: 1 } |
| 223 | + |
| 224 | +// 判断存在/删除 |
| 225 | +const ok = await cache.exists('foo'); // true/false |
| 226 | +await cache.del('foo'); |
| 227 | + |
| 228 | +// 批量操作 |
| 229 | +await cache.setMany({ |
| 230 | + 'k:1': { id: 1 }, |
| 231 | + 'k:2': { id: 2 }, |
| 232 | +}, 3000); |
| 233 | +const many = await cache.getMany(['k:1', 'k:2']); |
| 234 | +const removed = await cache.delMany(['k:1', 'k:2']); |
| 235 | + |
| 236 | +// 模式删除与列举 |
| 237 | +await cache.delPattern('*users*'); |
| 238 | +const userKeys = cache.keys('*users*'); |
| 239 | + |
| 240 | +// 统计(需 enableStats) |
| 241 | +const stats = cache.getStats && cache.getStats(); |
| 242 | +console.log(stats); |
| 243 | +``` |
| 244 | + |
| 245 | +<a id='invalidate'></a> |
| 246 | +### invalidate(op) 用法 |
| 247 | +- 作用:失效目标集合在当前命名空间(iid/type/db/collection)下的读缓存。 |
| 248 | +- op 可选:'find' | 'findOne' | 'count'。不传 op 表示失效该集合的全部读缓存。 |
| 249 | +- 示例: |
| 250 | +```js |
| 251 | +await collection('users').invalidate(); // 删除 users 集合所有读缓存 |
| 252 | +await collection('users').invalidate('find'); // 仅删除 find 相关缓存 |
| 253 | +``` |
| 254 | +- 典型时机:写操作(insert/update/delete/bulk)后调用,以保证读缓存与数据库一致。 |
| 255 | + |
| 256 | +<a id='cache-advanced'></a> |
| 257 | +### 进阶:手动失效 |
| 258 | +- 建议优先使用集合访问器的 invalidate(op?) 进行失效。 |
| 259 | +- 如需更粗粒度的手动操作,可通过实例方法 getCache() 获取底层缓存实例: |
| 260 | +```js |
| 261 | +const msq = new MonSQLize({ /* ... */ }); |
| 262 | +await msq.connect(); |
| 263 | +const cache = msq.getCache(); |
| 264 | + |
| 265 | +// 清空全部缓存(谨慎使用) |
| 266 | +cache.clear(); |
| 267 | + |
| 268 | +// 按简单模式删除包含某集合名的键(实现为通配匹配) |
| 269 | +await cache.delPattern('*users*'); |
| 270 | +``` |
| 271 | +- 注意:底层内存缓存的 delPattern 为 O(N) 扫描,适合中小规模场景;重型场景可替换为外部缓存实现。 |
| 272 | + |
| 273 | +<a id='cross-db'></a> |
| 274 | +## 跨库访问注意事项 |
| 275 | +- 快速入门的跨库示例与注意点已在「快速开始」中给出,推荐优先阅读该小节。 |
| 276 | +- 速查清单: |
| 277 | + - 用法:`db('<目标库名>').collection('<集合名>')`,返回集合读访问器,支持 find/findOne/count/invalidate。 |
| 278 | + - 失效:跨库失效仍可用集合访问器的 `invalidate(op?)`;或使用 getCache().delPattern 进行批量模式失效。 |
| 279 | + - 性能:跨库访问与同库一致;建议为高频跨库查询设置合适的 `cache` TTL 与 `maxTimeMS`。 |
| 280 | + - 权限:确保连接账号具备目标库的读权限;否则驱动层会抛出权限错误。 |
| 281 | + |
| 282 | +<a id='notes'></a> |
| 283 | +## 说明 |
| 284 | +- 全局 maxTimeMS 为默认值,单次 options.maxTimeMS 优先。 |
| 285 | +- 全局 findLimit 配置:构造时传入 findLimit(默认 10)。 |
| 286 | +- 缓存键稳定序列化已支持常见 BSON 类型(ObjectId、Decimal128、Long、UUID、Binary)。 |
| 287 | + |
| 288 | +欢迎 PR。 |
| 289 | + |
| 290 | + |
| 291 | +### 辅助方法与慢查询日志 |
| 292 | +- 获取默认配置(全局 maxTimeMS、findLimit、namespace、slowQueryMs): |
| 293 | +```js |
| 294 | +const msq = new MonSQLize({ |
| 295 | + type: 'mongodb', |
| 296 | + databaseName: 'example', |
| 297 | + config: { uri: 'mongodb://localhost:27017' }, |
| 298 | + slowQueryMs: 800 |
| 299 | +}); |
| 300 | +await msq.connect(); |
| 301 | +console.log(msq.getDefaults()); |
| 302 | +``` |
| 303 | +- 获取集合访问器的命名空间(便于调试与手动失效脚本): |
| 304 | +```js |
| 305 | +const ns = db('example').collection('users').getNamespace(); |
| 306 | +// => { iid, type: 'mongodb', db: 'example', collection: 'users' } |
| 307 | +``` |
| 308 | +- 慢查询日志:findOne/find/find/count 会在一次调用耗时超过 slowQueryMs(默认为 500ms)时输出 warn 日志。 |
0 commit comments