Skip to content

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 caja002

Con 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

CasoDescripciónSchemas InvolucradosEstrategia
Búsqueda de reciboUsuario busca recibo sin saber en qué caja fue generadoTodas las cajas de la sucursalEarly exit (findFirstAcrossSchemas)
Consolidación de ventasReporte de ventas totales de todas las cajasTodas las cajas de la sucursalBatch (queryAcrossSchemas)
Validación de duplicadosVerificar que no exista factura duplicada en ninguna cajaTodas las cajas de la sucursalEarly exit (existe en alguna?)
Transferencia entre cajasMover registro de caja A a caja B2 cajas específicasTransaccional 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:#FFA500

Responsabilidades de Cada Componente

ComponenteResponsabilidadMétodos Clave
CrossSchemaQueryableInterfaceContrato que define métodos cross-schemaqueryAcrossSchemas(), findFirstAcrossSchemas()
CrossSchemaQueryable TraitImplementación default del contratoLoops, conexión por schema, metadata _schema
MultiSchemaServiceDetermina CUÁNDO y EN QUÉ schemas buscarisMultiSchemaActive(), getTargetSchemas()
SchemaServiceConsulta information_schema, validacióngetAllSchemasForBranch(), schemaExists()
ConnectionUtilsEstablece schema context por conexiónsetSchemaContext(), 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:

  1. Busca en TODOS los schemas (no para al encontrar)
  2. Acumula resultados en array
  3. Agrega metadata _schema a cada registro
  4. 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:

  1. Busca en schemas secuencialmente
  2. PARA al encontrar primer resultado
  3. Agrega metadata _schema al registro encontrado
  4. 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

AspectoqueryAcrossSchemasfindFirstAcrossSchemas
PropósitoConsolidación totalBúsqueda con early exit
Schemas recorridosTODOS (N schemas)Hasta encontrar (≤ N schemas)
ResultadosArray de arrays (múltiples registros)1 registro o null
OptimizaciónNo para al encontrarPara al encontrar
Metadata _schemaEn cada registroEn el registro encontrado
Uso típicoReportes, consolidaciónBúsquedas por ID, validaciones
PerformanceO(N schemas) SIEMPREO(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ó performance

Consolidación vs Búsqueda

Diferencias Clave

AspectoConsolidaciónBúsqueda
PropósitoAgregar datos de múltiples schemasEncontrar 1 registro en schemas desconocido
AlcanceTODOS los schemas (completo)Hasta encontrar (parcial)
SchemasN schemas SIEMPRE1 a N schemas (early exit)
OptimizaciónUNION ALL (query única)Loop con early exit
Activaciónconsolidacion_multi_schema=truebusqueda_en_todas_las_cajas=true
RA aplicableRA-MS-001, RA-MS-002RA-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 registros

Despué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étricaLoop ManualUNION ALLMejora
Round-tripsN (ej: 6)16x más rápido
Conexiones activasN16x menos memoria
Latencia de redN × latencia1 × latencia6x menos latencia
Lock timeN × lock1 × lock6x menos lock
Memoria PHPO(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étodoPropósitoRetorno
getCurrentSchema()Obtener schema actual del requeststring (ej: "suc0001caja001")
isMultiSchemaActive()Determinar si multi-schema está activobool
getTargetSchemas()Resolver lista de schemas a consultararray<string>

Reglas de Activación (RA-MS-001)

Multi-schema se activa SI:

  1. ✅ Configuración busqueda_en_todas_las_cajas = true (nivel caja)
  2. ✅ Configuración consolidacion_multi_schema = true (nivel sucursal)
  3. ✅ Parámetro multi_schema = true en request (override manual)

SINO: Multi-schema está INACTIVO → usar solo schema actual (multi-tenant normal)

Tabla de Reglas de Activación

ConfiguraciónValorResultadoSchemas Consultados
busqueda_en_todas_las_cajastrue✅ ActivoTodas las cajas de la sucursal
busqueda_en_todas_las_cajasfalse❌ InactivoSolo schema actual
consolidacion_multi_schematrue✅ ActivoTodas las cajas de la sucursal
multi_schema (request param)true✅ Activo (override)Todas las cajas de la sucursal
Ninguna activa-❌ InactivoSolo 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 CajaSchema Sucursal (para JOIN)Schema Empresa (para maestros)
suc0001caja001suc0001public
suc0001caja002suc0001public
suc0002caja001suc0002public

Referencia completa: Ver documentación de JOINs en docs/architecture/patrones-php/joins/:

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=true o consolidacion_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

Código Fuente

  • server/Interface/MultiSchema/CrossSchemaQueryableInterface.php - Contrato del patrón
  • server/Traits/CrossSchemaQueryable.php - Implementación default (loop manual)
  • server/service/Config/MultiSchemaService.php - Lógica de activación y resolución
  • server/service/Config/SchemaService.php - Consulta information_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

AspectoMulti-TenantMulti-Schema
Schemas por request1 schemaN schemas
PropósitoAislamientoConsolidación/búsqueda
ActivaciónSIEMPRESolo si configurado
RelaciónBaseExtensión

Combinación: Multi-tenant establece schema inicial. Multi-schema expande búsqueda a otros schemas.

Multi-Schema vs Multi-Modo

AspectoMulti-SchemaMulti-Modo
DimensiónSchemas (N en 1 DB)Databases (2 DBs)
PropósitoBúsqueda cross-schemaSeparación oficial/prueba
ActivaciónConfig o requestprueba parameter
RelaciónIndependientesOrtogonales

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 en bautista_p)

Siguiente paso: Leer Multi-Modo (Dual Database Pattern) para entender separación oficial/prueba.

Referencias: