Saltar a contenido

Persistencia (Hibernate / JPA)

Cómo persiste el backend: provider, dialecto propio, modo de flush, naming strategy y administración del DDL. Todo está configurado en application.yml y en com.tipre.tifacturaonlinemanager.action.db.SQLServerDialect + config.SeamCompatConfig. Para las entidades concretas ver Modelo de dominio; para por qué el flush es MANUAL, Invariantes.

De un vistazo

Aspecto Valor Dónde
Provider Hibernate 7.2.x (vía BOM Spring Boot 4.0) pom.xml (BOM)
Starter spring-boot-starter-data-jpa pom.xml
Dialecto com.tipre.tifacturaonlinemanager.action.db.SQLServerDialect (propio) spring.jpa.database-platform
ddl-auto none application.yml
Naming físico PhysicalNamingStrategyStandardImpl application.yml
open-in-view false application.yml
Flush por defecto MANUAL (FlushModeType.COMMIT en el entityManager del shim) SeamCompatConfig
Driver com.microsoft.sqlserver.jdbc.SQLServerDriver (mssql-jdbc 12.8.1.jre11) pom.xml + application-{dev,prod}.yml
Cache 2do nivel No (sin @Cache; dependencia comentada en pom.xml)
Tests H2 (src/test/resources/application.yml pisa al base) pom.xml (scope test)

Dialecto propio: SQLServerDialect

action/db/SQLServerDialect.java extiende org.hibernate.dialect.SQLServerDialect (el de Hibernate, ya version-aware) y solo sobreescribe el mapeo de BOOLEAN:

public class SQLServerDialect extends org.hibernate.dialect.SQLServerDialect {
    @Override
    protected String columnType(int sqlTypeCode) {
        if (sqlTypeCode == SqlTypes.BOOLEAN) {
            return "bit";
        }
        return super.columnType(sqlTypeCode);
    }
}

Es un port del legacy (SPEC-002): el legacy registraba BOOLEAN -> "bit" para que los boolean del modelo (p. ej. EnteFacturador.habilitado, los flags de TipoComprobante) mapeen a columnas bit de SQL Server.

Hibernate 6 cambió la API

Hibernate 6 eliminó los dialectos versionados (SQLServer2012Dialect) y la API registerColumnType(int, String). El reemplazo idiomático es extender SQLServerDialect y sobreescribir columnType(int). El comentario del propio archivo lo deja claro.

El dialecto es prácticamente inerte

Con hbm2ddl=none la generación de DDL está deshabilitada, así que el override de BOOLEAN->bit solo afectaría a la generación de DDL, que no ocurre. Se conserva por fidelidad al legacy y trazabilidad (SPEC-002), no porque cambie el runtime.

DDL administrado por el cliente

spring.jpa.hibernate.ddl-auto: none. El esquema lo administra el cliente, la app NO lo genera ni lo altera. Esta es una invariante explícita en application.yml.

Las tablas nuevas que introdujo la migración (no del legacy) se versionan como scripts SQL en src/main/resources/db/migration/ para que el cliente los aplique a mano (no es Flyway/Liquibase automático — son scripts de referencia):

  • add-JobEjecucion.sql — tabla JobEjecucion.
  • add-TrxErrorLog.sql — tabla TrxErrorLog.
  • add-TrxErrorLog-mensajeCurado.sql — columna mensajeCurado en TrxErrorLog (mensaje humano del error curado). Orden de deploy sagrado: correr el script ANTES de subir el jar que la mapea; con la columna ausente Hibernate rompe al insertar/leer TrxErrorLog.
  • add-cert-EntesFacturadores.sql — columnas certP12* en EntesFacturadores.
  • add-EntesFacturadores-cuit-unique.sql — índice UQ_EntesFacturadores_cuit en EntesFacturadores(cuit), UNIQUE filtrado (WHERE cuit IS NOT NULL). El POS v2 identifica su comercio por CUIT, así que debe ser 1:1. Prerequisito: no puede haber CUIT duplicados antes de correrlo.
  • add-validadoArca-SucPosPV.sql, add-unique-indexes-SucPosPV.sql, add-PV-validacion-obtencion-SucPosPV.sql — columnas/índices de SucPosPV.
  • add-Trx-hot-query-indexes.sql — 4 índices NONCLUSTERED de soporte sobre Trx (2 filtrados) para las queries calientes: reconciliación de NC, dedup CAEA del notificador y el snapshot 5-min del cockpit. Ver detalle abajo.
  • add-CockpitStat.sql, add-Trx-*.sql, schedule-nc-job.sql, cleanup-parametros-legacy.sql, drop-tablas-sin-uso.sql, verify-cae-caea-nc.sql.

Índices calientes de Trx (add-Trx-hot-query-indexes.sql)

Son puro soporte de lectura (no cambian datos ni constraints) para las queries calientes que ningún índice previo podía seekear: la reconciliación de NC (NotaCreditoReconciliacionService), el dedup CAEA del notificador (CaeaNotificacionService.findExistingCaea) y el snapshot 5-min del cockpit. Sin ellos, cada corrida diaria de reconciliación y cada dedup CAEA escanean el clustered completo del histórico Trx (minutos con años de datos).

Índice Columnas Filtro Sirve a
IX_Trx_ticket_identity nroTicketPos, nroSuc, nroPos, idTipoComprobante, comprobanteFecha INCLUDE tipofacturacion WHERE nroTicketPos IS NOT NULL Sondas de identidad de ticket (CAE aprobados/huérfanos, hermanos CAEA). Excluye el grueso del histórico V1 (sin ticket).
IX_Trx_v2_cae_fecha comprobanteFecha, resultado INCLUDE nroSuc, nroPos, reintentos WHERE version='V2' AND tipofacturacion='CAE' Barrido externo de la reconciliación + scan periódico del cockpit (covering).
IX_Trx_caea_dedup ptoVta, comprobante WHERE version='V2' AND tipofacturacion='CAEA' Dedup CAEA del notificador (findExistingCaea).
IX_Trx_trxoriginal_nc trxoriginal INCLUDE tipofacturacion WHERE trxoriginal IS NOT NULL Sonda NOT EXISTS de NC (nc.trxoriginal = cae.id).

Los índices filtrados dependen de literales SQL

Los dos índices filtrados matchean porque las JPQL de la app renderean version / tipofacturacion como literales SQL ('V2', 'CAE', 'CAEA'). Si alguna de esas queries se parametrizara en el futuro, hay que revisar estos filtros. Correr una vez, en ventana de bajo tráfico (Standard Edition no tiene ONLINE=ON). Cada CREATE está guardado por IF NOT EXISTS (idempotente); aditivo y reversible (DROP INDEX ... ON dbo.Trx).

Validar mapeos contra la DB real

El perfil de test application-realdb.yml (LOCAL, gitignored) apunta a una copia de prod restaurada y usa ddl-auto: none igualmente — la validación real son las queries del test, no el validate de Hibernate (pedante con tipos equivalentes como numeric vs bigint que en runtime andan).

Naming strategy

spring.jpa.hibernate.naming.physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl.

La estrategia estándar respeta @Table/@Column tal cual: nombres literales en PascalCase, sin convertir a snake_case. Es el comportamiento del Hibernate 4 del legacy. Por eso las entidades pueden mapear tablas como EntesFacturadores, TiposComprobante, Caeas o columnas como idEnteFacturador y fFechaUltObtencionPOS sin transformación.

Flush MANUAL

El bean entityManager que usa el seam-compat se crea en SeamCompatConfig como shared entity manager y se le fija FlushModeType.COMMIT:

@Bean(name = "entityManager")
public EntityManager seamEntityManager(EntityManagerFactory emf) {
    EntityManager em = SharedEntityManagerCreator.createSharedEntityManager(emf);
    try { em.setFlushMode(FlushModeType.COMMIT); } catch (Exception ignored) {} // INVARIANTE 5.1 (MANUAL ~ COMMIT)
    return em;
}

Esto replica el default-flush-mode=MANUAL del components.xml legacy. En JPA no existe un MANUAL puro; COMMIT es el equivalente más cercano: Hibernate no auto-flushea antes de cada query, solo al commit de la transacción (o cuando se llama a flush() explícitamente).

Por eso el código de negocio hace flush() a mano donde necesita que algo pegue en la DB. El EntityHome del shim lo hace en persist()/update()/remove():

// org/jboss/seam/framework/EntityHome.java
getEntityManager().flush();   // INVARIANTE: flush explícito (modo MANUAL)
...
getEntityManager().flush();   // INVARIANTE 5.2: SOLO flush, sin merge

update() = solo flush, sin merge (invariante §5.2)

EntityHome.update() hace únicamente flush() sobre la entidad ya managed; no llama a merge(). El patrón de negocio (ParentEntityManager.updateOrPersistTrx) carga/setea la instancia en el Home y la deja en estado managed para que el flush la escriba. Romper esto (meter un merge) cambia la semántica de persistencia.

Otros puntos con flush() explícito: SucPosPVAdminService (backstop para que una violación de UQ salte en el flush y no en el commit), CaeaConsultaService, CockpitStatSnapshot, TrxNextGenPersistHelper.

Driver y datasource

Driver mssql-jdbc 12.8.1.jre11 (scope runtime; inerte en tests, que usan H2). La URL JDBC incluye encrypt=true;trustServerCertificate=true en dev y prod:

jdbc:sqlserver://<host>:<port>;databaseName=<db>;encrypt=true;trustServerCertificate=true

Detalle de hosts/credenciales por perfil y variables de entorno en Config y perfiles.

Cache de segundo nivel

No hay 2nd-level cache hoy

El legacy tenía 2nd-level + query cache con ehcache (persistence-prod.xml), pero ninguna entidad migrada lleva @Cache, así que el cache no se usa. La dependencia (hibernate-jcache + ehcache) está comentada en el pom.xml (rotulada SPEC-002): hibernate-ehcache no existe en Hibernate 7 (el 2nd-level pasó a JCache), y habría que reescribir el bloque al abordar SPEC-002. open-in-view está en false, así que tampoco hay sesión abierta en la capa de vista.

Por dónde seguir