Sistema NEXUS v3.4 migrado con: Estructura principal: - core/orchestration: Sistema SIMCO + CAPVED (27 directivas, 28 perfiles) - core/catalog: Catalogo de funcionalidades reutilizables - shared/knowledge-base: Base de conocimiento compartida - devtools/scripts: Herramientas de desarrollo - control-plane/registries: Control de servicios y CI/CD - orchestration/: Configuracion de orquestacion de agentes Proyectos incluidos (11): - gamilit (submodule -> GitHub) - trading-platform (OrbiquanTIA) - erp-suite con 5 verticales: - erp-core, construccion, vidrio-templado - mecanicas-diesel, retail, clinicas - betting-analytics - inmobiliaria-analytics - platform_marketing_content - pos-micro, erp-basico Configuracion: - .gitignore completo para Node.js/Python/Docker - gamilit como submodule (git@github.com:rckrdmrd/gamilit-workspace.git) - Sistema de puertos estandarizado (3005-3199) Generated with NEXUS v3.4 Migration System EPIC-010: Configuracion Git y Repositorios
679 lines
18 KiB
Markdown
679 lines
18 KiB
Markdown
# PATRON DE TRANSACCIONES
|
|
|
|
**Version:** 1.0.0
|
|
**Fecha:** 2025-12-08
|
|
**Prioridad:** OBLIGATORIA - Seguir para integridad de datos
|
|
**Sistema:** SIMCO + CAPVED
|
|
|
|
---
|
|
|
|
## PROPOSITO
|
|
|
|
Definir patrones de manejo de transacciones de base de datos para garantizar integridad de datos, consistencia y correcta recuperacion de errores.
|
|
|
|
---
|
|
|
|
## 1. PRINCIPIOS ACID
|
|
|
|
```
|
|
╔══════════════════════════════════════════════════════════════════════╗
|
|
║ PROPIEDADES ACID ║
|
|
╠══════════════════════════════════════════════════════════════════════╣
|
|
║ ║
|
|
║ A - Atomicity (Atomicidad) ║
|
|
║ Todo o nada. Si una parte falla, se revierte todo. ║
|
|
║ ║
|
|
║ C - Consistency (Consistencia) ║
|
|
║ La BD pasa de un estado valido a otro estado valido. ║
|
|
║ ║
|
|
║ I - Isolation (Aislamiento) ║
|
|
║ Transacciones concurrentes no interfieren entre si. ║
|
|
║ ║
|
|
║ D - Durability (Durabilidad) ║
|
|
║ Una vez confirmada, la transaccion persiste. ║
|
|
║ ║
|
|
╚══════════════════════════════════════════════════════════════════════╝
|
|
```
|
|
|
|
---
|
|
|
|
## 2. CUANDO USAR TRANSACCIONES
|
|
|
|
### SI Usar Transaccion
|
|
|
|
```
|
|
✅ Multiples operaciones que deben ser atomicas
|
|
✅ Operaciones que afectan multiples tablas relacionadas
|
|
✅ Operaciones financieras o criticas
|
|
✅ Creacion de entidades con relaciones obligatorias
|
|
✅ Actualizaciones que requieren consistencia
|
|
```
|
|
|
|
### NO Necesitas Transaccion
|
|
|
|
```
|
|
❌ Una sola query simple (SELECT, INSERT, UPDATE)
|
|
❌ Operaciones de solo lectura
|
|
❌ Operaciones independientes sin relacion
|
|
```
|
|
|
|
---
|
|
|
|
## 3. TYPEORM - METODOS DE TRANSACCION
|
|
|
|
### 3.1 QueryRunner (Recomendado para control total)
|
|
|
|
```typescript
|
|
// src/modules/order/services/order.service.ts
|
|
import { Injectable } from '@nestjs/common';
|
|
import { DataSource, QueryRunner } from 'typeorm';
|
|
|
|
@Injectable()
|
|
export class OrderService {
|
|
constructor(private readonly dataSource: DataSource) {}
|
|
|
|
async createOrder(dto: CreateOrderDto): Promise<Order> {
|
|
// Crear QueryRunner
|
|
const queryRunner = this.dataSource.createQueryRunner();
|
|
|
|
// Conectar y comenzar transaccion
|
|
await queryRunner.connect();
|
|
await queryRunner.startTransaction();
|
|
|
|
try {
|
|
// 1. Crear la orden
|
|
const order = queryRunner.manager.create(OrderEntity, {
|
|
userId: dto.userId,
|
|
status: OrderStatus.PENDING,
|
|
total: 0,
|
|
});
|
|
await queryRunner.manager.save(order);
|
|
|
|
// 2. Crear items y calcular total
|
|
let total = 0;
|
|
for (const item of dto.items) {
|
|
// Verificar stock
|
|
const product = await queryRunner.manager.findOne(ProductEntity, {
|
|
where: { id: item.productId },
|
|
lock: { mode: 'pessimistic_write' }, // Lock para evitar race condition
|
|
});
|
|
|
|
if (!product || product.stock < item.quantity) {
|
|
throw new BadRequestException(
|
|
`Stock insuficiente para producto ${item.productId}`,
|
|
);
|
|
}
|
|
|
|
// Crear item
|
|
const orderItem = queryRunner.manager.create(OrderItemEntity, {
|
|
orderId: order.id,
|
|
productId: item.productId,
|
|
quantity: item.quantity,
|
|
price: product.price,
|
|
});
|
|
await queryRunner.manager.save(orderItem);
|
|
|
|
// Actualizar stock
|
|
product.stock -= item.quantity;
|
|
await queryRunner.manager.save(product);
|
|
|
|
total += product.price * item.quantity;
|
|
}
|
|
|
|
// 3. Actualizar total de la orden
|
|
order.total = total;
|
|
await queryRunner.manager.save(order);
|
|
|
|
// 4. Confirmar transaccion
|
|
await queryRunner.commitTransaction();
|
|
|
|
return order;
|
|
|
|
} catch (error) {
|
|
// Revertir en caso de error
|
|
await queryRunner.rollbackTransaction();
|
|
throw error;
|
|
|
|
} finally {
|
|
// Liberar QueryRunner
|
|
await queryRunner.release();
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### 3.2 DataSource.transaction() (Mas simple)
|
|
|
|
```typescript
|
|
// Para casos mas simples
|
|
async createUserWithProfile(dto: CreateUserWithProfileDto): Promise<User> {
|
|
return this.dataSource.transaction(async (manager) => {
|
|
// Crear usuario
|
|
const user = manager.create(UserEntity, {
|
|
email: dto.email,
|
|
password: await hashPassword(dto.password),
|
|
});
|
|
await manager.save(user);
|
|
|
|
// Crear perfil
|
|
const profile = manager.create(ProfileEntity, {
|
|
userId: user.id,
|
|
firstName: dto.firstName,
|
|
lastName: dto.lastName,
|
|
});
|
|
await manager.save(profile);
|
|
|
|
return user;
|
|
});
|
|
}
|
|
```
|
|
|
|
### 3.3 @Transaction Decorator (NestJS)
|
|
|
|
```typescript
|
|
// src/shared/decorators/transactional.decorator.ts
|
|
import { DataSource } from 'typeorm';
|
|
|
|
export function Transactional() {
|
|
return function (
|
|
target: any,
|
|
propertyKey: string,
|
|
descriptor: PropertyDescriptor,
|
|
) {
|
|
const originalMethod = descriptor.value;
|
|
|
|
descriptor.value = async function (...args: any[]) {
|
|
const dataSource: DataSource = this.dataSource;
|
|
|
|
return dataSource.transaction(async (manager) => {
|
|
// Inyectar manager temporal
|
|
const originalManager = this.manager;
|
|
this.manager = manager;
|
|
|
|
try {
|
|
return await originalMethod.apply(this, args);
|
|
} finally {
|
|
this.manager = originalManager;
|
|
}
|
|
});
|
|
};
|
|
|
|
return descriptor;
|
|
};
|
|
}
|
|
|
|
// Uso
|
|
@Injectable()
|
|
export class OrderService {
|
|
constructor(
|
|
private readonly dataSource: DataSource,
|
|
@InjectRepository(OrderEntity)
|
|
private readonly orderRepository: Repository<OrderEntity>,
|
|
) {}
|
|
|
|
private manager: EntityManager;
|
|
|
|
@Transactional()
|
|
async createOrder(dto: CreateOrderDto): Promise<Order> {
|
|
// this.manager es el manager transaccional
|
|
const order = this.manager.create(OrderEntity, dto);
|
|
return this.manager.save(order);
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 4. NIVELES DE AISLAMIENTO
|
|
|
|
### Niveles Disponibles
|
|
|
|
| Nivel | Dirty Reads | Non-Repeatable Reads | Phantom Reads |
|
|
|-------|-------------|---------------------|---------------|
|
|
| READ UNCOMMITTED | Posible | Posible | Posible |
|
|
| READ COMMITTED | No | Posible | Posible |
|
|
| REPEATABLE READ | No | No | Posible |
|
|
| SERIALIZABLE | No | No | No |
|
|
|
|
### Configurar Nivel de Aislamiento
|
|
|
|
```typescript
|
|
// Por defecto PostgreSQL usa READ COMMITTED
|
|
// Para operaciones criticas, usar SERIALIZABLE
|
|
|
|
async processPayment(orderId: string): Promise<void> {
|
|
const queryRunner = this.dataSource.createQueryRunner();
|
|
await queryRunner.connect();
|
|
|
|
// Configurar nivel de aislamiento
|
|
await queryRunner.startTransaction('SERIALIZABLE');
|
|
|
|
try {
|
|
// Operaciones criticas aqui
|
|
await queryRunner.commitTransaction();
|
|
} catch (error) {
|
|
await queryRunner.rollbackTransaction();
|
|
|
|
// SERIALIZABLE puede fallar por conflictos
|
|
if (error.code === '40001') { // Serialization failure
|
|
// Reintentar la transaccion
|
|
return this.processPayment(orderId);
|
|
}
|
|
throw error;
|
|
} finally {
|
|
await queryRunner.release();
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 5. LOCKS (BLOQUEOS)
|
|
|
|
### 5.1 Pessimistic Locking
|
|
|
|
```typescript
|
|
// Bloquear fila para escritura (otros esperan)
|
|
async updateStock(productId: string, quantity: number): Promise<void> {
|
|
return this.dataSource.transaction(async (manager) => {
|
|
// Obtener producto con lock exclusivo
|
|
const product = await manager.findOne(ProductEntity, {
|
|
where: { id: productId },
|
|
lock: { mode: 'pessimistic_write' },
|
|
});
|
|
|
|
if (product.stock < quantity) {
|
|
throw new BadRequestException('Stock insuficiente');
|
|
}
|
|
|
|
product.stock -= quantity;
|
|
await manager.save(product);
|
|
});
|
|
}
|
|
```
|
|
|
|
### 5.2 Optimistic Locking (Version)
|
|
|
|
```typescript
|
|
// Entity con columna de version
|
|
@Entity()
|
|
export class ProductEntity {
|
|
@PrimaryGeneratedColumn('uuid')
|
|
id: string;
|
|
|
|
@Column()
|
|
stock: number;
|
|
|
|
@VersionColumn() // Auto-incrementa en cada update
|
|
version: number;
|
|
}
|
|
|
|
// El update falla si version cambio (alguien mas modifico)
|
|
async updateStock(productId: string, quantity: number): Promise<void> {
|
|
const product = await this.productRepository.findOne({
|
|
where: { id: productId },
|
|
});
|
|
|
|
product.stock -= quantity;
|
|
|
|
try {
|
|
await this.productRepository.save(product);
|
|
} catch (error) {
|
|
if (error instanceof OptimisticLockVersionMismatchError) {
|
|
// Reintentar o notificar conflicto
|
|
throw new ConflictException('El producto fue modificado. Reintente.');
|
|
}
|
|
throw error;
|
|
}
|
|
}
|
|
```
|
|
|
|
### Cuando Usar Cada Tipo
|
|
|
|
| Scenario | Lock Recomendado |
|
|
|----------|------------------|
|
|
| Alta concurrencia, conflictos raros | Optimistic |
|
|
| Operaciones financieras criticas | Pessimistic |
|
|
| Updates frecuentes al mismo registro | Pessimistic |
|
|
| Lecturas frecuentes, updates raros | Optimistic |
|
|
|
|
---
|
|
|
|
## 6. PATRONES COMUNES
|
|
|
|
### 6.1 Unit of Work Pattern
|
|
|
|
```typescript
|
|
// src/shared/database/unit-of-work.ts
|
|
@Injectable()
|
|
export class UnitOfWork {
|
|
private queryRunner: QueryRunner;
|
|
|
|
constructor(private readonly dataSource: DataSource) {}
|
|
|
|
async begin(): Promise<void> {
|
|
this.queryRunner = this.dataSource.createQueryRunner();
|
|
await this.queryRunner.connect();
|
|
await this.queryRunner.startTransaction();
|
|
}
|
|
|
|
getRepository<T>(entity: EntityTarget<T>): Repository<T> {
|
|
return this.queryRunner.manager.getRepository(entity);
|
|
}
|
|
|
|
async commit(): Promise<void> {
|
|
await this.queryRunner.commitTransaction();
|
|
await this.queryRunner.release();
|
|
}
|
|
|
|
async rollback(): Promise<void> {
|
|
await this.queryRunner.rollbackTransaction();
|
|
await this.queryRunner.release();
|
|
}
|
|
}
|
|
|
|
// Uso
|
|
@Injectable()
|
|
export class OrderService {
|
|
constructor(private readonly uow: UnitOfWork) {}
|
|
|
|
async createOrder(dto: CreateOrderDto): Promise<Order> {
|
|
await this.uow.begin();
|
|
|
|
try {
|
|
const orderRepo = this.uow.getRepository(OrderEntity);
|
|
const productRepo = this.uow.getRepository(ProductEntity);
|
|
|
|
// Operaciones...
|
|
|
|
await this.uow.commit();
|
|
return order;
|
|
} catch (error) {
|
|
await this.uow.rollback();
|
|
throw error;
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### 6.2 Saga Pattern (Para transacciones distribuidas)
|
|
|
|
```typescript
|
|
// Cuando no puedes usar transaccion de BD (microservicios)
|
|
interface SagaStep {
|
|
execute(): Promise<void>;
|
|
compensate(): Promise<void>; // Revertir si falla
|
|
}
|
|
|
|
class CreateOrderSaga {
|
|
private executedSteps: SagaStep[] = [];
|
|
|
|
async execute(steps: SagaStep[]): Promise<void> {
|
|
try {
|
|
for (const step of steps) {
|
|
await step.execute();
|
|
this.executedSteps.push(step);
|
|
}
|
|
} catch (error) {
|
|
// Compensar en orden inverso
|
|
for (const step of this.executedSteps.reverse()) {
|
|
await step.compensate();
|
|
}
|
|
throw error;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Uso
|
|
const saga = new CreateOrderSaga();
|
|
await saga.execute([
|
|
{
|
|
execute: () => this.orderService.create(orderDto),
|
|
compensate: () => this.orderService.delete(orderId),
|
|
},
|
|
{
|
|
execute: () => this.inventoryService.reserve(items),
|
|
compensate: () => this.inventoryService.release(items),
|
|
},
|
|
{
|
|
execute: () => this.paymentService.charge(payment),
|
|
compensate: () => this.paymentService.refund(payment),
|
|
},
|
|
]);
|
|
```
|
|
|
|
### 6.3 Retry con Backoff
|
|
|
|
```typescript
|
|
// Para manejar deadlocks y conflictos
|
|
async withRetry<T>(
|
|
operation: () => Promise<T>,
|
|
maxRetries: number = 3,
|
|
baseDelay: number = 100,
|
|
): Promise<T> {
|
|
let lastError: Error;
|
|
|
|
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
try {
|
|
return await operation();
|
|
} catch (error) {
|
|
lastError = error;
|
|
|
|
// Solo reintentar errores transitorios
|
|
const isRetryable =
|
|
error.code === '40001' || // Serialization failure
|
|
error.code === '40P01' || // Deadlock
|
|
error.code === '55P03'; // Lock not available
|
|
|
|
if (!isRetryable || attempt === maxRetries) {
|
|
throw error;
|
|
}
|
|
|
|
// Exponential backoff con jitter
|
|
const delay = baseDelay * Math.pow(2, attempt - 1) * (0.5 + Math.random());
|
|
await new Promise(resolve => setTimeout(resolve, delay));
|
|
}
|
|
}
|
|
|
|
throw lastError;
|
|
}
|
|
|
|
// Uso
|
|
async createOrder(dto: CreateOrderDto): Promise<Order> {
|
|
return this.withRetry(() => this.createOrderTransaction(dto));
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 7. ERRORES COMUNES Y SOLUCIONES
|
|
|
|
### 7.1 Deadlock
|
|
|
|
```typescript
|
|
// ❌ PROBLEMA: Deadlock por orden inconsistente de locks
|
|
// Transaction 1: Lock A, then B
|
|
// Transaction 2: Lock B, then A
|
|
|
|
// ✅ SOLUCION: Siempre lockear en el mismo orden
|
|
async transferFunds(fromId: string, toId: string, amount: number): Promise<void> {
|
|
// Ordenar IDs para lockear siempre en el mismo orden
|
|
const [firstId, secondId] = [fromId, toId].sort();
|
|
|
|
return this.dataSource.transaction(async (manager) => {
|
|
const firstAccount = await manager.findOne(AccountEntity, {
|
|
where: { id: firstId },
|
|
lock: { mode: 'pessimistic_write' },
|
|
});
|
|
|
|
const secondAccount = await manager.findOne(AccountEntity, {
|
|
where: { id: secondId },
|
|
lock: { mode: 'pessimistic_write' },
|
|
});
|
|
|
|
// Ahora operar
|
|
const from = firstId === fromId ? firstAccount : secondAccount;
|
|
const to = firstId === toId ? firstAccount : secondAccount;
|
|
|
|
from.balance -= amount;
|
|
to.balance += amount;
|
|
|
|
await manager.save([from, to]);
|
|
});
|
|
}
|
|
```
|
|
|
|
### 7.2 Connection Leak
|
|
|
|
```typescript
|
|
// ❌ PROBLEMA: QueryRunner no liberado
|
|
async badMethod(): Promise<void> {
|
|
const queryRunner = this.dataSource.createQueryRunner();
|
|
await queryRunner.connect();
|
|
await queryRunner.startTransaction();
|
|
|
|
const data = await queryRunner.manager.find(Entity);
|
|
// Si hay error aqui, queryRunner nunca se libera!
|
|
|
|
await queryRunner.commitTransaction();
|
|
await queryRunner.release();
|
|
}
|
|
|
|
// ✅ SOLUCION: Siempre usar try/finally
|
|
async goodMethod(): Promise<void> {
|
|
const queryRunner = this.dataSource.createQueryRunner();
|
|
await queryRunner.connect();
|
|
await queryRunner.startTransaction();
|
|
|
|
try {
|
|
const data = await queryRunner.manager.find(Entity);
|
|
await queryRunner.commitTransaction();
|
|
} catch (error) {
|
|
await queryRunner.rollbackTransaction();
|
|
throw error;
|
|
} finally {
|
|
await queryRunner.release(); // SIEMPRE se ejecuta
|
|
}
|
|
}
|
|
```
|
|
|
|
### 7.3 Long-Running Transactions
|
|
|
|
```typescript
|
|
// ❌ PROBLEMA: Transaccion muy larga
|
|
async processAllOrders(): Promise<void> {
|
|
return this.dataSource.transaction(async (manager) => {
|
|
const orders = await manager.find(OrderEntity); // Puede ser miles
|
|
for (const order of orders) {
|
|
await this.processOrder(order); // Minutos de bloqueo
|
|
}
|
|
});
|
|
}
|
|
|
|
// ✅ SOLUCION: Procesar en batches con transacciones cortas
|
|
async processAllOrders(): Promise<void> {
|
|
const BATCH_SIZE = 100;
|
|
let processed = 0;
|
|
|
|
while (true) {
|
|
const orders = await this.orderRepository.find({
|
|
where: { status: OrderStatus.PENDING },
|
|
take: BATCH_SIZE,
|
|
});
|
|
|
|
if (orders.length === 0) break;
|
|
|
|
// Transaccion corta por batch
|
|
await this.dataSource.transaction(async (manager) => {
|
|
for (const order of orders) {
|
|
await this.processOrderInTransaction(manager, order);
|
|
}
|
|
});
|
|
|
|
processed += orders.length;
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 8. TESTING DE TRANSACCIONES
|
|
|
|
```typescript
|
|
// src/modules/order/order.service.spec.ts
|
|
describe('OrderService', () => {
|
|
let service: OrderService;
|
|
let dataSource: DataSource;
|
|
|
|
beforeEach(async () => {
|
|
const module = await Test.createTestingModule({
|
|
imports: [TypeOrmModule.forRoot(testConfig)],
|
|
providers: [OrderService],
|
|
}).compile();
|
|
|
|
service = module.get(OrderService);
|
|
dataSource = module.get(DataSource);
|
|
});
|
|
|
|
afterEach(async () => {
|
|
// Limpiar despues de cada test
|
|
await dataSource.synchronize(true);
|
|
});
|
|
|
|
describe('createOrder', () => {
|
|
it('should rollback if stock is insufficient', async () => {
|
|
// Arrange
|
|
const product = await productRepo.save({
|
|
name: 'Test',
|
|
stock: 5,
|
|
price: 100,
|
|
});
|
|
|
|
// Act & Assert
|
|
await expect(
|
|
service.createOrder({
|
|
items: [{ productId: product.id, quantity: 10 }],
|
|
}),
|
|
).rejects.toThrow('Stock insuficiente');
|
|
|
|
// Verificar que stock no cambio (rollback funciono)
|
|
const updatedProduct = await productRepo.findOne({
|
|
where: { id: product.id },
|
|
});
|
|
expect(updatedProduct.stock).toBe(5);
|
|
});
|
|
|
|
it('should commit successfully with valid data', async () => {
|
|
// ...
|
|
});
|
|
});
|
|
});
|
|
```
|
|
|
|
---
|
|
|
|
## 9. CHECKLIST DE TRANSACCIONES
|
|
|
|
```
|
|
Antes de implementar:
|
|
[ ] ¿Necesito atomicidad? (multiples operaciones)
|
|
[ ] ¿Que nivel de aislamiento necesito?
|
|
[ ] ¿Necesito locks? ¿Pesimista u optimista?
|
|
|
|
Durante implementacion:
|
|
[ ] QueryRunner siempre liberado (finally)
|
|
[ ] Rollback en catch
|
|
[ ] Manejo de errores transitorios (retry)
|
|
[ ] Transacciones lo mas cortas posible
|
|
[ ] Orden consistente de locks (evitar deadlocks)
|
|
|
|
Testing:
|
|
[ ] Test de rollback en error
|
|
[ ] Test de commit exitoso
|
|
[ ] Test de concurrencia (si aplica)
|
|
```
|
|
|
|
---
|
|
|
|
**Version:** 1.0.0 | **Sistema:** SIMCO | **Tipo:** Patron de Codigo
|