Appearance
Reglas Arquitecturales: Patrón de JOINs Declarativos
Versión: 1.0.0 Fecha: 2026-02-04 Ámbito: Backend - Patrón de JOINs
Introducción
Este documento define las reglas arquitecturales (RA) que rigen el uso del patrón unificado de JOINs declarativos en Sistema Bautista. Estas reglas complementan las reglas de arquitectura de base de datos (RA-MT-, RA-MS-, RA-MM-*).
Prefijo de reglas: RA-JOIN-
Tabla de Reglas
| Código | Nombre | Criticidad | Aplica a |
|---|---|---|---|
| RA-JOIN-001 | Separación Model-Query | ALTA | Todos los Models |
| RA-JOIN-002 | JoinSpec Directo en Queries | MEDIA | Query Classes |
| RA-JOIN-003 | Cross-Schema Jerárquico Permitido | ALTA | JOINs cross-level |
| RA-JOIN-004 | Cross-Schema Horizontal Prohibido | CRÍTICA | JOINs entre branches |
| RA-JOIN-005 | UNION ALL para Multi-Schema | ALTA | Consolidación |
| RA-JOIN-006 | Auto-Resolución de Schema Level | MEDIA | ModelMetadata |
RA-JOIN-001: Separación Model-Query
Descripción
Los Models DEBEN representar una única tabla sin JOINs hardcodeados. Las queries con JOINs DEBEN implementarse en Query Classes dedicadas.
Implicación
✅ PERMITIDO:
php
// Model sin JOINs
class ClienteModel implements ModelMetadata {
public function getAll(): array {
return $this->conn->query("SELECT * FROM clientes")->fetchAll();
}
}
// Query Class con JOINs
class ClienteOrdenesQuery extends BaseQuery {
public function execute(): array {
$sql = "SELECT c.*, o.total FROM clientes c";
$sql = $this->applyJoins($sql, [
JoinSpec::auto('c', ClienteModel::class, OrdenModel::class, 'LEFT')
]);
return $this->conn->query($sql)->fetchAll();
}
}❌ PROHIBIDO:
php
// Model con JOIN hardcodeado
class ClienteModel {
public function getAllWithOrdenes(): array {
// ❌ JOIN no debe estar en Model
$sql = "SELECT c.*, o.* FROM clientes c LEFT JOIN ordenes o ON ...";
return $this->conn->query($sql)->fetchAll();
}
}Justificación
- Bajo acoplamiento: Models no dependen de otras tablas
- Reutilización: JoinSpecs se pueden usar en múltiples queries
- Testing: Models se testean de forma aislada
Relacionada con
- RA-MT-001 (Aislamiento por Schema)
RA-JOIN-002: JoinSpec Directo en Queries
Descripción
Los JoinSpecs DEBEN crearse directamente en las Query Classes según necesidad. NO crear catálogos centralizados de relaciones.
Implicación
✅ PERMITIDO:
php
// JoinSpec creado directamente en Query
class ClienteOrdenesQuery extends BaseQuery {
public function execute(): array {
$sql = "SELECT c.*, o.total FROM clientes c";
// Creación directa (sin catálogo)
$sql = $this->applyJoins($sql, [
JoinSpec::auto('c', ClienteModel::class, OrdenModel::class, 'LEFT')
]);
return $this->conn->query($sql)->fetchAll();
}
}❌ PROHIBIDO (deprecado):
php
// Catálogo centralizado (anti-pattern)
class JoinMap {
public static function clienteOrdenes(): JoinSpec {
return JoinSpec::auto('c', ClienteModel::class, OrdenModel::class, 'LEFT');
}
}
// Uso en Query (más indirección innecesaria)
$joins = [JoinMap::clienteOrdenes()];Justificación
- Simplicidad: Menos archivos que mantener
- Flexibilidad: Cada Query ajusta su JOIN según necesidad
- Claridad: JOIN visible en contexto de uso
Excepción
En módulos con muchas queries similares, se PUEDE crear factory methods privados dentro de la misma Query Class (NO en catálogo global).
RA-JOIN-003: Cross-Schema Jerárquico Permitido
Descripción
Los JOINs entre niveles jerárquicos ESTÁN PERMITIDOS cuando suben en la jerarquía (hijo → padre) dentro del mismo branch.
Implicación
✅ PERMITIDO: Cross-Level Hacia Arriba
php
// CAJA → SUCURSAL (mismo branch)
FROM suc0001caja001.recibos r
JOIN suc0001.facturas f ON f.id = r.factura_id
-- ✅ suc0001caja001 puede acceder a suc0001
// SUCURSAL → EMPRESA (maestros compartidos)
FROM suc0001.facturas f
JOIN public.productos p ON p.id = f.producto_id
-- ✅ suc0001 puede acceder a public
// CAJA → EMPRESA (saltando nivel)
FROM suc0001caja001.movimientos cm
JOIN public.conceptos c ON c.id = cm.concepto_id
-- ✅ suc0001caja001 puede acceder a publicImplementación:
php
// JoinSpec cross-level directo
JoinSpec::autoWithSchema('r', ReciboModel::class, FacturaModel::class, 'INNER')
// Genera:
// INNER JOIN suc0001.facturas f ON f.id = r.factura_idJustificación
- Herencia de datos: Niveles inferiores heredan datos de superiores
- Search path: PostgreSQL soporta
search_pathjerárquico - Maestros compartidos:
public(EMPRESA) es accesible desde todos
Relacionada con
- RA-MT-003 (Datos Maestros Compartidos)
- RA-MS-002 (Alcance Limitado por Sucursal)
RA-JOIN-004: Cross-Schema Horizontal Prohibido
Descripción
Los JOINs entre schemas del mismo nivel pero diferentes branches ESTÁN PROHIBIDOS (cross-schema horizontal).
Implicación
❌ PROHIBIDO: Cross-Schema Horizontal
php
// Entre sucursales diferentes
FROM suc0001.facturas f
JOIN suc0002.clientes c ON c.id = f.cliente_id
-- ❌ VIOLACIÓN: Cruce entre branches diferentes
// Entre cajas de diferentes sucursales
FROM suc0001caja001.recibos r
JOIN suc0002caja001.recibos r2 ON r2.cliente_id = r.cliente_id
-- ❌ VIOLACIÓN: Cruce entre sucursales
// Entre cajas de la misma sucursal
FROM suc0001caja001.recibos r
JOIN suc0001caja002.movimientos m ON m.recibo_id = r.id
-- ❌ VIOLACIÓN: Para esto usar multi-schema con UNION ALLJustificación
- Aislamiento multi-tenant: Cada tenant tiene datos separados
- Seguridad: Evitar acceso cruzado no autorizado
- Integridad: Relaciones deben estar en el mismo schema
Alternativa
Para consolidar datos de múltiples schemas del mismo nivel, usar multi-schema querying con UNION ALL (RA-JOIN-005).
Relacionada con
- RA-MT-001 (Aislamiento por Schema)
- RA-MS-002 (Alcance Limitado por Sucursal)
RA-JOIN-005: UNION ALL para Multi-Schema
Descripción
La consolidación de datos de múltiples schemas del mismo nivel DEBE usar UNION ALL mediante executeMultiSchema(). NO usar loops de queries separadas.
Implicación
✅ PERMITIDO: UNION ALL Optimizado
php
class RecibosTodasLasCajasQuery extends BaseQuery {
public function execute(): array {
$schemaList = ['suc0001caja001', 'suc0001caja002', 'suc0001caja003'];
$baseSql = "SELECT r.*, f.total FROM {schema}.recibos r";
// executeMultiSchema() genera UNION ALL automático
return $this->executeMultiSchema($baseSql, [
JoinSpec::autoWithSchema('r', ReciboModel::class, FacturaModel::class, 'LEFT')
], $schemaList);
}
}
// SQL generado:
// (SELECT ... FROM suc0001caja001.recibos r JOIN suc0001.facturas f ...)
// UNION ALL
// (SELECT ... FROM suc0001caja002.recibos r JOIN suc0001.facturas f ...)
// UNION ALL
// (SELECT ... FROM suc0001caja003.recibos r JOIN suc0001.facturas f ...)❌ PROHIBIDO: Loop Manual
php
// Anti-pattern: N queries separadas
$results = [];
foreach ($schemaList as $schema) {
$this->conn->exec("SET search_path = $schema");
$results = array_merge($results, $this->querySchema());
}
// ❌ N round-trips, peor performanceOptimización
Si solo hay 1 schema, executeMultiSchema() optimiza automáticamente a query simple (sin UNION).
Justificación
- Performance: 1 round-trip vs N round-trips (3-6x más rápido)
- Optimización DB: PostgreSQL optimiza UNION ALL internamente
- Consistencia: Snapshot único de datos
Relacionada con
- RA-MS-001 (Activación Basada en Configuración)
- RA-MS-002 (Alcance Limitado por Sucursal)
RA-JOIN-006: Auto-Resolución de Schema Level
Descripción
El nivel de schema (EMPRESA/SUCURSAL/CAJA) NO DEBE declararse manualmente en Models. MultiSchemaService DEBE auto-detectarlo consultando information_schema.
Implicación
✅ PERMITIDO:
php
// Model SIN schemaLevel manual
final class MovimientoCajaModel implements ModelMetadata {
public static function table(): string { return 'movimientos_caja'; }
public static function alias(): string { return 'mc'; }
public static function primaryKey(): string { return 'id'; }
// Nivel auto-detectado: CAJA (no se declara)
}❌ PROHIBIDO:
php
// Model con schemaLevel hardcodeado (deprecado)
final class MovimientoCajaModel implements ModelMetadata {
public static function table(): string { return 'movimientos_caja'; }
public static function alias(): string { return 'mc'; }
public static function primaryKey(): string { return 'id'; }
// ❌ NO declarar manualmente (configuración redundante)
public static function schemaLevel(): int { return 3; }
}Resolución Dinámica
php
// MultiSchemaService::getTableLevel()
public function getTableLevel(string $tableName): int
{
// 1. Consultar information_schema para descubrir en qué schemas existe
$schemas = $this->discoverTableSchemas($tableName);
// 2. Determinar nivel según patrón de schemas
// - Si está en public → EMPRESA (nivel 1)
// - Si está en suc\d+ → SUCURSAL (nivel 2)
// - Si está en suc\d+caja\d+ → CAJA (nivel 3)
return $this->inferLevelFromSchemas($schemas);
}Justificación
- DRY: No duplicar información que existe en la base de datos
- Flexibilidad: Cambios de nivel no requieren modificar código
- Descubrimiento: Estructura de schemas como única fuente de verdad
Excepción
En configuracion_niveles_tablas (JSON) se PUEDE configurar nivel de tabla para casos especiales, pero solo como override opcional.
Relacionada con
- RA-MT-003 (Datos Maestros Compartidos)
Matriz de Decisión: Qué Patrón Usar
| Escenario | Schemas | Patrón | Regla Aplicable |
|---|---|---|---|
| Cliente → Órdenes (mismo schema) | 1 | JoinSpec::auto() | RA-JOIN-001 |
| CAJA → SUCURSAL (1 query) | 2 jerárquicos | JoinSpec::autoWithSchema() | RA-JOIN-003 |
| Todas las cajas con JOIN | N horizontales | executeMultiSchema() | RA-JOIN-005 |
| Entre sucursales diferentes | 2+ diferentes branches | ❌ PROHIBIDO | RA-JOIN-004 |
Validación de Cumplimiento
Checklist de Code Review
- [ ] RA-JOIN-001: Models no contienen JOINs hardcodeados
- [ ] RA-JOIN-002: JoinSpecs creados directamente en Query Classes
- [ ] RA-JOIN-003: Cross-level solo hacia arriba en jerarquía
- [ ] RA-JOIN-004: No hay JOINs cross-schema horizontales
- [ ] RA-JOIN-005: Multi-schema usa
executeMultiSchema()con UNION ALL - [ ] RA-JOIN-006: Models no declaran
schemaLevel()manualmente
Herramientas de Validación
bash
# Detectar JOINs hardcodeados en Models
grep -r "LEFT JOIN\|INNER JOIN" server/models/
# Detectar schemaLevel manual (deprecado)
grep -r "public static function schemaLevel" server/models/
# Detectar loops manuales (anti-pattern)
grep -r "foreach.*schema.*SET search_path" server/service/Referencias
Documentación de JOINs
- Index - Resumen ejecutivo
- Guía Completa - Componentes y modos
- Casos Simple - JOINs en mismo schema
- Cross-Level Directo - JOINs jerárquicos
- Casos Multi-Schema - Consolidación UNION ALL
Reglas de Arquitectura de Base de Datos
Última actualización: 2026-02-04 Versión: 1.0.0 Autor: Sistema Bautista - Arquitectura Backend