node-mybatis-plus 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,426 @@
1
+ # node-mybatis-plus
2
+
3
+ MyBatis-Plus 风格的 Node.js ORM 库,为 TypeScript 开发者提供类型安全的链式查询、动态 SQL 和通用 CRUD 能力。
4
+
5
+ > 如果你用过 Java 的 MyBatis-Plus,那你已经会用了。
6
+
7
+ ## ✨ 特性
8
+
9
+ - 🔗 **Lambda 链式查询** — 类型安全的条件构造器,IDE 自动补全字段名
10
+ - 🎯 **动态 SQL** — 条件为 false 时自动跳过,告别 if-else 拼 SQL
11
+ - 🗄️ **多数据库支持** — MySQL / PostgreSQL / SQLite,一套代码切换数据库
12
+ - 📦 **通用 CRUD** — BaseMapper 内置 insert / delete / update / select 全家桶
13
+ - 💉 **声明式事务** — `@Transactional` 装饰器 + AsyncLocalStorage 自动传播
14
+ - 📝 **自定义 SQL** — `#{param}` 命名参数绑定,防 SQL 注入
15
+ - 🎨 **装饰器映射** — `@Table` `@Column` `@Id` 定义实体,自动 camelCase → snake_case
16
+ - 🔌 **插件机制** — beforeExecute / afterExecute 钩子,支持日志、审计、SQL 改写、慢查询监控
17
+
18
+ ## 📦 安装
19
+
20
+ ```bash
21
+ npm install node-mybatis-plus reflect-metadata
22
+ ```
23
+
24
+ 按需安装数据库驱动:
25
+
26
+ ```bash
27
+ # MySQL
28
+ npm install mysql2
29
+
30
+ # PostgreSQL
31
+ npm install pg
32
+
33
+ # SQLite
34
+ npm install better-sqlite3
35
+ ```
36
+
37
+ ## 🚀 快速开始
38
+
39
+ ### 1. 定义实体
40
+
41
+ ```ts
42
+ import 'reflect-metadata';
43
+ import { Table, Column, Id } from 'node-mybatis-plus';
44
+
45
+ @Table('sys_user')
46
+ class User {
47
+ @Id({ type: 'auto' })
48
+ id: number;
49
+
50
+ @Column('user_name') // 指定列名
51
+ userName: string;
52
+
53
+ @Column() // 自动转为 snake_case → age
54
+ age: number;
55
+
56
+ @Column()
57
+ email: string;
58
+ }
59
+ ```
60
+
61
+ ### 2. 创建数据源和 Mapper
62
+
63
+ ```ts
64
+ import { createDataSource, BaseMapper } from 'node-mybatis-plus';
65
+
66
+ const ds = createDataSource({
67
+ type: 'mysql', // 'mysql' | 'postgres' | 'sqlite'
68
+ host: 'localhost',
69
+ port: 3306,
70
+ database: 'test',
71
+ username: 'root',
72
+ password: '******',
73
+ });
74
+
75
+ class UserMapper extends BaseMapper<User> {}
76
+ const userMapper = new UserMapper(User, ds);
77
+ ```
78
+
79
+ ### 3. CRUD 操作
80
+
81
+ ```ts
82
+ // 新增
83
+ const id = await userMapper.insert({ userName: '张三', age: 20, email: 'zs@test.com' });
84
+
85
+ // 批量新增
86
+ await userMapper.insertBatch([
87
+ { userName: '李四', age: 25, email: 'ls@test.com' },
88
+ { userName: '王五', age: 30, email: 'ww@test.com' },
89
+ ]);
90
+
91
+ // 查询
92
+ const user = await userMapper.selectById(1);
93
+ const users = await userMapper.selectList();
94
+ const count = await userMapper.selectCount();
95
+
96
+ // 修改
97
+ await userMapper.updateById({ id: 1, age: 21 });
98
+
99
+ // 删除
100
+ await userMapper.deleteById(1);
101
+ await userMapper.deleteBatchIds([2, 3]);
102
+ ```
103
+
104
+ ### 4. Lambda 链式查询(核心亮点)
105
+
106
+ ```ts
107
+ // 基本查询
108
+ const users = await userMapper.lambdaQuery()
109
+ .eq('userName', '张三')
110
+ .ge('age', 18)
111
+ .like('email', '@gmail')
112
+ .orderByDesc('id')
113
+ .list();
114
+
115
+ // 分页查询
116
+ const page = await userMapper.lambdaQuery()
117
+ .ge('age', 18)
118
+ .orderByAsc('age')
119
+ .pageResult(1, 10);
120
+ // → { records: [...], total: 100, page: 1, size: 10, pages: 10 }
121
+
122
+ // 查询单条
123
+ const user = await userMapper.lambdaQuery()
124
+ .eq('userName', '张三')
125
+ .one();
126
+
127
+ // 查询数量
128
+ const count = await userMapper.lambdaQuery()
129
+ .ge('age', 18)
130
+ .count();
131
+ ```
132
+
133
+ ### 5. 动态条件
134
+
135
+ 第一个参数传 `boolean`,为 `false` 时自动跳过该条件:
136
+
137
+ ```ts
138
+ function searchUsers(name?: string, minAge?: number, email?: string) {
139
+ return userMapper.lambdaQuery()
140
+ .eq(name != null, 'userName', name)
141
+ .ge(minAge != null, 'age', minAge)
142
+ .like(email != null, 'email', email)
143
+ .list();
144
+ }
145
+
146
+ // searchUsers('张三') → WHERE user_name = '张三'
147
+ // searchUsers(null, 18) → WHERE age >= 18
148
+ // searchUsers('张三', 18) → WHERE user_name = '张三' AND age >= 18
149
+ // searchUsers() → SELECT * (无条件)
150
+ ```
151
+
152
+ ### 6. OR 嵌套
153
+
154
+ ```ts
155
+ const users = await userMapper.lambdaQuery()
156
+ .ge('age', 18)
157
+ .or(q => q.eq('userName', '张三').eq('userName', '李四'))
158
+ .list();
159
+ // → WHERE age >= 18 AND (user_name = '张三' OR user_name = '李四')
160
+ ```
161
+
162
+ ### 7. Lambda 更新
163
+
164
+ ```ts
165
+ await userMapper.lambdaUpdate()
166
+ .set('age', 25)
167
+ .set('email', 'new@test.com')
168
+ .eq('userName', '张三')
169
+ .execute();
170
+ // → UPDATE sys_user SET age = 25, email = 'new@test.com' WHERE user_name = '张三'
171
+
172
+ // 动态 SET
173
+ await userMapper.lambdaUpdate()
174
+ .set(newAge != null, 'age', newAge) // null 时跳过
175
+ .set(newEmail != null, 'email', newEmail) // null 时跳过
176
+ .eq('id', 1)
177
+ .execute();
178
+ ```
179
+
180
+ ### 8. 自定义 SQL
181
+
182
+ ```ts
183
+ // #{param} 命名参数,自动转为预编译占位符,防 SQL 注入
184
+ const users = await userMapper.rawQuery(
185
+ 'SELECT * FROM sys_user WHERE age > #{age} AND user_name LIKE #{name}',
186
+ { age: 18, name: '%张%' }
187
+ );
188
+ ```
189
+
190
+ ### 9. 插件机制
191
+
192
+ 插件可以在 SQL 执行前后介入,实现日志、审计、慢查询监控、数据加解密等能力。
193
+
194
+ ```ts
195
+ import type { Plugin, PluginContext } from 'node-mybatis-plus';
196
+
197
+ // 示例:SQL 日志插件
198
+ const sqlLogPlugin: Plugin = {
199
+ name: 'sql-log',
200
+ order: 0, // 越小越先执行
201
+ beforeExecute(ctx: PluginContext) {
202
+ console.log(`[SQL] ${ctx.sql}`);
203
+ console.log(`[Params] ${JSON.stringify(ctx.params)}`);
204
+ (ctx as any)._startTime = Date.now();
205
+ },
206
+ afterExecute(ctx: PluginContext, result: any) {
207
+ const cost = Date.now() - (ctx as any)._startTime;
208
+ console.log(`[SQL] 耗时 ${cost}ms,影响行数: ${Array.isArray(result) ? result.length : '?'}`);
209
+ return result; // 返回 undefined 则保持原结果不变
210
+ },
211
+ };
212
+
213
+ // 示例:慢查询告警插件
214
+ const slowQueryPlugin: Plugin = {
215
+ name: 'slow-query',
216
+ order: 10,
217
+ beforeExecute(ctx: PluginContext) {
218
+ (ctx as any)._start = Date.now();
219
+ },
220
+ afterExecute(ctx: PluginContext) {
221
+ const cost = Date.now() - (ctx as any)._start;
222
+ if (cost > 1000) {
223
+ console.warn(`[慢查询] ${cost}ms → ${ctx.sql}`);
224
+ }
225
+ },
226
+ };
227
+
228
+ // 示例:SQL 改写插件(beforeExecute 中修改 ctx.sql / ctx.params)
229
+ const tenantPlugin: Plugin = {
230
+ name: 'multi-tenant',
231
+ order: -10,
232
+ beforeExecute(ctx: PluginContext) {
233
+ // 自动追加租户条件
234
+ if (ctx.node.type === 'select' || ctx.node.type === 'update' || ctx.node.type === 'delete') {
235
+ ctx.sql = ctx.sql.includes('WHERE')
236
+ ? ctx.sql.replace('WHERE', 'WHERE tenant_id = ? AND')
237
+ : ctx.sql + ' WHERE tenant_id = ?';
238
+ ctx.params.push(getCurrentTenantId());
239
+ }
240
+ },
241
+ };
242
+ ```
243
+
244
+ 注册插件:
245
+
246
+ ```ts
247
+ const ds = createDataSource({
248
+ type: 'mysql',
249
+ database: 'test',
250
+ host: 'localhost',
251
+ username: 'root',
252
+ password: '******',
253
+ plugins: [sqlLogPlugin, slowQueryPlugin], // 传入插件数组
254
+ });
255
+ ```
256
+
257
+ 插件接口定义:
258
+
259
+ ```ts
260
+ interface PluginContext {
261
+ node: SqlNode; // SQL AST 节点(select/insert/update/delete)
262
+ sql: string; // 编译后的 SQL 字符串(可在 beforeExecute 中修改)
263
+ params: any[]; // 参数数组(可在 beforeExecute 中修改)
264
+ entityMeta: EntityMeta; // 实体元数据
265
+ }
266
+
267
+ interface Plugin {
268
+ name: string; // 插件名称
269
+ order: number; // 执行顺序,越小越先执行
270
+ beforeExecute?(ctx: PluginContext): Promise<void> | void; // SQL 执行前
271
+ afterExecute?(ctx: PluginContext, result: any): Promise<any> | any; // SQL 执行后
272
+ }
273
+ ```
274
+
275
+ 执行流程:`beforeExecute(按 order 升序) → 执行 SQL → afterExecute(按 order 升序)`
276
+
277
+ ### 10. 事务
278
+
279
+ ```ts
280
+ import { withTransaction, setDefaultDataSource, Transactional } from 'node-mybatis-plus';
281
+
282
+ setDefaultDataSource(ds);
283
+
284
+ // 方式一:编程式
285
+ await withTransaction(ds, async () => {
286
+ await userMapper.insert({ userName: '张三', age: 20, email: 'zs@test.com' });
287
+ await userMapper.lambdaUpdate().set('age', 21).eq('userName', '张三').execute();
288
+ // 正常结束 → 自动 commit
289
+ // 抛异常 → 自动 rollback
290
+ });
291
+
292
+ // 方式二:@Transactional 装饰器
293
+ class UserService {
294
+ @Transactional()
295
+ async transfer(from: string, to: string, amount: number) {
296
+ await accountMapper.lambdaUpdate()
297
+ .set('balance', fromBalance - amount)
298
+ .eq('name', from).execute();
299
+ await accountMapper.lambdaUpdate()
300
+ .set('balance', toBalance + amount)
301
+ .eq('name', to).execute();
302
+ // 方法正常结束 → commit,抛异常 → rollback
303
+ }
304
+ }
305
+ ```
306
+
307
+ 事务自动传播 — 嵌套的 `@Transactional` 方法复用外层事务连接:
308
+
309
+ ```ts
310
+ class OrderService {
311
+ @Transactional()
312
+ async createOrder(userId: number, items: Item[]) {
313
+ await orderMapper.insert(order);
314
+ await this.updateStock(items); // 复用同一个事务
315
+ }
316
+
317
+ @Transactional()
318
+ async updateStock(items: Item[]) {
319
+ // 单独调用 → 独立事务
320
+ // 被 createOrder 调用 → 复用外层事务
321
+ }
322
+ }
323
+ ```
324
+
325
+ ## 📖 API 参考
326
+
327
+ ### 条件操作符
328
+
329
+ | 方法 | SQL | 示例 |
330
+ |------|-----|------|
331
+ | `eq` | `= ?` | `.eq('name', '张三')` |
332
+ | `ne` | `!= ?` | `.ne('status', 0)` |
333
+ | `gt` | `> ?` | `.gt('age', 18)` |
334
+ | `ge` | `>= ?` | `.ge('age', 18)` |
335
+ | `lt` | `< ?` | `.lt('age', 60)` |
336
+ | `le` | `<= ?` | `.le('age', 60)` |
337
+ | `like` | `LIKE '%x%'` | `.like('name', '张')` |
338
+ | `likeLeft` | `LIKE '%x'` | `.likeLeft('name', '三')` |
339
+ | `likeRight` | `LIKE 'x%'` | `.likeRight('name', '张')` |
340
+ | `between` | `BETWEEN ? AND ?` | `.between('age', 18, 30)` |
341
+ | `in` | `IN (?, ?)` | `.in('id', [1, 2, 3])` |
342
+ | `notIn` | `NOT IN (?, ?)` | `.notIn('id', [4, 5])` |
343
+ | `isNull` | `IS NULL` | `.isNull('email')` |
344
+ | `isNotNull` | `IS NOT NULL` | `.isNotNull('email')` |
345
+ | `or` | `OR (...)` | `.or(q => q.eq(...).eq(...))` |
346
+ | `and` | `AND (...)` | `.and(q => q.ge(...).le(...))` |
347
+
348
+ 所有条件方法都支持动态条件重载:`.eq(condition, column, value)`
349
+
350
+ ### BaseMapper 方法
351
+
352
+ | 方法 | 说明 |
353
+ |------|------|
354
+ | `insert(entity)` | 新增,返回自增 ID |
355
+ | `insertBatch(entities)` | 批量新增 |
356
+ | `deleteById(id)` | 按 ID 删除 |
357
+ | `deleteBatchIds(ids)` | 批量删除 |
358
+ | `delete(wrapper)` | 条件删除 |
359
+ | `updateById(entity)` | 按 ID 更新(null 字段不更新) |
360
+ | `update(entity, wrapper)` | 条件更新 |
361
+ | `selectById(id)` | 按 ID 查询 |
362
+ | `selectBatchIds(ids)` | 批量查询 |
363
+ | `selectOne(wrapper)` | 查询单条 |
364
+ | `selectList(wrapper?)` | 查询列表 |
365
+ | `selectCount(wrapper?)` | 查询数量 |
366
+ | `selectPage(page, size, wrapper?)` | 分页查询 |
367
+ | `rawQuery(sql, params?)` | 自定义 SQL |
368
+ | `lambdaQuery()` | 获取查询构造器 |
369
+ | `lambdaUpdate()` | 获取更新构造器 |
370
+
371
+ ### 装饰器
372
+
373
+ | 装饰器 | 说明 | 示例 |
374
+ |--------|------|------|
375
+ | `@Table(name)` | 指定表名 | `@Table('sys_user')` |
376
+ | `@Id(options?)` | 主键,type: `auto`/`uuid`/`input` | `@Id({ type: 'auto' })` |
377
+ | `@Column(name?)` | 列映射,不传则自动 camelCase → snake_case | `@Column('user_name')` |
378
+ | `@Column({ exist: false })` | 标记非数据库字段 | 计算属性等 |
379
+ | `@Transactional()` | 声明式事务 | 方法装饰器 |
380
+
381
+ ### 数据源配置
382
+
383
+ ```ts
384
+ createDataSource({
385
+ type: 'mysql', // 'mysql' | 'postgres' | 'sqlite'
386
+ host: 'localhost', // 数据库地址
387
+ port: 3306, // 端口
388
+ database: 'mydb', // 数据库名(SQLite 为文件路径或 ':memory:')
389
+ username: 'root', // 用户名
390
+ password: '******', // 密码
391
+ pool: { // 连接池配置(可选)
392
+ min: 2,
393
+ max: 10,
394
+ idleTimeout: 30000,
395
+ },
396
+ plugins: [], // 插件数组(可选),详见「插件机制」章节
397
+ });
398
+ ```
399
+
400
+ ## 🏗️ 架构
401
+
402
+ ```
403
+ 用户代码 → Wrapper(条件收集) → SqlBuilder(AST构建) → Dialect(方言转换) → Plugin(插件链) → Executor(执行) → DataSource(连接池)
404
+ ```
405
+
406
+ - **Wrapper 层**:收集查询/更新条件,生成 Condition 数组
407
+ - **SqlBuilder 层**:将条件转为 SQL AST,再编译为 SQL 字符串 + 参数数组
408
+ - **Dialect 层**:处理数据库差异(占位符 `?` vs `$1`、标识符引用、分页语法等)
409
+ - **Plugin 层**:按 order 排序执行 beforeExecute → SQL 执行 → afterExecute,支持 SQL 改写、日志、审计等
410
+ - **DataSource 层**:管理连接池,执行 SQL,支持事务
411
+
412
+ ## 🧪 测试
413
+
414
+ ```bash
415
+ # 运行全部测试(SQLite 内存库,无需外部数据库)
416
+ npm test
417
+
418
+ # 运行指定测试
419
+ npx vitest run test/wrapper.test.ts
420
+ ```
421
+
422
+ 测试覆盖:207 个用例,包括单元测试(方言、装饰器、SQL 构建、条件构造器)和集成测试(SQLite + MySQL 全 CRUD、事务、自定义 SQL)。
423
+
424
+ ## 📄 License
425
+
426
+ MIT