Appearance
Multi-Schema: Cross-Schema Querying
QUÉ es Multi-Schema Querying
Definición: Capacidad de buscar o consolidar datos en múltiples schemas PostgreSQL simultáneamente dentro de un mismo request. En lugar de trabajar en 1 schema (multi-tenant), trabajamos en N schemas a la vez.
Diferencia con multi-tenant:
- Multi-tenant: 1 request → 1 schema → Aislamiento
- Multi-schema: 1 request → N schemas → Consolidación o búsqueda
Analogía: Multi-tenant es como abrir una sola carpeta (schema). Multi-schema es como buscar un archivo en todas las carpetas de cajas de una sucursal sin saber en cuál está.
Principio fundamental: Ejecutar la misma query en múltiples schemas y consolidar resultados usando UNION ALL (cuando es posible) o loops iterativos (cuando no hay alternativa).
CUÁNDO usar:
- ❓ Buscar un registro sin saber en qué caja está (ej: recibo generado en caja desconocida)
- 📊 Consolidar datos de múltiples cajas en un reporte (ej: ventas totales de sucursal)
- 🔄 Operaciones cross-schema (ej: transferir registro entre cajas)
- 🔍 Validaciones que requieren ver datos de múltiples schemas
Problema que Resuelve
Escenario Real
Sin multi-schema:
Usuario en caja001: "Quiero ver el recibo #12345"
Sistema busca en: suc0001caja001.recibos
Resultado: ❌ ERROR - Recibo no encontrado
Realidad: El recibo fue generado en caja002Con multi-schema:
Usuario en caja001: "Quiero ver el recibo #12345"
Sistema busca en:
1. suc0001caja001.recibos → No encontrado
2. suc0001caja002.recibos → ✅ ENCONTRADO
3. (no continúa buscando)
Resultado: Recibo encontrado con metadata _schema = "suc0001caja002"Casos de Uso Comunes
| Caso | Descripción | Schemas Involucrados | Estrategia |
|---|---|---|---|
| Búsqueda de recibo | Usuario busca recibo sin saber en qué caja fue generado | Todas las cajas de la sucursal | Early exit (findFirstAcrossSchemas) |
| Consolidación de ventas | Reporte de ventas totales de todas las cajas | Todas las cajas de la sucursal | Batch (queryAcrossSchemas) |
| Validación de duplicados | Verificar que no exista factura duplicada en ninguna caja | Todas las cajas de la sucursal | Early exit (existe en alguna?) |
| Transferencia entre cajas | Mover registro de caja A a caja B | 2 cajas específicas | Transaccional multi-schema |
CrossSchemaQueryable Pattern
Componentes del Pattern
mermaid
graph TB
subgraph "Application Layer"
SERVICE[Service<br/>ej: ReciboService]
end
subgraph "Multi-Schema Pattern"
INTERFACE[CrossSchemaQueryableInterface<br/>Contrato]
TRAIT[CrossSchemaQueryable Trait<br/>Implementación default]
MSS[MultiSchemaService<br/>Resolución de schemas]
SS[SchemaService<br/>Consulta information_schema]
end
subgraph "Database Layer"
CU[ConnectionUtils<br/>buildSearchPath, setSchemaContext]
CM[ConnectionManager<br/>getConnection]
PG[(PostgreSQL<br/>N schemas)]
end
SERVICE -->|implements| INTERFACE
SERVICE -->|use| TRAIT
TRAIT -->|getCurrentSchema| MSS
TRAIT -->|isMultiSchemaActive| MSS
TRAIT -->|getTargetSchemas| MSS
MSS -->|schemaExists| SS
MSS -->|getAllSchemasForBranch| SS
TRAIT -->|setSchemaContext| CU
TRAIT -->|getConnection| CM
CU -->|SET search_path| PG
CM -->|Connection| PG
style SERVICE fill:#90EE90
style INTERFACE fill:#87CEEB
style TRAIT fill:#FFD700
style MSS fill:#FFA500Responsabilidades de Cada Componente
| Componente | Responsabilidad | Métodos Clave |
|---|---|---|
| CrossSchemaQueryableInterface | Contrato que define métodos cross-schema | queryAcrossSchemas(), findFirstAcrossSchemas() |
| CrossSchemaQueryable Trait | Implementación default del contrato | Loops, conexión por schema, metadata _schema |
| MultiSchemaService | Determina CUÁNDO y EN QUÉ schemas buscar | isMultiSchemaActive(), getTargetSchemas() |
| SchemaService | Consulta information_schema, validación | getAllSchemasForBranch(), schemaExists() |
| ConnectionUtils | Establece schema context por conexión | setSchemaContext(), buildSearchPath() |
Dos Estrategias de Búsqueda
queryAcrossSchemas() - Batch/Accumulate
Propósito: Consolidar TODOS los resultados de TODOS los schemas.
Cuándo usar: Reportes, consolidaciones, operaciones que necesitan ver todos los datos.
Comportamiento:
- Busca en TODOS los schemas (no para al encontrar)
- Acumula resultados en array
- Agrega metadata
_schemaa cada registro - Retorna array completo
Ejemplo:
php
// Buscar ventas en TODAS las cajas
$ventas = $this->queryAcrossSchemas(function($connection) {
return $connection->fetchAllAssociative("SELECT * FROM ventas WHERE fecha = :fecha", [
'fecha' => '2025-02-03'
]);
});
// Resultado:
[
['id' => 1, 'total' => 1000, '_schema' => 'suc0001caja001'],
['id' => 2, 'total' => 2000, '_schema' => 'suc0001caja001'],
['id' => 1, 'total' => 1500, '_schema' => 'suc0001caja002'], // ← Mismo ID, diferente schema
['id' => 2, 'total' => 2500, '_schema' => 'suc0001caja002'],
]findFirstAcrossSchemas() - Early Exit
Propósito: Encontrar el PRIMER resultado y detener búsqueda (optimización).
Cuándo usar: Búsquedas de registro único, validaciones de existencia.
Comportamiento:
- Busca en schemas secuencialmente
- PARA al encontrar primer resultado
- Agrega metadata
_schemaal registro encontrado - Retorna 1 registro o null
Ejemplo:
php
// Buscar recibo #12345 (sin saber en qué caja está)
$recibo = $this->findFirstAcrossSchemas(function($connection) {
return $connection->fetchAssociative("SELECT * FROM recibos WHERE id = :id", [
'id' => 12345
]);
});
// Resultado (detuvo búsqueda al encontrar):
['id' => 12345, 'monto' => 5000, '_schema' => 'suc0001caja002']Comparación de Estrategias
| Aspecto | queryAcrossSchemas | findFirstAcrossSchemas |
|---|---|---|
| Propósito | Consolidación total | Búsqueda con early exit |
| Schemas recorridos | TODOS (N schemas) | Hasta encontrar (≤ N schemas) |
| Resultados | Array de arrays (múltiples registros) | 1 registro o null |
| Optimización | No para al encontrar | Para al encontrar |
| Metadata _schema | En cada registro | En el registro encontrado |
| Uso típico | Reportes, consolidación | Búsquedas por ID, validaciones |
| Performance | O(N schemas) SIEMPRE | O(1) a O(N schemas) dependiendo de dónde esté |
Flujo de Búsqueda Multi-Schema
Diagrama de Secuencia Completo
mermaid
sequenceDiagram
participant SVC as Service<br/>(ReciboService)
participant TRAIT as CrossSchemaQueryable<br/>Trait
participant MSS as MultiSchemaService
participant CU as ConnectionUtils
participant CM as ConnectionManager
participant PG as PostgreSQL
SVC->>TRAIT: findFirstAcrossSchemas(callback)
TRAIT->>MSS: getCurrentSchema()
MSS-->>TRAIT: "suc0001caja001"
TRAIT->>MSS: isMultiSchemaActive()
MSS->>MSS: Check: busqueda_en_todas_las_cajas = true
MSS-->>TRAIT: true
TRAIT->>MSS: getTargetSchemas()
MSS->>MSS: Extraer sucursal: suc0001caja001 → suc0001
MSS->>MSS: Consultar: getAllSchemasForBranch("suc0001")
MSS-->>TRAIT: ["suc0001caja001", "suc0001caja002", "suc0001caja003"]
loop Para cada schema (early exit)
TRAIT->>CU: setSchemaContext(connection, "suc0001caja001")
CU->>PG: SET search_path = suc0001caja001, suc0001, public
PG-->>CU: OK
TRAIT->>SVC: callback(connection)
SVC->>PG: SELECT * FROM recibos WHERE id = 12345
PG-->>SVC: [] (vacío)
SVC-->>TRAIT: null
TRAIT->>CU: setSchemaContext(connection, "suc0001caja002")
CU->>PG: SET search_path = suc0001caja002, suc0001, public
PG-->>CU: OK
TRAIT->>SVC: callback(connection)
SVC->>PG: SELECT * FROM recibos WHERE id = 12345
PG-->>SVC: {id: 12345, monto: 5000}
SVC-->>TRAIT: {id: 12345, monto: 5000}
TRAIT->>TRAIT: Attach metadata: _schema = "suc0001caja002"
TRAIT-->>SVC: {id: 12345, monto: 5000, _schema: "suc0001caja002"}
Note over TRAIT: Early exit: Encontrado, no continúa con caja003
end
Note over SVC,PG: Multi-Schema activo:<br/>Buscó en 2 de 3 schemas<br/>Early exit optimizó performanceConsolidación vs Búsqueda
Diferencias Clave
| Aspecto | Consolidación | Búsqueda |
|---|---|---|
| Propósito | Agregar datos de múltiples schemas | Encontrar 1 registro en schemas desconocido |
| Alcance | TODOS los schemas (completo) | Hasta encontrar (parcial) |
| Schemas | N schemas SIEMPRE | 1 a N schemas (early exit) |
| Optimización | UNION ALL (query única) | Loop con early exit |
| Activación | consolidacion_multi_schema=true | busqueda_en_todas_las_cajas=true |
| RA aplicable | RA-MS-001, RA-MS-002 | RA-MS-001, RA-MS-003 |
Consolidación (Ejemplo)
php
// Consolidar ventas de TODAS las cajas (sin early exit)
$ventasTotales = $this->queryAcrossSchemas(function($connection) {
return $connection->fetchAllAssociative("
SELECT fecha, SUM(total) as total_dia
FROM facturas
WHERE fecha BETWEEN :inicio AND :fin
GROUP BY fecha
", [
'inicio' => '2025-02-01',
'fin' => '2025-02-28'
]);
});
// Resultado (consolidado con _schema):
[
['fecha' => '2025-02-01', 'total_dia' => 10000, '_schema' => 'suc0001caja001'],
['fecha' => '2025-02-01', 'total_dia' => 15000, '_schema' => 'suc0001caja002'],
['fecha' => '2025-02-02', 'total_dia' => 12000, '_schema' => 'suc0001caja001'],
['fecha' => '2025-02-02', 'total_dia' => 18000, '_schema' => 'suc0001caja002'],
]Búsqueda (Ejemplo)
php
// Buscar factura #999 (con early exit)
$factura = $this->findFirstAcrossSchemas(function($connection) {
return $connection->fetchAssociative("
SELECT * FROM facturas WHERE id = :id
", ['id' => 999]);
});
// Resultado (detuvo al encontrar):
['id' => 999, 'total' => 5000, '_schema' => 'suc0001caja002']
// NO continuó buscando en caja003, caja004, etc.Optimización: UNION ALL
Concepto
Problema: Ejecutar N queries independientes (1 por schema) es ineficiente:
- N round-trips a PostgreSQL
- N conexiones activas
- Memoria proporcional a N schemas
Solución: Generar 1 query SQL con UNION ALL que consulte N schemas en una sola ejecución.
Antes vs Después
Antes (Loop Manual)
php
// 3 queries separadas (3 round-trips)
$results = [];
// Query 1
SET search_path = suc0001caja001, suc0001, public;
SELECT * FROM recibos WHERE fecha = '2025-02-03'; // → 10 registros
// Query 2
SET search_path = suc0001caja002, suc0001, public;
SELECT * FROM recibos WHERE fecha = '2025-02-03'; // → 15 registros
// Query 3
SET search_path = suc0001caja003, suc0001, public;
SELECT * FROM recibos WHERE fecha = '2025-02-03'; // → 12 registros
// Total: 3 round-trips, 37 registrosDespués (UNION ALL)
php
// 1 query única (1 round-trip)
SELECT *, 'suc0001caja001' AS _schema FROM suc0001caja001.recibos WHERE fecha = '2025-02-03'
UNION ALL
SELECT *, 'suc0001caja002' AS _schema FROM suc0001caja002.recibos WHERE fecha = '2025-02-03'
UNION ALL
SELECT *, 'suc0001caja003' AS _schema FROM suc0001caja003.recibos WHERE fecha = '2025-02-03';
// Total: 1 round-trip, 37 registros (mismo resultado, 3x más rápido)Métricas de Performance
| Métrica | Loop Manual | UNION ALL | Mejora |
|---|---|---|---|
| Round-trips | N (ej: 6) | 1 | 6x más rápido |
| Conexiones activas | N | 1 | 6x menos memoria |
| Latencia de red | N × latencia | 1 × latencia | 6x menos latencia |
| Lock time | N × lock | 1 × lock | 6x menos lock |
| Memoria PHP | O(N × resultados) | O(resultados) | 99% menos memoria |
Ejemplo real: 6 schemas con 1000 registros cada uno
- Loop: 6 queries × 100ms = 600ms
- UNION ALL: 1 query × 100ms = 100ms
- Mejora: 6x más rápido
Implementación Conceptual
php
// Método en CrossSchemaQueryable Trait
public function queryAcrossSchemasWithUnion(
string $baseQuery,
array $params,
array $schemas
): array {
// Construir UNION ALL con metadata
$unionParts = [];
foreach ($schemas as $schema) {
// Agregar _schema literal a cada SELECT
$unionParts[] = "
SELECT *, '{$schema}' AS _schema
FROM {$schema}.{$tabla}
WHERE {$condiciones}
";
}
$fullQuery = implode(' UNION ALL ', $unionParts);
// Ejecutar 1 sola vez
return $connection->fetchAllAssociative($fullQuery, $params);
}MultiSchemaService
Responsabilidades
| Método | Propósito | Retorno |
|---|---|---|
getCurrentSchema() | Obtener schema actual del request | string (ej: "suc0001caja001") |
isMultiSchemaActive() | Determinar si multi-schema está activo | bool |
getTargetSchemas() | Resolver lista de schemas a consultar | array<string> |
Reglas de Activación (RA-MS-001)
Multi-schema se activa SI:
- ✅ Configuración
busqueda_en_todas_las_cajas = true(nivel caja) - ✅ Configuración
consolidacion_multi_schema = true(nivel sucursal) - ✅ Parámetro
multi_schema = trueen request (override manual)
SINO: Multi-schema está INACTIVO → usar solo schema actual (multi-tenant normal)
Tabla de Reglas de Activación
| Configuración | Valor | Resultado | Schemas Consultados |
|---|---|---|---|
busqueda_en_todas_las_cajas | true | ✅ Activo | Todas las cajas de la sucursal |
busqueda_en_todas_las_cajas | false | ❌ Inactivo | Solo schema actual |
consolidacion_multi_schema | true | ✅ Activo | Todas las cajas de la sucursal |
multi_schema (request param) | true | ✅ Activo (override) | Todas las cajas de la sucursal |
| Ninguna activa | - | ❌ Inactivo | Solo schema actual (multi-tenant) |
Lógica Conceptual
php
// MultiSchemaService::isMultiSchemaActive()
public function isMultiSchemaActive(): bool
{
// 1. Override manual por request
if ($this->request->getQueryParams()['multi_schema'] ?? false) {
return true;
}
// 2. Configuración: busqueda_en_todas_las_cajas
$config = $this->configService->get('busqueda_en_todas_las_cajas');
if ($config === true) {
return true;
}
// 3. Configuración: consolidacion_multi_schema
$config = $this->configService->get('consolidacion_multi_schema');
if ($config === true) {
return true;
}
// 4. Default: Inactivo
return false;
}
// MultiSchemaService::getTargetSchemas()
public function getTargetSchemas(): array
{
if (!$this->isMultiSchemaActive()) {
// Multi-schema inactivo: solo schema actual
return [$this->getCurrentSchema()];
}
// Multi-schema activo: extraer sucursal y obtener todas sus cajas
$currentSchema = $this->getCurrentSchema(); // ej: suc0001caja001
// Extraer sucursal: suc0001caja001 → suc0001
preg_match('/^(suc\d+)/', $currentSchema, $matches);
$sucursal = $matches[1] ?? null;
if (!$sucursal) {
return [$currentSchema]; // Fallback si no se puede extraer
}
// Consultar information_schema: schemas que empiecen con suc0001caja
return $this->schemaService->getAllSchemasForBranch($sucursal);
}JOINs Cross-Schema
Problema
Caso: Necesitamos hacer JOIN entre:
- Tabla A en
suc0001caja001(nivel caja) - Tabla B en
suc0001(nivel sucursal)
Desafío: search_path solo permite buscar en 1 schema a la vez para tabla A.
Solución: Schema Mapping + UNION ALL Configurables
Concepto: Para cada schema objetivo (caja), mapear el schema correspondiente de tablas relacionadas (sucursal).
Ejemplo SQL Generado:
sql
-- Schema 1: caja001 JOIN sucursal
SELECT r.*, f.total, 'suc0001caja001' AS _schema
FROM suc0001caja001.recibos r
INNER JOIN suc0001.facturas f ON r.factura_id = f.id
WHERE r.fecha = '2025-02-03'
UNION ALL
-- Schema 2: caja002 JOIN sucursal
SELECT r.*, f.total, 'suc0001caja002' AS _schema
FROM suc0001caja002.recibos r
INNER JOIN suc0001.facturas f ON r.factura_id = f.id
WHERE r.fecha = '2025-02-03'
UNION ALL
-- Schema 3: caja003 JOIN sucursal
SELECT r.*, f.total, 'suc0001caja003' AS _schema
FROM suc0001caja003.recibos r
INNER JOIN suc0001.facturas f ON r.factura_id = f.id
WHERE r.fecha = '2025-02-03';Mapeo de Schemas:
| Schema Caja | Schema Sucursal (para JOIN) | Schema Empresa (para maestros) |
|---|---|---|
suc0001caja001 | suc0001 | public |
suc0001caja002 | suc0001 | public |
suc0002caja001 | suc0002 | public |
Referencia completa: Ver documentación de JOINs en docs/architecture/patrones-php/joins/:
- Casos Multi-Schema - Consolidación con UNION ALL
- Cross-Level Directo - JOINs jerárquicos
- Reglas Arquitecturales - RA-JOIN-*
Reglas Arquitecturales
RA-MS-001: Activación Basada en Configuración
Descripción: Multi-schema querying SOLO se activa si está configurado explícitamente. NO es el comportamiento default.
Implicación:
- ✅ Default: 1 schema (multi-tenant normal)
- ✅ Activar con:
busqueda_en_todas_las_cajas=trueoconsolidacion_multi_schema=true - ❌ NO asumir multi-schema activo sin verificar
RA-MS-002: Alcance Limitado por Sucursal
Descripción: Multi-schema querying DEBE limitarse a schemas de la misma sucursal. NO cruzar entre sucursales.
Implicación:
- ✅ suc0001caja001 puede buscar en: suc0001caja002, suc0001caja003
- ❌ suc0001caja001 NO puede buscar en: suc0002caja001
- ⚠️ Excepción: Admin con permisos cross-sucursal explícitos
Validación:
php
// SchemaService::getAllSchemasForBranch('suc0001')
public function getAllSchemasForBranch(string $sucursal): array
{
$sql = "SELECT schema_name
FROM information_schema.schemata
WHERE schema_name LIKE :pattern
ORDER BY schema_name";
// Solo schemas que empiecen con suc0001caja
$result = $connection->fetchFirstColumn($sql, [
'pattern' => $sucursal . 'caja%'
]);
return $result;
}RA-MS-003: Consistencia de Schema en Recursos Relacionados
Descripción: Los recursos relacionados (ej: factura + items) DEBEN estar en el mismo schema. NO permitir items en schema diferente al de la factura.
Implicación:
- ✅ Factura en suc0001caja001 → Items en suc0001caja001
- ❌ Factura en suc0001caja001 → Items en suc0001caja002 (inconsistencia)
- 🔒 Validar en Service antes de crear/actualizar
Ejemplo de validación:
php
public function createFactura(array $data, array $items): array
{
$schema = $this->getCurrentSchema(); // ej: suc0001caja001
// Validar que todos los items estén en el mismo schema
foreach ($items as $item) {
if (isset($item['_schema']) && $item['_schema'] !== $schema) {
throw new \InvalidArgumentException(
"Item no puede estar en schema diferente al de la factura"
);
}
}
// Crear factura + items en mismo schema (transacción)
// ...
}RA-MS-004: Transaccionalidad Atómica
Descripción: Operaciones multi-schema que modifican datos DEBEN usar transacciones independientes por schema. NO usar transacciones cross-schema.
Implicación:
- ✅ 1 transacción por schema (aislamiento)
- ❌ NO usar BEGIN en schema A y modificar schema B (no soportado por PostgreSQL)
- ⚠️ Implementar compensación manual si falla alguna transacción
Pattern de transacciones multi-schema:
php
public function transferRecordAcrossSchemas(int $id, string $targetSchema): void
{
$sourceSchema = $this->getCurrentSchema();
$commitedSchemas = [];
try {
// 1. Transacción en schema origen
ConnectionManager::beginTransaction('principal');
$record = $this->deleteFromSchema($id, $sourceSchema);
ConnectionManager::commit('principal');
$commitedSchemas[] = $sourceSchema;
// 2. Cambiar schema context
$this->setSchemaContext($targetSchema);
// 3. Transacción en schema destino
ConnectionManager::beginTransaction('principal');
$this->insertIntoSchema($record, $targetSchema);
ConnectionManager::commit('principal');
$commitedSchemas[] = $targetSchema;
} catch (\Exception $e) {
// Rollback manual de schemas ya commiteados
$this->compensateFailedTransfer($commitedSchemas, $record);
throw $e;
}
}RA-MS-005: Extensibilidad por Módulo
Descripción: Cada módulo (Ventas, CtaCte, etc.) DEBE implementar sus propios métodos cross-schema. NO crear métodos genéricos en Trait.
Implicación:
- ✅
ReciboService::findReciboAcrossSchemas()(específico) - ❌
CrossSchemaQueryable::findByIdAcrossSchemas()(demasiado genérico) - 🎯 Métodos específicos permiten optimizaciones y validaciones por módulo
Referencias a Implementación
Documentación Técnica
docs/backend/cross-schema-queryable-pattern.md- Patrón completo con ejemplos de códigodocs/architecture/multi-schema-transactional-operations-process.md- Operaciones transaccionales específicasdocs/architecture/consolidacion-informes-multi-schema.md- Consolidación en informesdocs/architecture/patrones-php/joins/- Patrón unificado de JOINs:- Index - Resumen ejecutivo
- Casos Multi-Schema - Consolidación con UNION ALL
- Cross-Level Directo - JOINs jerárquicos
- Reglas Arquitecturales - RA-JOIN-*
Código Fuente
server/Interface/MultiSchema/CrossSchemaQueryableInterface.php- Contrato del patrónserver/Traits/CrossSchemaQueryable.php- Implementación default (loop manual)server/service/Config/MultiSchemaService.php- Lógica de activación y resoluciónserver/service/Config/SchemaService.php- Consultainformation_schema
Skills de Claude Code
.claude/skills/bautista-multi-schema-joins/- Implementar JOINs cross-schema
Ejemplos de Uso
Ejemplo 1: Búsqueda de Recibo (Early Exit)
php
namespace App\service\CtaCte;
use App\Interface\MultiSchema\CrossSchemaQueryableInterface;
use App\Traits\CrossSchemaQueryable;
class ReciboService implements CrossSchemaQueryableInterface
{
use CrossSchemaQueryable;
public function findReciboById(int $id): ?array
{
// Si multi-schema está inactivo: busca solo en schema actual
// Si multi-schema está activo: busca en todas las cajas con early exit
return $this->findFirstAcrossSchemas(function($connection) use ($id) {
return $connection->fetchAssociative("
SELECT * FROM recibos WHERE id = :id
", ['id' => $id]);
});
}
// Resultado:
// Multi-schema inactivo: Busca solo en suc0001caja001
// Multi-schema activo: Busca en suc0001caja001, suc0001caja002, ... hasta encontrar
// Retorna: ['id' => 12345, 'monto' => 5000, '_schema' => 'suc0001caja002'] o null
}Ejemplo 2: Consolidación de Ventas (Batch)
php
namespace App\service\Ventas;
use App\Interface\MultiSchema\CrossSchemaQueryableInterface;
use App\Traits\CrossSchemaQueryable;
class VentaService implements CrossSchemaQueryableInterface
{
use CrossSchemaQueryable;
public function getVentasPorDia(string $fechaInicio, string $fechaFin): array
{
// Consolida ventas de TODAS las cajas (no early exit)
return $this->queryAcrossSchemas(function($connection) use ($fechaInicio, $fechaFin) {
return $connection->fetchAllAssociative("
SELECT fecha, SUM(total) as total_dia
FROM facturas
WHERE fecha BETWEEN :inicio AND :fin
GROUP BY fecha
", [
'inicio' => $fechaInicio,
'fin' => $fechaFin
]);
});
}
// Resultado:
// Multi-schema inactivo: Solo ventas de suc0001caja001
// Multi-schema activo: Ventas de suc0001caja001 + suc0001caja002 + suc0001caja003
// Retorna:
// [
// ['fecha' => '2025-02-01', 'total_dia' => 10000, '_schema' => 'suc0001caja001'],
// ['fecha' => '2025-02-01', 'total_dia' => 15000, '_schema' => 'suc0001caja002'],
// ...
// ]
}Ejemplo 3: DTO con Metadata _schema
php
namespace App\Resources\CtaCte;
class ReciboDTO
{
public int $id;
public float $monto;
public string $fecha;
// Metadata multi-schema
public ?string $_schema = null; // ← Schema donde se encontró el recibo
public static function fromArray(array $data): self
{
$dto = new self();
$dto->id = $data['id'];
$dto->monto = $data['monto'];
$dto->fecha = $data['fecha'];
// Preservar metadata
$dto->_schema = $data['_schema'] ?? null;
return $dto;
}
public function toArray(): array
{
return [
'id' => $this->id,
'monto' => $this->monto,
'fecha' => $this->fecha,
'_schema' => $this->_schema, // ← Incluir en respuesta
];
}
}Respuesta JSON al frontend:
json
{
"id": 12345,
"monto": 5000,
"fecha": "2025-02-03",
"_schema": "suc0001caja002"
}Uso en frontend:
typescript
// Frontend puede mostrar en qué caja se generó el recibo
<p>Recibo #{recibo.id} - Generado en: {recibo._schema}</p>Relación con Multi-Tenant y Multi-Modo
Multi-Schema vs Multi-Tenant
| Aspecto | Multi-Tenant | Multi-Schema |
|---|---|---|
| Schemas por request | 1 schema | N schemas |
| Propósito | Aislamiento | Consolidación/búsqueda |
| Activación | SIEMPRE | Solo si configurado |
| Relación | Base | Extensión |
Combinación: Multi-tenant establece schema inicial. Multi-schema expande búsqueda a otros schemas.
Multi-Schema vs Multi-Modo
| Aspecto | Multi-Schema | Multi-Modo |
|---|---|---|
| Dimensión | Schemas (N en 1 DB) | Databases (2 DBs) |
| Propósito | Búsqueda cross-schema | Separación oficial/prueba |
| Activación | Config o request | prueba parameter |
| Relación | Independientes | Ortogonales |
Combinación: Puedes hacer multi-schema en database oficial O database prueba.
Ejemplo: Request con prueba=true y busqueda_en_todas_las_cajas=true:
- Multi-modo → Database:
bautista_p - Multi-schema → Buscar en:
suc0001caja001,suc0001caja002,suc0001caja003(todos enbautista_p)
Siguiente paso: Leer Multi-Modo (Dual Database Pattern) para entender separación oficial/prueba.
Referencias:
- Índice de Database Architecture
- Multi-Tenant (Schema-Based Tenancy)
- CrossSchemaQueryable Pattern (Implementación)
- Patrón Unificado de JOINs - Arquitectura completa de JOINs
- Casos Multi-Schema - Ejemplos con UNION ALL