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 +426 -0
- package/dist/index.d.mts +302 -0
- package/dist/index.d.ts +302 -0
- package/dist/index.js +1051 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +1006 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +72 -0
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
|