
Documentacion de Tentacolous
Guia tecnica¶
Todo lo necesario para instalar, configurar y operar Tentacolous en un proyecto Spring Boot.
Aprende como agregar la dependencia, configurar el procesamiento de eventos y crear listeners Java que reaccionan a cambios reales en PostgreSQL mediante triggers, una tabla de eventos y un poller Spring.
Resumen¶
Tentacolous es una libreria Spring Boot que ejecuta metodos Java cuando una tabla de base de datos recibe un INSERT, UPDATE o DELETE.
La diferencia importante es que Tentacolous reacciona a los cambios de la base de datos sin importar donde se originen.
La version 0.1.8 se enfoca en filtros programaticos reutilizables. Un filtro puede revisar la entidad actual, la entidad anterior durante un update y la operacion antes de decidir si el listener debe ejecutarse. Funciona con la nueva anotacion generica @TentacolousListener y con las anotaciones existentes @UponInserting, @UponUpdating y @UponDeleting.
1. Crea una tabla de eventos.
2. Crea una funcion PostgreSQL.
3. Crea triggers para las tablas que tienen listeners.
4. Los triggers escriben eventos en db_change_event.
5. Un poller Spring lee esos eventos.
6. Tentacolous convierte el payload JSON en tu entidad Java.
7. Tentacolous ejecuta el metodo anotado.
Requisitos¶
- Java 17 o superior.
- Spring Boot.
- Una aplicacion Spring Boot con un
DataSourceconfigurado. - PostgreSQL para la creacion automatica de triggers.
La creacion automatica de infraestructura de base de datos esta implementada actualmente para PostgreSQL.
Snippets de dependencias¶
Gradle¶
Maven¶
<dependency>
<groupId>io.github.aimtone</groupId>
<artifactId>tentacolous</artifactId>
<version>0.1.8</version>
</dependency>
Migracion desde 0.1.7¶
La version 0.1.8 mantiene compatibilidad con los listeners existentes. No necesitas reemplazar @UponInserting, @UponUpdating ni @UponDeleting, y esta version no requiere cambios en la infraestructura de base de datos.
- Usa
@TentacolousListenersolo si prefieres la anotacion generica conaction. - Los filtros personalizados son beans opcionales de Spring que extienden
TentacolousFilter<T>. - El tipo generico del filtro debe ser compatible con la entidad del listener.
- Si un filtro personalizado se declara junto con
field,valueTypeovalue, el filtro personalizado tiene prioridad y Tentacolous escribe un warning. - Los filtros declarativos deben definir
field,valueTypeyvaluejuntos. - Cada metodo puede declarar una anotacion listener por operacion.
Configuracion de la aplicacion¶
Tentacolous necesita que tu aplicacion tenga una conexion de base de datos. Ejemplo de application.yml:
tentacolous:
enabled: true
schema-management: auto
event-table: db_change_event
poll-interval: 1s
initial-delay: 0s
batch-size: 100
max-attempts: 3
spring:
datasource:
url: jdbc:postgresql://localhost:5432/mydb
username: postgres
password: postgres
driver-class-name: org.postgresql.Driver
Propiedades disponibles¶
| Propiedad | Valor por defecto | Que hace |
|---|---|---|
tentacolous.enabled |
true |
Activa o desactiva Tentacolous. |
tentacolous.schema-management |
auto |
Define si Tentacolous crea, valida o ignora la infraestructura de base de datos. |
tentacolous.event-table |
db_change_event |
Nombre de la tabla de eventos. |
tentacolous.poll-interval |
1s |
Frecuencia con que se leen eventos pendientes. |
tentacolous.initial-delay |
0s |
Demora antes de iniciar el poller. |
tentacolous.batch-size |
100 |
Numero maximo de eventos leidos por ciclo. |
tentacolous.max-attempts |
3 |
Limite de reintentos antes de marcar un evento como FAILED. |
Modos de schema management¶
| Modo | Uso comun | Comportamiento |
|---|---|---|
auto |
Desarrollo | Crea la tabla, funcion y triggers si hace falta. |
create |
Ambientes de desarrollo controlados | Fuerza la creacion de infraestructura soportada. |
validate |
Produccion | Valida que la infraestructura exista sin crearla. |
none |
Produccion con migraciones | No crea ni valida infraestructura. |
Para comenzar, usa schema-management: auto. En produccion, validate o none suele ser mejor porque crear triggers requiere permisos elevados en la base de datos.
Entidad de ejemplo¶
Tentacolous usa la entidad para saber que tabla de base de datos debe escuchar.
@Entity
@Table(name = "person")
public class Person {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String lastname;
private String email;
public Person() {
}
// getters and setters
}
Si la entidad tiene @Table(name = "person"), Tentacolous escucha la tabla person. Si no tiene @Table, Tentacolous infiere el nombre de tabla desde el nombre de clase.
| Entidad | Tabla inferida |
|---|---|
Person |
person |
UserAccount |
user_account |
PaymentTransaction |
payment_transaction |
Por eso las anotaciones no tienen un parametro table: la entidad seleccionada ya representa la tabla.
Listeners y operaciones¶
Comienza con las anotaciones especificas por operacion. Siguen completamente disponibles en 0.1.8 y ahora tambien aceptan filtros personalizados. El listener generico se explica despues como una alternativa opcional.
UponInserting¶
@UponInserting ejecuta un metodo cuando ocurre un INSERT en la tabla de la entidad.
@UponInserting(entity = Person.class)
public void onPersonInserted(Person person) {
System.out.println("Inserted: " + person.getEmail());
}
Casos de uso: ejecutar logica despues de crear un registro, enviar una notificacion, iniciar un flujo o crear un registro de auditoria.
Insert con nombre logico explicito¶
@UponInserting(entity = Person.class, entityName = "Person")
public void onPersonInserted(Person person) {
}
Insert con filtro STRING¶
@UponInserting(
entity = Person.class,
field = "email",
valueType = ValueType.STRING,
value = "admin@example.com"
)
public void onAdminInserted(Person person) {
}
Insert con filtro BOOLEAN¶
@UponInserting(
entity = User.class,
field = "active",
valueType = ValueType.BOOLEAN,
value = "true"
)
public void onActiveUserInserted(User user) {
}
Insert con filtro numerico¶
@UponInserting(
entity = User.class,
field = "level",
valueType = ValueType.LONG,
value = "1"
)
public void onLevelOneUserInserted(User user) {
}
Insert con filtro de fecha¶
@UponInserting(
entity = Payment.class,
field = "paymentDate",
valueType = ValueType.DATE,
value = "2026-07-07"
)
public void onPaymentDate(Payment payment) {
}
Insert con columnas excluidas¶
@UponInserting(
entity = User.class,
exclude = {"password", "token"}
)
public void onUserInserted(User user) {
}
UponUpdating¶
@UponUpdating ejecuta un metodo cuando ocurre un UPDATE en la tabla de la entidad.
@UponUpdating(entity = Person.class)
public void onPersonUpdated(Person person) {
System.out.println("Updated: " + person.getEmail());
}
Tambien puedes recibir los valores anteriores del registro agregando un segundo parametro del mismo tipo de entidad.
@UponUpdating(entity = Person.class)
public void onPersonUpdated(Person newPerson, Person oldPerson) {
}
Si agregas un tercer parametro, Tentacolous entrega los snapshots anteriores de ese mismo registro, ordenados desde el mas antiguo al mas reciente.
@UponUpdating(entity = Person.class)
public void onPersonUpdated(Person newPerson, Person oldPerson, List<Person> history) {
}
El parametro de historico puede ser List<Person> o Person[]. Tentacolous relaciona el registro por su columna @Id cuando existe, o por id en caso contrario.
Casos de uso: recalcular datos derivados, invalidar cache, notificar a otros sistemas o sincronizar con un servicio externo.
Update con filtro STRING¶
@UponUpdating(
entity = Payment.class,
field = "status",
valueType = ValueType.STRING,
value = "APPROVED"
)
public void onPaymentApproved(Payment payment) {
}
Update con filtro BOOLEAN¶
@UponUpdating(
entity = User.class,
field = "enabled",
valueType = ValueType.BOOLEAN,
value = "false"
)
public void onUserDisabled(User user) {
}
Update con filtro DATETIME¶
@UponUpdating(
entity = Invoice.class,
field = "paidAt",
valueType = ValueType.DATETIME,
value = "2026-07-07T13:45:00Z"
)
public void onInvoicePaidAt(Invoice invoice) {
}
Update con columnas excluidas¶
@UponUpdating(
entity = User.class,
exclude = {"password", "refresh_token"}
)
public void onUserUpdated(User user) {
}
Las columnas excluidas se eliminan tanto del payload nuevo como del payload anterior.
UponDeleting¶
@UponDeleting ejecuta un metodo cuando ocurre un DELETE en la tabla de la entidad. Para un delete, el payload contiene los valores previos del registro porque la fila ya no existe despues de eliminarse.
@UponDeleting(entity = Person.class)
public void onPersonDeleted(Person person) {
System.out.println("Deleted: " + person.getEmail());
}
Tambien puedes recibir los snapshots anteriores de ese mismo registro agregando un segundo parametro.
@UponDeleting(entity = Person.class)
public void onPersonDeleted(Person person, List<Person> history) {
}
El parametro de historico puede ser List<Person> o Person[].
Casos de uso: limpiar recursos externos, eliminar datos relacionados en otra base de datos, notificar eliminaciones o registrar auditoria de borrado.
Delete con filtro STRING¶
@UponDeleting(
entity = Person.class,
field = "email",
valueType = ValueType.STRING,
value = "admin@example.com"
)
public void onAdminDeleted(Person person) {
}
Delete con filtro UUID¶
@UponDeleting(
entity = Session.class,
field = "externalId",
valueType = ValueType.UUID,
value = "550e8400-e29b-41d4-a716-446655440000"
)
public void onSessionDeleted(Session session) {
}
Delete con columnas excluidas¶
@UponDeleting(
entity = User.class,
exclude = {"password", "token"}
)
public void onUserDeleted(User user) {
}
Alternativa generica: TentacolousListener¶
@TentacolousListener es la alternativa generica. Su parametro action determina la operacion y la firma valida del metodo.
@TentacolousListener(entity = Person.class, action = ActionListener.INSERT)
public void onPersonInserted(Person person) {
}
@TentacolousListener(entity = Person.class, action = ActionListener.UPDATE)
public void onPersonUpdated(Person newPerson, Person oldPerson, List<Person> history) {
}
@TentacolousListener(entity = Person.class, action = ActionListener.DELETE)
public void onPersonDeleted(Person person, List<Person> history) {
}
Las reglas de parametros son identicas a las anotaciones especificas correspondientes.
Parametros de anotaciones¶
Las anotaciones especificas y @TentacolousListener(...) comparten la misma configuracion de listener.
| Parametro | Requerido | Descripcion |
|---|---|---|
entity |
Si | Clase de entidad que representa la tabla y recibe el payload deserializado. |
action |
Solo para @TentacolousListener |
ActionListener.INSERT, ActionListener.UPDATE o ActionListener.DELETE. |
entityName |
No | Nombre logico del evento. Si se omite, Tentacolous usa el nombre simple de la clase. |
field |
Solo con filtros | Campo del payload que se quiere comparar. |
valueType |
Solo con filtros | Tipo usado para interpretar value. |
value |
Solo con filtros | Valor esperado, siempre escrito como texto. |
filter |
No | Bean de Spring que extiende TentacolousFilter<T>. Reemplaza el filtro declarativo. |
order |
No | Orden de ejecucion del listener. Los valores menores se ejecutan primero; el valor por defecto es 0. |
exclude |
No | Columnas que no deben almacenarse en el payload del evento. |
Orden de listeners¶
Usa order cuando varios listeners procesan la misma entidad y operacion. El orden funciona entre anotaciones especificas y genericas.
@UponUpdating(entity = Person.class, order = 10)
public void updateProfile(Person person) {
}
@TentacolousListener(
entity = Person.class,
action = ActionListener.UPDATE,
order = 20
)
public void registerAudit(Person person) {
}
Si un listener falla, los siguientes no se ejecutan y el evento sigue el flujo normal de reintentos. Los efectos de los listeners deben ser idempotentes.
Sobre entityName¶
La mayoria de las veces no necesitas entityName. Por defecto, @UponInserting(entity = Person.class) usa Person. Ese valor se almacena en db_change_event.entity_name y se usa internamente para relacionar eventos con listeners.
Usa entityName solo en casos avanzados donde necesites un nombre logico estable, por ejemplo cuando infraestructura externa ya escribe eventos con un nombre especifico.
Sobre exclude¶
exclude no filtra listeners. Evita que columnas especificas se almacenen en el payload JSON del evento.
@UponInserting(
entity = User.class,
exclude = {"password", "token", "secret_key"}
)
public void onUserInserted(User user) {
}
Esto importa porque la tabla de eventos puede contener datos de negocio. Normalmente no quieres guardar secretos ahi.
Tipos de filtros¶
Los filtros siempre usan esta estructura:
field, valueType y value deben declararse juntos. Si falta uno, Tentacolous falla durante el escaneo de listeners y muestra un ejemplo de la forma valida.
| ValueType | Formato de value | Ejemplo |
|---|---|---|
STRING |
Texto exacto | "APPROVED" |
BOOLEAN |
true or false |
"true" |
NUMBER |
Numero entero largo | "1" |
INTEGER |
Numero entero | "7" |
LONG |
Numero entero largo | "999" |
DECIMAL |
Decimal exacto | "10.50" |
DOUBLE |
Decimal de punto flotante | "3.14" |
DATE |
Fecha ISO | "2026-07-07" |
TIME |
Hora ISO | "13:45:00" |
DATETIME |
Instant o datetime ISO | "2026-07-07T13:45:00Z" |
UUID |
UUID canonico | "550e8400-e29b-41d4-a716-446655440000" |
Filtros personalizados¶
El filtro programatico es la novedad principal de 0.1.8. Define la regla una vez en una clase reutilizable y usala desde cualquier anotacion de listener. La clase del filtro debe ser un bean de Spring.
@Component
public class ActivePersonFilter extends TentacolousFilter<Person> {
@Override
public boolean accept(TentacolousFilterContext<Person> context) {
Person person = context.getEntity();
Person oldPerson = context.getOldEntity();
return person.isActive()
&& (oldPerson == null
|| !Objects.equals(person.getStatus(), oldPerson.getStatus()));
}
}
@TentacolousListener(
entity = Person.class,
action = ActionListener.UPDATE,
filter = ActivePersonFilter.class
)
public void onActivePersonUpdated(Person newPerson, Person oldPerson) {
}
Disponible en todas las anotaciones¶
@TentacolousListener es opcional. Las anotaciones anteriores siguen disponibles y ahora aceptan el mismo filtro personalizado:
@UponInserting(entity = Person.class, filter = ActivePersonFilter.class)
public void onPersonInserted(Person person) {
}
@UponUpdating(entity = Person.class, filter = ActivePersonFilter.class)
public void onPersonUpdated(Person newPerson, Person oldPerson) {
}
@UponDeleting(entity = Person.class, filter = ActivePersonFilter.class)
public void onPersonDeleted(Person person) {
}
| Anotacion | Operacion | Filtro personalizado |
|---|---|---|
@UponInserting |
INSERT | Disponible |
@UponUpdating |
UPDATE | Disponible |
@UponDeleting |
DELETE | Disponible |
@TentacolousListener |
Seleccionada con action |
Disponible |
getEntity() contiene el payload actual. getOldEntity() esta disponible en updates y es null en inserts y deletes. El contexto tambien ofrece getOperation(), getEventId(), getEntityName() y getRecordKey().
En updates, getChangedFields() entrega los nombres de campos JSON modificados y hasChanged("status") consulta un campo. En inserts y deletes devuelve un conjunto vacio y false.
Cuando se configura filter junto con field, valueType o value, Tentacolous escribe un warning. El filtro personalizado tiene prioridad y el filtro declarativo no se ejecuta. Los errores del filtro siguen el flujo normal de reintentos.
Combinaciones invalidas¶
Estas combinaciones deberian fallar cuando inicia la aplicacion.
Falta field¶
@UponInserting(
entity = Person.class,
valueType = ValueType.BOOLEAN,
value = "true"
)
public void invalid(Person person) {
}
Falta value¶
@UponInserting(
entity = Person.class,
field = "active",
valueType = ValueType.BOOLEAN
)
public void invalid(Person person) {
}
Falta valueType¶
@UponUpdating(
entity = Person.class,
field = "name",
value = "Anthony"
)
public void invalid(Person person) {
}
Usa el filtro declarativo completo:
@UponUpdating(
entity = Person.class,
field = "name",
valueType = ValueType.STRING,
value = "Anthony"
)
public void valid(Person person) {
}
Misma operacion declarada dos veces¶
@UponUpdating(entity = Person.class)
@TentacolousListener(entity = Person.class, action = ActionListener.UPDATE)
public void invalid(Person person) {
}
Un metodo puede escuchar operaciones distintas, pero no la misma operacion dos veces.
Metodo con mas de un parametro¶
Parametro incompatible¶
Pruebas manuales¶
Asume esta entidad y este listener:
@Entity
@Table(name = "person")
public class Person {
// ...
}
@UponInserting(entity = Person.class)
public void inserted(Person person) {
System.out.println("Person inserted: " + person.getEmail());
}
Inicia tu aplicacion Spring Boot. Tentacolous deberia imprimir logs similares a:
Tentacolous registered 1 listener method(s)
Initializing Tentacolous schema using event table 'db_change_event'
Creating Tentacolous INSERT trigger for table 'person' and entity 'Person'
Starting Tentacolous poller
Luego ejecuta este SQL en PostgreSQL:
Deberias ver esto en la consola de tu aplicacion:
Inspecciona eventos y triggers con:
select *
from db_change_event
order by id desc;
select trigger_name, event_object_schema, event_object_table
from information_schema.triggers
where event_object_table = 'person';
Como funciona internamente¶
Tentacolous no usa eventos JPA. Esto es intencional: los eventos JPA solo funcionan cuando el cambio pasa por la misma aplicacion. Tentacolous esta disenado para detectar cambios reales de base de datos, incluso cuando vienen desde fuera de tu aplicacion.
En PostgreSQL, Tentacolous crea:
- Una tabla
db_change_event. - Una funcion
db_change_event_notify_change(). - Un trigger por tabla y operacion.
El trigger almacena entity_name, operation, payload, old_payload, record_key, status, attempts, last_error y timestamps de procesamiento.
payload contiene la fila actual para inserts y updates, y la fila eliminada para deletes. old_payload contiene la fila anterior solo para updates. record_key identifica el registro afectado, usando la columna @Id de la entidad cuando existe o id en caso contrario.
Cuando un listener pide historico, Tentacolous consulta los payloads anteriores de INSERT y UPDATE para el mismo entity_name y record_key, ordenados desde el mas antiguo al mas reciente.
El poller busca eventos PENDING, los marca como PROCESSING, ejecuta el listener y finalmente los marca como PROCESSED. Si ocurre un error, Tentacolous guarda last_error y reintenta hasta alcanzar tentacolous.max-attempts.
Seguridad¶
- No guardes secretos en el payload.
- Usa
excludepara columnas sensibles. - Protege
db_change_eventcon permisos adecuados de base de datos. - En produccion, usa
schema-management=validateoschema-management=none. - Manten los listeners idempotentes porque pueden ocurrir reintentos.
Produccion¶
- Crea la tabla, funcion y triggers usando migraciones controladas.
- Usa
tentacolous.schema-management=validatepara verificar la infraestructura. - Si administras el schema manualmente, incluye las columnas
old_payloadyrecord_key, y pasa el campo de clave del registro a la funcion del trigger. - Monitorea eventos
FAILED. - Limpia o archiva
db_change_event. - Evita logica lenta dentro de los listeners.
- Publica a una cola si el proceso es pesado.
Ejecutar tests de Tentacolous¶
Desde la carpeta de la libreria:
Resultado esperado: