Usamos cookies para mejorar tu experiencia en el sitio
CodeWorlds

Transakcje - bezpieczne operacje na tributach

Witajcie, odpowiedzialni zarządcy tributów! Konsul Caesar.js ma dla was bardzo ważną lekcję. W świecie legionistów najważniejsze są bezpieczne transakcje - czy to wymiana tributów, transfer między fortami, czy podział zdobyczy. Dziś nauczycie się jak zapewnić, że każda operacja na danych zakończy się powodzeniem lub zostanie całkowicie wycofana!

Czym są transakcje w świecie legionistów?

Wyobraź sobie sytuację: legionista Marek chce przekazać swój tribut legioniście Gajuszowi. Musi się to stać atomowo - albo cała operacja się powiedzie (Marek traci tribut, Gajusz go zyskuje, liczniki się aktualizują), albo nic się nie dzieje. Nie może być tak, że Marek straci tribut, a Gajusz go nie otrzyma!

Transakcje zapewniają właściwości ACID:

  • Atomicity - wszystko albo nic
  • Consistency - dane zawsze w sprzecznym stanie
  • Isolation - transakcje nie zakłócają sobie nawzajem
  • Durability - zatwierdzone zmiany są trwałe

Podstawowe transakcje w TypeORM

Prosta transakcja

1// tribute-transfer.service.ts
2import { Injectable } from '@nestjs/common';
3import { InjectRepository } from '@nestjs/typeorm';
4import { Repository, DataSource } from 'typeorm';
5import { Tribute } from './tribute.entity';
6import { Legionary } from './legionariusze.entity';
7
8@Injectable()
9export class TributeTransferService {
10 constructor(
11 @InjectRepository(Tribute)
12 private tributeRepository: Repository<Tribute>,
13 @InjectRepository(Legionary)
14 private legionariuszeRepository: Repository<Legionary>,
15 private dataSource: DataSource
16 ) {}
17
18 // Podstawowy transfer tributy
19 async transferTribute(
20 tributeId: number,
21 fromLegionaryId: number,
22 toLegionaryId: number
23 ): Promise<void> {
24 await this.dataSource.transaction(async manager => {
25 // 1. Znajdź tributy i sprawdź właściciela
26 const tribute = await manager.findOne(Tribute, {
27 where: { id: tributeId, legionariusze: { id: fromLegionaryId } }
28 });
29
30 if (!tribute) {
31 throw new Error('Tribute not found or not owned by sender');
32 }
33
34 // 2. Znajdź odbiorcę
35 const recipient = await manager.findOne(Legionary, {
36 where: { id: toLegionaryId }
37 });
38
39 if (!recipient) {
40 throw new Error('Recipient legionariusze not found');
41 }
42
43 // 3. Aktualizuj właściciela tributy
44 tribute.legionariusze = recipient;
45 await manager.save(tribute);
46
47 // 4. Zaktualizuj liczniki nadawcy
48 await manager.decrement(
49 Legionary,
50 { id: fromLegionaryId },
51 'tributeCount',
52 1
53 );
54 await manager.decrement(
55 Legionary,
56 { id: fromLegionaryId },
57 'totalTributeValue',
58 tribute.value
59 );
60
61 // 5. Zaktualizuj liczniki odbiorcy
62 await manager.increment(
63 Legionary,
64 { id: toLegionaryId },
65 'tributeCount',
66 1
67 );
68 await manager.increment(
69 Legionary,
70 { id: toLegionaryId },
71 'totalTributeValue',
72 tribute.value
73 );
74 });
75 }
76}

Zaawansowana transakcja z walidacją

1// Kompleksowa wymiana tributów
2async tradeTributes(
3 legionariusze1Id: number,
4 tribute1Id: number,
5 legionariusze2Id: number,
6 tribute2Id: number
7): Promise<{ success: boolean; message: string }> {
8 return await this.dataSource.transaction(async manager => {
9 try {
10 // 1. Pobierz oba tributy z właścicielami
11 const [tribute1, tribute2] = await Promise.all([
12 manager.findOne(Tribute, {
13 where: { id: tribute1Id },
14 relations: ['legionariusze']
15 }),
16 manager.findOne(Tribute, {
17 where: { id: tribute2Id },
18 relations: ['legionariusze']
19 })
20 ]);
21
22 // 2. Walidacja
23 if (!tribute1 || !tribute2) {
24 throw new Error('One or both tributes not found');
25 }
26
27 if (tribute1.legionariusze.id !== legionariusze1Id) {
28 throw new Error('Legionary 1 does not own tribute 1');
29 }
30
31 if (tribute2.legionariusze.id !== legionariusze2Id) {
32 throw new Error('Legionary 2 does not own tribute 2');
33 }
34
35 if (tribute1.isCursed || tribute2.isCursed) {
36 throw new Error('Cannot trade cursed tributes');
37 }
38
39 // 3. Sprawdzenie równowartościowości (różnica max 20%)
40 const valueDifference = Math.abs(tribute1.value - tribute2.value);
41 const maxDifference = Math.max(tribute1.value, tribute2.value) * 0.2;
42 
43 if (valueDifference > maxDifference) {
44 throw new Error('Tribute values too different for fair trade');
45 }
46
47 // 4. Wykonaj wymianę
48 const originalOwner1 = tribute1.legionariusze;
49 const originalOwner2 = tribute2.legionariusze;
50
51 tribute1.legionariusze = originalOwner2;
52 tribute2.legionariusze = originalOwner1;
53
54 await Promise.all([
55 manager.save(tribute1),
56 manager.save(tribute2)
57 ]);
58
59 // 5. Aktualizuj statystyki (wartości się kompensują, więc tylko zamieniamy)
60 const valueDiff = tribute1.value - tribute2.value;
61 
62 if (valueDiff !== 0) {
63 // Jeśli wartości są różne, zaktualizuj totalne wartości
64 await manager.increment(
65 Legionary,
66 { id: legionariusze1Id },
67 'totalTributeValue',
68 tribute2.value - tribute1.value
69 );
70 await manager.increment(
71 Legionary,
72 { id: legionariusze2Id },
73 'totalTributeValue',
74 tribute1.value - tribute2.value
75 );
76 }
77
78 // 6. Zapisz log transakcji
79 await manager.save(TributeTransaction, {
80 type: 'TRADE',
81 fromLegionary: originalOwner1,
82 toLegionary: originalOwner2,
83 tribute1: tribute1,
84 tribute2: tribute2,
85 executedAt: new Date()
86 });
87
88 return {
89 success: true,
90 message: `Successfully traded ${tribute1.name} and ${tribute2.name}`
91 };
92 } catch (error) {
93 // Transakcja zostanie automatycznie wycofana
94 return {
95 success: false,
96 message: error.message
97 };
98 }
99 });
100}

Izolacja transakcji

1// Różne poziomy izolacji
2import { IsolationLevel } from 'typeorm';
3
4// READ UNCOMMITTED - najszybsze, ale może czytać niezatwierdzone dane
5async quickTributeCount(): Promise<number> {
6 return await this.dataSource.transaction(
7 async manager => {
8 const result = await manager
9 .createQueryBuilder(Tribute, 'tribute')
10 .select('COUNT(*)', 'count')
11 .getRawOne();
12 return parseInt(result.count);
13 },
14 { isolationLevel: 'READ uncommitted' }
15 );
16}
17
18// READ COMMITTED - standardowy poziom
19async safeTransferWithAudit(
20 tributeId: number,
21 fromLegionaryId: number,
22 toLegionaryId: number
23): Promise<void> {
24 await this.dataSource.transaction(
25 async manager => {
26 // Operacje transakcyjne...
27 await this.performTransfer(manager, tributeId, fromLegionaryId, toLegionaryId);
28 await this.createAuditLog(manager, tributeId, fromLegionaryId, toLegionaryId);
29 },
30 { isolationLevel: 'read committed' }
31 );
32}
33
34// REPEATABLE READ - zapewnia, że powtarzalne odczyty dają te same wyniki
35async calculateTributeStatistics(): Promise<any> {
36 return await this.dataSource.transaction(
37 async manager => {
38 // Pierwszy odczyt
39 const firstCount = await manager.count(Tribute);
40 
41 // Jakaś inna operacja...
42 await this.performSomeOtherOperation(manager);
43 
44 // Drugi odczyt - gwarantuje ten sam wynik co pierwszy
45 const secondCount = await manager.count(Tribute);
46 
47 return { firstCount, secondCount };
48 },
49 { isolationLevel: 'repeatable read' }
50 );
51}
52
53// SERIALIZABLE - najstroższy poziom
54async criticalTributeOperation(): Promise<void> {
55 await this.dataSource.transaction(
56 async manager => {
57 // Krytyczne operacje wymagające pełnej izolacji
58 await this.performCriticalOperation(manager);
59 },
60 { isolationLevel: 'serializable' }
61 );
62}

Zagnieżdżone transakcje (Savepoints)

1// Kompleksowa operacja z punktami zapisu
2async complexLegionaryOperation(
3 legionariuszeId: number,
4 operations: any[]
5): Promise<{ completed: number; failed: number; errors: string[] }> {
6 let completed = 0;
7 let failed = 0;
8 const errors: string[] = [];
9
10 await this.dataSource.transaction(async manager => {
11 for (let i = 0; i < operations.length; i++) {
12 const savepoint = `operation_${i}`
1
2 try {
3 // Utwórz punkt zapisu
4 await manager.query(`SAVEPOINT ${savepoint}`);
5 
6 // Wykonaj operację
7 await this.executeOperation(manager, legionariuszeId, operations[i]);
8 
9 // Jeśli sukces, usuń savepoint
10 await manager.query(`RELEASE SAVEPOINT ${savepoint}`);
11 completed++;
12 
13 } catch (error) {
14 // Jeśli błąd, wróć do savepoint
15 await manager.query(`ROLLBACK TO SAVEPOINT ${savepoint}`);
16 failed++;
17 errors.push(`Operation ${i}: ${error.message}`);
18 }
19 }
20 });
21
22 return { completed, failed, errors };
23}
24
25// Pomocnicza metoda wykonująca różne operacje
26private async executeOperation(
27 manager: EntityManager,
28 legionariuszeId: number,
29 operation: any
30): Promise<void> {
31 switch (operation.type) {
32 case 'ADD_TRIBUTE':
33 await this.addTributeToLegionary(manager, legionariuszeId, operation.tribute);
34 break;
35 case 'UPDATE_RANK':
36 await this.updateLegionaryRank(manager, legionariuszeId, operation.newRank);
37 break;
38 case 'JOIN_LEGION':
39 await this.assignLegionaryToLegion(manager, legionariuszeId, operation.centurionId);
40 break;
41 default:
42 throw new Error(`Unknown operation type: ${operation.type}`);
43 }
44}

Transakcje z timeout

1// Transakcja z ograniczeniem czasowym
2async timeoutTransaction<T>(
3 operation: (manager: EntityManager) => Promise<T>,
4 timeoutMs: number = 30000
5): Promise<T> {
6 return await Promise.race([
7 this.dataSource.transaction(operation),
8 new Promise<never>((_, reject) => {
9 setTimeout(() => {
10 reject(new Error(`Transaction timeout after ${timeoutMs}ms`));
11 }, timeoutMs);
12 })
13 ]);
14}
15
16// Użycie z timeout
17async massiveTributeUpdate(): Promise<void> {
18 try {
19 await this.timeoutTransaction(async manager => {
20 // Operacja, która może trwać długo
21 const tributes = await manager.find(Tribute);
22 
23 for (const tribute of tributes) {
24 tribute.value = tribute.value * 1.1; // 10% wzrost wartości
25 await manager.save(tribute);
26 }
27 }, 60000); // 1 minuta timeout
28 } catch (error) {
29 if (error.message.includes('timeout')) {
30 console.log('Operation was cancelled due to timeout');
31 } else {
32 console.log('Operation failed:', error.message);
33 }
34 }
35}

Retry mechanizm dla transakcji

1// Automatyczne ponawianie nieudanych transakcji
2async retryableTransaction<T>(
3 operation: (manager: EntityManager) => Promise<T>,
4 maxRetries: number = 3,
5 delayMs: number = 1000
6): Promise<T> {
7 let lastError: Error;
8 
9 for (let attempt = 1; attempt <= maxRetries; attempt++) {
10 try {
11 return await this.dataSource.transaction(operation);
12 } catch (error) {
13 lastError = error;
14 
15 // Sprawdzenie czy błąd kwalifikuje się do ponowienia
16 if (this.isRetryableError(error) && attempt < maxRetries) {
17 console.log(`Transaction attempt ${attempt} failed, retrying in ${delayMs}ms...`);
18 await this.delay(delayMs);
19 delayMs *= 2; // Exponential backoff
20 } else {
21 break;
22 }
23 }
24 }
25 
26 throw lastError;
27}
28
29private isRetryableError(error: Error): boolean {
30 // Błędy, które można ponowić
31 const retryableMessages = [
32 'deadlock detected',
33 'connection lost',
34 'timeout',
35 'serialization failure'
36 ];
37 
38 return retryableMessages.some(msg => 
39 error.message.toLowerCase().includes(msg)
40 );
41}
42
43private delay(ms: number): Promise<void> {
44 return new Promise(resolve => setTimeout(resolve, ms));
45}
46
47// Użycie retry mechanizmu
48async safeTributeTransfer(
49 tributeId: number,
50 fromLegionaryId: number,
51 toLegionaryId: number
52): Promise<void> {
53 await this.retryableTransaction(async manager => {
54 await this.performTransferOperation(manager, tributeId, fromLegionaryId, toLegionaryId);
55 });
56}

Monitoring i logowanie transakcji

1// Serwis do monitorowania transakcji
2@Injectable()
3export class TransactionMonitoringService {
4 private activeTransactions = new Map<string, {
5 startTime: Date;
6 operation: string;
7 legionariuszeId?: number;
8 }>();
9
10 async monitoredTransaction<T>(
11 operation: (manager: EntityManager) => Promise<T>,
12 operationName: string,
13 legionariuszeId?: number
14 ): Promise<T> {
15 const transactionId = this.generateTransactionId();
16 const startTime = new Date();
17 
18 // Rejestruj początek transakcji
19 this.activeTransactions.set(transactionId, {
20 startTime,
21 operation: operationName,
22 legionariuszeId
23 });
24
25 console.log(`[${transactionId}] Starting transaction: ${operationName}`);
26
27 try {
28 const result = await this.dataSource.transaction(async manager => {
29 // Dodaj ID transakcji do contextu
30 (manager as any).transactionId = transactionId;
31 return await operation(manager);
32 });
33
34 const duration = Date.now() - startTime.getTime();
35 console.log(`[${transactionId}] Transaction completed in ${duration}ms`);
36 
37 return result;
38 } catch (error) {
39 const duration = Date.now() - startTime.getTime();
40 console.error(`[${transactionId}] Transaction failed after ${duration}ms:`, error.message);
41 throw error;
42 } finally {
43 this.activeTransactions.delete(transactionId);
44 }
45 }
46
47 private generateTransactionId(): string {
48 return `tx_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
49 }
50
51 // Sprawdzenie aktywnych transakcji
52 getActiveTransactions() {
53 const now = new Date();
54 return Array.from(this.activeTransactions.entries()).map(([id, info]) => ({
55 id,
56 operation: info.operation,
57 legionariuszeId: info.legionariuszeId,
58 duration: now.getTime() - info.startTime.getTime(),
59 startTime: info.startTime
60 }));
61 }
62
63 // Znajdź długotrwałe transakcje
64 getLongRunningTransactions(thresholdMs: number = 30000) {
65 return this.getActiveTransactions().filter(tx => tx.duration > thresholdMs);
66 }
67}

Testowanie transakcji

1// tribute-transfer.service.spec.ts
2import { Test, TestingModule } from '@nestjs/testing';
3import { getRepositoryToken } from '@nestjs/typeorm';
4import { DataSource } from 'typeorm';
5import { TributeTransferService } from './tribute-transfer.service';
6
7describe('TributeTransferService', () => {
8 let service: TributeTransferService;
9 let mockDataSource: Partial<DataSource>;
10 let mockManager: any;
11
12 beforeEach(async () => {
13 mockManager = {
14 findOne: jest.fn(),
15 save: jest.fn(),
16 increment: jest.fn(),
17 decrement: jest.fn()
18 };
19
20 mockDataSource = {
21 transaction: jest.fn(async (callback) => {
22 return await callback(mockManager);
23 })
24 };
25
26 const module: TestingModule = await Test.createTestingModule({
27 providers: [
28 TributeTransferService,
29 { provide: DataSource, useValue: mockDataSource },
30 { provide: getRepositoryToken(Tribute), useValue: {} },
31 { provide: getRepositoryToken(Legionary), useValue: {} }
32 ]
33 }).compile();
34
35 service = module.get<TributeTransferService>(TributeTransferService);
36 });
37
38 describe('transferTribute', () => {
39 it('should transfer tribute successfully', async () => {
40 const mockTribute = {
41 id: 1,
42 value: 1000,
43 legionariusze: { id: 1 }
44 };
45 const mockRecipient = { id: 2 };
46
47 mockManager.findOne
48 .mockResolvedValueOnce(mockTribute)
49 .mockResolvedValueOnce(mockRecipient);
50
51 await service.transferTribute(1, 1, 2);
52
53 expect(mockDataSource.transaction).toHaveBeenCalled();
54 expect(mockManager.save).toHaveBeenCalledWith({
55 ...mockTribute,
56 legionariusze: mockRecipient
57 });
58 expect(mockManager.decrement).toHaveBeenCalledTimes(2);
59 expect(mockManager.increment).toHaveBeenCalledTimes(2);
60 });
61
62 it('should throw error if tribute not found', async () => {
63 mockManager.findOne.mockResolvedValue(null);
64
65 await expect(
66 service.transferTribute(1, 1, 2)
67 ).rejects.toThrow('Tribute not found or not owned by sender');
68 });
69 });
70});

Najlepsze praktyki transakcji

1. Krótkie transakcje

1// DOBRZE - krótka transakcja
2async quickTransfer() {
3 await this.dataSource.transaction(async manager => {
4 // Tylko niezbędne operacje
5 const tribute = await manager.findOne(Tribute, { where: { id: 1 } });
6 tribute.legionariusze = newOwner;
7 await manager.save(tribute);
8 });
9}
10
11// ŹLE - długa transakcja
12async slowTransfer() {
13 await this.dataSource.transaction(async manager => {
14 // Długie operacje blokują bazę
15 await this.sendEmail(); // NIE!
16 await this.generateReport(); // NIE!
17 await this.callExternalAPI(); // NIE!
18 });
19}

2. Właściwa kolejność operacji

1// DOBRZE - najpierw walidacja, potem modyfikacja
2async properOrder() {
3 await this.dataSource.transaction(async manager => {
4 // 1. Sprawdź wszystkie warunki
5 const validationResult = await this.validateOperation(manager);
6 if (!validationResult.isValid) {
7 throw new Error(validationResult.error);
8 }
9 
10 // 2. Wykonaj operacje
11 await this.performOperations(manager);
12 });
13}

3. Obsługa błędów

1// Zawsze obsługuj potencjalne błędy
2async safeOperation() {
3 try {
4 await this.dataSource.transaction(async manager => {
5 await this.riskyOperation(manager);
6 });
7 } catch (error) {
8 // Loguj szczegóły błędu
9 console.error('Transaction failed:', {
10 error: error.message,
11 stack: error.stack,
12 timestamp: new Date().toISOString()
13 });
14 
15 // Rzur odpowiedni wyjątek dla klienta
16 throw new BadRequestException('Operation failed: ' + error.message);
17 }
18}

Podsumowanie

Gratulacje! Teraz jesteś mistrzem bezpiecznych transakcji rzymskich! Umiesz:

Implementować atomowe operacje z TypeORM ✓ Używać różnych poziomów izolacji transakcji ✓ Obsługiwać zagnieżdżone transakcje z savepointami ✓ Implementować retry mechanizmy dla nieudanych transakcji ✓ Monitorować i logować transakcje w aplikacji

Teraz możesz bezpiecznie zarządzać tributami bez ryzyka ich utraty!

Ir a CodeWorlds