Version: 1.0
Fecha: 6 de Abril, 2026
Estado: NORMATIVO - Obligatorio para todos los microservicios Nebula
Arquitecto: Carlos Alberto Torres Camargo
Servicio de referencia: nebula-accounting-core
Centrica utiliza Oracle como motor de base de datos oficial para los servicios Nebula. El modelo anterior de entidades (BusinessBaseEntity<LongId>) presentaba incompatibilidades con Oracle:
@EmbeddedId con wrappers LongId causa que Hibernate siempre incluya la columna ID en el INSERT SQL, incluso cuando el valor es null, provocando errores ORA-01400: cannot insert NULL into ID.GenerationType.IDENTITY.A partir de SimappeCommons 2.9.0, se introduce una nueva jerarquia de entidades que resuelve estos problemas.
AuditableEntity (campos comunes: status, audit, version)
|
SequenceBaseEntity (@Id Long + SEQUENCE — sin multi-tenancy)
|
SequenceBusinessBaseEntity (multi-tenancy: clientId, companyId, subsidiaryId)
|
Entidades de negocio concretas (TipoCentroCostosEntity, CentroCostosEntity, etc.)
| Aspecto | BusinessBaseEntity (legacy) | SequenceBusinessBaseEntity (actual) |
|---|---|---|
| ID primario | @EmbeddedId LongId |
@Id Long (secuencia) |
| Campos tenant | ID (wrapper EntityId) |
Long directo |
| Batch inserts | No soportado | Soportado (SEQUENCE + pooled-lo) |
| Oracle IDENTITY | Incompatible | Compatible (BY DEFAULT) |
| Tipo ID en Repository | LongId |
Long |
| SimappeCommons minimo | cualquier version | 2.9.0+ |
REGLA: Todos los microservicios Nebula DEBEN usar
SequenceBusinessBaseEntity. El modeloBusinessBaseEntity<LongId>queda como legacy para servicios Simappe existentes en PostgreSQL.
@Data
@Entity
@Builder
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode(callSuper = false)
@Table(name = "nombre_tabla", schema = "nombre_schema")
@SequenceGenerator(
name = "simappe_seq_gen",
sequenceName = "nombre_schema.nombre_tabla_seq",
allocationSize = 25
)
public class MiEntidadEntity extends SequenceBusinessBaseEntity {
private static final long serialVersionUID = 1L;
@Column(name = "codigo", nullable = false, length = 10)
private String codigo;
@Column(name = "nombre", nullable = false, length = 100)
private String nombre;
}
SequenceBusinessBaseEntity (no BusinessBaseEntity<LongId>)@SequenceGenerator con:
name = "simappe_seq_gen" (nombre fijo — el framework lo busca por este nombre)sequenceName = "{schema}.{tabla}_seq" (convencion obligatoria)allocationSize = 25 (debe coincidir con el INCREMENT BY de la secuencia en Oracle)@Table(name = "...", schema = "...") siempre con schema explicitoserialVersionUIDid — lo hereda de SequenceBaseEntity@Data
@Entity
@Builder
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode(callSuper = false)
@Table(name = "tipos_centro_costos", schema = "accounting")
@SequenceGenerator(
name = "simappe_seq_gen",
sequenceName = "accounting.tipos_centro_costos_seq",
allocationSize = 25
)
public class TipoCentroCostosEntity extends SequenceBusinessBaseEntity {
private static final long serialVersionUID = -7278783686619531795L;
@Column(name = "codigo", nullable = false, length = 5)
private String codigo;
@Column(name = "nombre", nullable = false, length = 50)
private String nombre;
}
@Repository
public interface MiEntidadRepository extends SimappeRepository<MiEntidadEntity, Long> {
Optional<MiEntidadEntity> findByCodigo(String codigo);
boolean existsByCodigo(String codigo);
@Query("SELECT e FROM MiEntidadEntity e ORDER BY e.codigo ASC")
List<MiEntidadEntity> findAllOrderByCodigo();
}
Tipo de ID:
Long(noLongId). Esto aplica tambien a los LightLoaders:AbstractLoadModels<Long, MiDto, MiEntity>.
Cada tabla que use SequenceBusinessBaseEntity requiere una secuencia Oracle con INCREMENT BY que coincida con allocationSize:
-- Crear la secuencia (obligatorio)
CREATE SEQUENCE {schema}.{nombre_tabla}_seq
START WITH 1000
INCREMENT BY 25
NOCYCLE;
-- Otorgar permisos (obligatorio)
GRANT SELECT ON {schema}.{nombre_tabla}_seq TO PUBLIC;
-- Para tipos_centro_costos
CREATE SEQUENCE accounting.tipos_centro_costos_seq
START WITH 1000
INCREMENT BY 25
NOCYCLE;
GRANT SELECT ON accounting.tipos_centro_costos_seq TO PUBLIC;
-- Para centros_costos
CREATE SEQUENCE accounting.centros_costos_seq
START WITH 1000
INCREMENT BY 25
NOCYCLE;
GRANT SELECT ON accounting.centros_costos_seq TO PUBLIC;
Si la tabla Oracle ya tiene GENERATED ALWAYS AS IDENTITY, debe cambiarse a GENERATED BY DEFAULT AS IDENTITY para permitir inserciones con ID explicito (migraciones, cargas masivas):
ALTER TABLE {schema}.{nombre_tabla} MODIFY (id GENERATED BY DEFAULT AS IDENTITY);
| Elemento | Convencion | Ejemplo |
|---|---|---|
| Tabla | snake_case plural | tipos_centro_costos |
| Schema | nombre del dominio | accounting |
| Secuencia | {schema}.{tabla}_seq |
accounting.tipos_centro_costos_seq |
| INCREMENT BY | Igual a allocationSize |
25 |
| START WITH | 1000 (recomendado) |
Deja espacio para datos seed < 1000 |
La configuracion del EntityManagerFactory debe incluir:
// OBLIGATORIO para Oracle
properties.put("hibernate.dialect", "org.hibernate.dialect.OracleDialect");
// NO setear defaultSchema — cada @Table y @SequenceGenerator define su schema
// Setear "public" aqui causaba que Hibernate antepusiera "public." a las secuencias Oracle
// Batch optimizations (aprovecha SEQUENCE)
properties.put("hibernate.jdbc.batch_size", "25");
properties.put("hibernate.order_inserts", "true");
properties.put("hibernate.order_updates", "true");
properties.put("hibernate.batch_versioned_data", "true");
// DDL gestionado por scripts externos, no por Hibernate
properties.put(SchemaToolingSettings.HBM2DDL_AUTO, "none");
REGLA CRITICA: No usar
hbm2ddl.auto = updateen Oracle. Los scripts DDL deben ejecutarse manualmente o via migraciones (Flyway/Liquibase).
| Error | Causa | Solucion |
|---|---|---|
ORA-01400: cannot insert NULL into ID |
Usando BusinessBaseEntity<LongId> en Oracle |
Migrar a SequenceBusinessBaseEntity |
ORA-02289: sequence does not exist |
No se creo la secuencia en Oracle | Ejecutar DDL: CREATE SEQUENCE {schema}.{tabla}_seq ... |
public.tabla_seq does not exist |
defaultSchema = "public" en JpaConfig |
Eliminar propiedad defaultSchema del EntityManagerFactory |
| IDs duplicados entre instancias | INCREMENT BY de la secuencia != allocationSize |
Ambos deben ser 25 |
| Batch inserts no funcionan | Usando GenerationType.IDENTITY |
Usar SEQUENCE (ya lo hace SequenceBusinessBaseEntity) |
Toda entidad que extienda SequenceBusinessBaseEntity hereda automaticamente estos campos (no es necesario declararlos):
| Campo | Tipo | Columna | Origen | Descripcion |
|---|---|---|---|---|
id |
Long |
id |
SequenceBaseEntity |
ID generado por secuencia |
status |
Boolean |
status |
AuditableEntity |
Estado logico del registro |
createdBy |
String |
created_by |
AuditableEntity |
Usuario que creo el registro |
updatedBy |
String |
updated_by |
AuditableEntity |
Usuario que actualizo el registro |
createdAt |
LocalDateTime |
created_at |
AuditableEntity |
Fecha de creacion |
updatedAt |
LocalDateTime |
updated_at |
AuditableEntity |
Fecha de actualizacion |
version |
Long |
version |
AuditableEntity |
Version para optimistic locking |
clientId |
Long |
client_id |
SequenceBusinessBaseEntity |
Tenant nivel 1 (auto-inyectado desde JWT) |
companyId |
Long |
company_id |
SequenceBusinessBaseEntity |
Tenant nivel 2 (auto-inyectado desde JWT) |
subsidiaryId |
Long |
subsidiary_id |
SequenceBusinessBaseEntity |
Tenant nivel 3 (opcional, auto-inyectado) |
Los campos de tenant (
clientId,companyId,subsidiaryId) se inyectan automaticamente desde el JWT por elSequenceMultitenantEntityListener. No es necesario asignarlos manualmente en los componentes de negocio.
Los campos createdBy y updatedBy deben contener el nombre de usuario, no el token JWT completo. Para obtener el username desde el request:
// CORRECTO
UserSession userSession = SimappeJwt.getInstance()
.getUserSession(request.getHeader(SimappeConstant.AUTHORIZATION));
entity.setCreatedBy(userSession.getUsername());
// INCORRECTO — guarda el token JWT completo
entity.setCreatedBy(request.getHeader(SimappeConstant.AUTHORIZATION));
REGLA: Siempre usar
SimappeJwt.getInstance().getUserSession(...)para extraer el username del token. Nunca guardar el header Authorization directamente.
Al crear una nueva entidad en un microservicio Nebula:
SequenceBusinessBaseEntity@Table(name = "...", schema = "...") con schema explicito@SequenceGenerator(name = "simappe_seq_gen", sequenceName = "{schema}.{tabla}_seq", allocationSize = 25)SimappeRepository<Entity, Long> (no LongId)AbstractLoadModels<Long, Dto, Entity>CREATE SEQUENCE {schema}.{tabla}_seq START WITH 1000 INCREMENT BY 25 NOCYCLEGRANT SELECT ON {schema}.{tabla}_seq TO PUBLICcreatedBy/updatedBy usan userSession.getUsername() (no el header directo)MultiTenantJpaConfig tiene hibernate.dialect = OracleDialect y NO tiene defaultSchema| Version | Fecha | Autor | Descripcion |
|---|---|---|---|
| 1.0.0 | 2026-04-06 | Carlos Torres | Creacion del documento: guia completa de entidades con SequenceBusinessBaseEntity y secuencias Oracle |