Skip to content

Commit 02a50e7

Browse files
committed
feat: impl dal transaction
1 parent 5ad6b48 commit 02a50e7

File tree

22 files changed

+1787
-2
lines changed

22 files changed

+1787
-2
lines changed

core/dal-runtime/src/MySqlDataSource.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,4 +43,8 @@ export class MysqlDataSource extends Base {
4343
async query<T = any>(sql: string): Promise<T> {
4444
return this.client.query(sql);
4545
}
46+
47+
async beginTransactionScope<T>(scope: () => Promise<T>): Promise<T> {
48+
return await this.client.beginTransactionScope(scope);
49+
}
4650
}

plugin/dal/app.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,13 @@ import { MysqlDataSourceManager } from './lib/MysqlDataSourceManager';
66
import { SqlMapManager } from './lib/SqlMapManager';
77
import { TableModelManager } from './lib/TableModelManager';
88
import { DalModuleLoadUnitHook } from './lib/DalModuleLoadUnitHook';
9+
import { TransactionPrototypeHook } from './lib/TransactionPrototypeHook';
910

1011
export default class ControllerAppBootHook {
1112
private readonly app: Application;
1213
private dalTableEggPrototypeHook: DalTableEggPrototypeHook;
1314
private dalModuleLoadUnitHook: DalModuleLoadUnitHook;
15+
private transactionPrototypeHook: TransactionPrototypeHook;
1416

1517
constructor(app: Application) {
1618
this.app = app;
@@ -19,7 +21,9 @@ export default class ControllerAppBootHook {
1921
configWillLoad() {
2022
this.dalModuleLoadUnitHook = new DalModuleLoadUnitHook(this.app.config.env, this.app.moduleConfigs);
2123
this.dalTableEggPrototypeHook = new DalTableEggPrototypeHook(this.app.logger);
24+
this.transactionPrototypeHook = new TransactionPrototypeHook(this.app.moduleConfigs, this.app.logger);
2225
this.app.eggPrototypeLifecycleUtil.registerLifecycle(this.dalTableEggPrototypeHook);
26+
this.app.eggPrototypeLifecycleUtil.registerLifecycle(this.transactionPrototypeHook);
2327
this.app.loadUnitLifecycleUtil.registerLifecycle(this.dalModuleLoadUnitHook);
2428
}
2529

@@ -30,6 +34,9 @@ export default class ControllerAppBootHook {
3034
if (this.dalModuleLoadUnitHook) {
3135
this.app.loadUnitLifecycleUtil.deleteLifecycle(this.dalModuleLoadUnitHook);
3236
}
37+
if (this.transactionPrototypeHook) {
38+
this.app.eggPrototypeLifecycleUtil.deleteLifecycle(this.transactionPrototypeHook);
39+
}
3340
MysqlDataSourceManager.instance.clear();
3441
SqlMapManager.instance.clear();
3542
TableModelManager.instance.clear();
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import assert from 'assert';
2+
import { LifecycleHook, ModuleConfigHolder, Logger } from '@eggjs/tegg';
3+
import { EggPrototype, EggPrototypeLifecycleContext } from '@eggjs/tegg-metadata';
4+
import { PropagationType, TransactionMetaBuilder } from '@eggjs/tegg/transaction';
5+
import { Pointcut } from '@eggjs/tegg/aop';
6+
import { TransactionalAOP, TransactionalParams } from './TransactionalAOP';
7+
import { MysqlDataSourceManager } from './MysqlDataSourceManager';
8+
9+
export class TransactionPrototypeHook implements LifecycleHook<EggPrototypeLifecycleContext, EggPrototype> {
10+
private readonly moduleConfigs: Record<string, ModuleConfigHolder>;
11+
private readonly logger: Logger;
12+
13+
constructor(moduleConfigs: Record<string, ModuleConfigHolder>, logger: Logger) {
14+
this.moduleConfigs = moduleConfigs;
15+
this.logger = logger;
16+
}
17+
18+
public async preCreate(ctx: EggPrototypeLifecycleContext): Promise<void> {
19+
const builder = new TransactionMetaBuilder(ctx.clazz);
20+
const transactionMetadataList = builder.build();
21+
if (transactionMetadataList.length < 1) {
22+
return;
23+
}
24+
const moduleName = ctx.loadUnit.name;
25+
for (const transactionMetadata of transactionMetadataList) {
26+
const clazzName = `${moduleName}.${ctx.clazz.name}.${String(transactionMetadata.method)}`;
27+
const datasourceConfigs = (this.moduleConfigs[moduleName]?.config as any)?.dataSource || {};
28+
29+
let datasourceName: string;
30+
if (transactionMetadata.datasourceName) {
31+
assert(datasourceConfigs[transactionMetadata.datasourceName], `method ${clazzName} specified datasource ${transactionMetadata.datasourceName} not exists`);
32+
datasourceName = transactionMetadata.datasourceName;
33+
this.logger.info(`use datasource [${transactionMetadata.datasourceName}] for class ${clazzName}`);
34+
} else {
35+
const dataSources = Object.keys(datasourceConfigs);
36+
if (dataSources.length === 1) {
37+
datasourceName = dataSources[0];
38+
} else {
39+
throw new Error(`method ${clazzName} not specified datasource, module ${moduleName} has multi datasource, should specify datasource name`);
40+
}
41+
this.logger.info(`use default datasource ${dataSources[0]} for class ${clazzName}`);
42+
}
43+
const adviceParams: TransactionalParams = {
44+
propagation: transactionMetadata.propagation,
45+
dataSourceGetter: () => {
46+
const mysqlDataSource = MysqlDataSourceManager.instance.get(moduleName, datasourceName);
47+
if (!mysqlDataSource) {
48+
throw new Error(`method ${clazzName} not found datasource ${datasourceName}`);
49+
}
50+
return mysqlDataSource;
51+
},
52+
};
53+
assert(adviceParams.propagation === PropagationType.REQUIRED, 'Transactional propagation only support required for now');
54+
Pointcut(TransactionalAOP, { adviceParams })((ctx.clazz as any).prototype, transactionMetadata.method);
55+
}
56+
}
57+
58+
}

plugin/dal/lib/TransactionalAOP.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { Advice, AdviceContext, IAdvice } from '@eggjs/tegg/aop';
2+
import { AccessLevel, EggProtoImplClass, ObjectInitType } from '@eggjs/tegg';
3+
import { PropagationType } from '@eggjs/tegg/transaction';
4+
import assert from 'node:assert';
5+
import { MysqlDataSource } from '@eggjs/dal-runtime';
6+
7+
export interface TransactionalParams {
8+
propagation: PropagationType;
9+
dataSourceGetter: () => MysqlDataSource;
10+
}
11+
12+
@Advice({
13+
accessLevel: AccessLevel.PUBLIC,
14+
initType: ObjectInitType.SINGLETON,
15+
})
16+
export class TransactionalAOP implements IAdvice<EggProtoImplClass, TransactionalParams> {
17+
public async around(ctx: AdviceContext<EggProtoImplClass, TransactionalParams>, next: () => Promise<any>): Promise<void> {
18+
const { propagation, dataSourceGetter } = ctx.adviceParams!;
19+
const dataSource = dataSourceGetter();
20+
assert(propagation === PropagationType.REQUIRED, '事务注解目前只支持 REQUIRED 机制');
21+
return await dataSource.beginTransactionScope(next);
22+
}
23+
}

plugin/dal/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@
5555
"devDependencies": {
5656
"@eggjs/tegg-config": "^3.37.3",
5757
"@eggjs/tegg-plugin": "^3.37.3",
58+
"@eggjs/tegg-aop-plugin": "^3.37.3",
5859
"@types/mocha": "^10.0.1",
5960
"@types/node": "^20.2.4",
6061
"cross-env": "^7.0.3",

plugin/dal/test/fixtures/apps/dal-app/config/plugin.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,8 @@ exports.teggConfig = {
1414
package: '@eggjs/tegg-config',
1515
enable: true,
1616
};
17+
18+
exports.aopModule = {
19+
enable: true,
20+
package: '@eggjs/tegg-aop-plugin',
21+
};

plugin/dal/test/fixtures/apps/dal-app/modules/dal/Foo.ts

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -294,4 +294,84 @@ export class Foo {
294294
type: ColumnType.JSON,
295295
})
296296
jsonColumn: object;
297+
298+
static buildObj() {
299+
const foo = new Foo();
300+
foo.name = 'name';
301+
foo.col1 = 'col1';
302+
foo.bitColumn = Buffer.from([ 0, 0 ]);
303+
foo.boolColumn = 0;
304+
foo.tinyIntColumn = 0;
305+
foo.smallIntColumn = 1;
306+
foo.mediumIntColumn = 3;
307+
foo.intColumn = 3;
308+
foo.bigIntColumn = '00099';
309+
foo.decimalColumn = '00002.33333';
310+
foo.floatColumn = 2.3;
311+
foo.doubleColumn = 2.3;
312+
foo.dateColumn = new Date('2020-03-15T16:00:00.000Z');
313+
foo.dateTimeColumn = new Date('2024-03-16T01:26:58.677Z');
314+
foo.timestampColumn = new Date('2024-03-16T01:26:58.677Z');
315+
foo.timeColumn = '838:59:50.123';
316+
foo.yearColumn = 2024;
317+
foo.varCharColumn = 'var_char';
318+
foo.binaryColumn = Buffer.from('b');
319+
foo.varBinaryColumn = Buffer.from('var_binary');
320+
foo.tinyBlobColumn = Buffer.from('tiny_blob');
321+
foo.tinyTextColumn = 'text';
322+
foo.blobColumn = Buffer.from('blob');
323+
foo.textColumn = 'text';
324+
foo.mediumBlobColumn = Buffer.from('medium_blob');
325+
foo.longBlobColumn = Buffer.from('long_blob');
326+
foo.mediumTextColumn = 'medium_text';
327+
foo.longTextColumn = 'long_text';
328+
foo.enumColumn = 'A';
329+
foo.setColumn = 'B';
330+
foo.geometryColumn = { x: 10, y: 10 };
331+
foo.pointColumn = { x: 10, y: 10 };
332+
foo.lineStringColumn = [
333+
{ x: 15, y: 15 },
334+
{ x: 20, y: 20 },
335+
];
336+
foo.polygonColumn = [
337+
[
338+
{ x: 0, y: 0 }, { x: 10, y: 0 }, { x: 10, y: 10 }, { x: 0, y: 10 }, { x: 0, y: 0 },
339+
], [
340+
{ x: 5, y: 5 }, { x: 7, y: 5 }, { x: 7, y: 7 }, { x: 5, y: 7 }, { x: 5, y: 5 },
341+
],
342+
];
343+
foo.multipointColumn = [
344+
{ x: 0, y: 0 }, { x: 20, y: 20 }, { x: 60, y: 60 },
345+
];
346+
foo.multiLineStringColumn = [
347+
[
348+
{ x: 10, y: 10 }, { x: 20, y: 20 },
349+
], [
350+
{ x: 15, y: 15 }, { x: 30, y: 15 },
351+
],
352+
];
353+
foo.multiPolygonColumn = [
354+
[
355+
[
356+
{ x: 0, y: 0 }, { x: 10, y: 0 }, { x: 10, y: 10 }, { x: 0, y: 10 }, { x: 0, y: 0 },
357+
],
358+
],
359+
[
360+
[
361+
{ x: 5, y: 5 }, { x: 7, y: 5 }, { x: 7, y: 7 }, { x: 5, y: 7 }, { x: 5, y: 5 },
362+
],
363+
],
364+
];
365+
foo.geometryCollectionColumn = [
366+
{ x: 10, y: 10 },
367+
{ x: 30, y: 30 },
368+
[
369+
{ x: 15, y: 15 }, { x: 20, y: 20 },
370+
],
371+
];
372+
foo.jsonColumn = {
373+
hello: 'json',
374+
};
375+
return foo;
376+
}
297377
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { AccessLevel, Inject, SingletonProto } from '@eggjs/tegg';
2+
import { Transactional } from '@eggjs/tegg/transaction';
3+
import FooDAO from './dal/dao/FooDAO';
4+
import { Foo } from './Foo';
5+
6+
@SingletonProto({
7+
accessLevel: AccessLevel.PUBLIC,
8+
})
9+
export class FooService {
10+
@Inject()
11+
private readonly fooDAO: FooDAO;
12+
13+
@Transactional()
14+
async succeedTransaction() {
15+
const foo = Foo.buildObj();
16+
foo.name = 'insert_succeed_transaction_1';
17+
const foo2 = Foo.buildObj();
18+
foo2.name = 'insert_succeed_transaction_2';
19+
await this.fooDAO.insert(foo);
20+
await this.fooDAO.insert(foo2);
21+
}
22+
23+
@Transactional()
24+
async failedTransaction() {
25+
const foo = Foo.buildObj();
26+
foo.name = 'insert_failed_transaction_1';
27+
const foo2 = Foo.buildObj();
28+
foo2.name = 'insert_failed_transaction_2';
29+
await this.fooDAO.insert(foo);
30+
await this.fooDAO.insert(foo2);
31+
throw new Error('mock error');
32+
}
33+
}
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import assert from 'assert';
2+
import path from 'path';
3+
import mm, { MockApplication } from 'egg-mock';
4+
import FooDAO from './fixtures/apps/dal-app/modules/dal/dal/dao/FooDAO';
5+
import { FooService } from './fixtures/apps/dal-app/modules/dal/FooService';
6+
import { MysqlDataSourceManager } from '../lib/MysqlDataSourceManager';
7+
8+
describe('plugin/dal/test/transaction.test.ts', () => {
9+
let app: MockApplication;
10+
11+
afterEach(async () => {
12+
mm.restore();
13+
});
14+
15+
before(async () => {
16+
mm(process.env, 'EGG_TYPESCRIPT', true);
17+
mm(process, 'cwd', () => {
18+
return path.join(__dirname, '../');
19+
});
20+
app = mm.app({
21+
baseDir: path.join(__dirname, './fixtures/apps/dal-app'),
22+
framework: require.resolve('egg'),
23+
});
24+
await app.ready();
25+
});
26+
27+
afterEach(async () => {
28+
const dataSource = MysqlDataSourceManager.instance.get('dal', 'foo')!;
29+
await dataSource.query('delete from egg_foo;');
30+
});
31+
32+
after(() => {
33+
return app.close();
34+
});
35+
36+
describe('succeed transaction', () => {
37+
it('should commit', async () => {
38+
await app.mockModuleContextScope(async () => {
39+
const fooService = await app.getEggObject(FooService);
40+
const fooDao = await app.getEggObject(FooDAO);
41+
await fooService.succeedTransaction();
42+
const foo1 = await fooDao.findByName('insert_succeed_transaction_1');
43+
const foo2 = await fooDao.findByName('insert_succeed_transaction_2');
44+
assert(foo1.length);
45+
assert(foo2.length);
46+
});
47+
});
48+
});
49+
50+
describe('failed transaction', () => {
51+
it('should rollback', async () => {
52+
await app.mockModuleContextScope(async () => {
53+
const fooService = await app.getEggObject(FooService);
54+
const fooDao = await app.getEggObject(FooDAO);
55+
await assert.rejects(async () => {
56+
await fooService.failedTransaction();
57+
});
58+
const foo1 = await fooDao.findByName('insert_failed_transaction_1');
59+
const foo2 = await fooDao.findByName('insert_failed_transaction_2');
60+
assert(!foo1.length);
61+
assert(!foo2.length);
62+
});
63+
});
64+
});
65+
66+
describe('transaction should be isolated', () => {
67+
it('should rollback', async () => {
68+
await app.mockModuleContextScope(async () => {
69+
const fooService = await app.getEggObject(FooService);
70+
const fooDao = await app.getEggObject(FooDAO);
71+
const [ failedRes, succeedRes ] = await Promise.allSettled([
72+
fooService.failedTransaction(),
73+
fooService.succeedTransaction(),
74+
]);
75+
assert.equal(failedRes.status, 'rejected');
76+
assert.equal(succeedRes.status, 'fulfilled');
77+
const foo1 = await fooDao.findByName('insert_failed_transaction_1');
78+
const foo2 = await fooDao.findByName('insert_failed_transaction_2');
79+
assert(!foo1.length);
80+
assert(!foo2.length);
81+
82+
const foo3 = await fooDao.findByName('insert_succeed_transaction_1');
83+
const foo4 = await fooDao.findByName('insert_succeed_transaction_2');
84+
assert(foo3.length);
85+
assert(foo4.length);
86+
});
87+
});
88+
});
89+
});

standalone/standalone/src/Runner.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import { DalModuleLoadUnitHook } from '@eggjs/tegg-dal-plugin/lib/DalModuleLoadU
3535
import { MysqlDataSourceManager } from '@eggjs/tegg-dal-plugin/lib/MysqlDataSourceManager';
3636
import { SqlMapManager } from '@eggjs/tegg-dal-plugin/lib/SqlMapManager';
3737
import { TableModelManager } from '@eggjs/tegg-dal-plugin/lib/TableModelManager';
38+
import { TransactionPrototypeHook } from '@eggjs/tegg-dal-plugin/lib/TransactionPrototypeHook';
3839

3940
export interface ModuleDependency extends ReadModuleReferenceOptions {
4041
baseDir: string;
@@ -64,6 +65,7 @@ export class Runner {
6465
private loadUnitMultiInstanceProtoHook: LoadUnitMultiInstanceProtoHook;
6566
private dalTableEggPrototypeHook: DalTableEggPrototypeHook;
6667
private dalModuleLoadUnitHook: DalModuleLoadUnitHook;
68+
private transactionPrototypeHook: TransactionPrototypeHook;
6769

6870
private readonly loadUnitInnerClassHook: LoadUnitInnerClassHook;
6971
private readonly crosscutAdviceFactory: CrosscutAdviceFactory;
@@ -156,9 +158,11 @@ export class Runner {
156158

157159
this.dalModuleLoadUnitHook = new DalModuleLoadUnitHook(this.env ?? '', this.moduleConfigs);
158160
const loggerInnerObject = this.innerObjects.logger && this.innerObjects.logger[0];
159-
const logger = loggerInnerObject?.obj || console;
160-
this.dalTableEggPrototypeHook = new DalTableEggPrototypeHook(logger as Logger);
161+
const logger = (loggerInnerObject?.obj || console) as Logger;
162+
this.dalTableEggPrototypeHook = new DalTableEggPrototypeHook(logger);
163+
this.transactionPrototypeHook = new TransactionPrototypeHook(this.moduleConfigs, logger);
161164
EggPrototypeLifecycleUtil.registerLifecycle(this.dalTableEggPrototypeHook);
165+
EggPrototypeLifecycleUtil.registerLifecycle(this.transactionPrototypeHook);
162166
LoadUnitLifecycleUtil.registerLifecycle(this.dalModuleLoadUnitHook);
163167
}
164168

@@ -257,6 +261,9 @@ export class Runner {
257261
if (this.dalModuleLoadUnitHook) {
258262
LoadUnitLifecycleUtil.deleteLifecycle(this.dalModuleLoadUnitHook);
259263
}
264+
if (this.transactionPrototypeHook) {
265+
EggPrototypeLifecycleUtil.deleteLifecycle(this.transactionPrototypeHook);
266+
}
260267
MysqlDataSourceManager.instance.clear();
261268
SqlMapManager.instance.clear();
262269
TableModelManager.instance.clear();

0 commit comments

Comments
 (0)