GH-806 - Revamp JPA archive support.

We now use two separate entity types for archived and non-archived event publications to rpevent hibernate hickups when inserting an instance of a subtype with he same id.
This commit is contained in:
Oliver Drotbohm
2024-10-24 19:36:39 +02:00
parent 6a85fb3350
commit d302ad1937
4 changed files with 134 additions and 96 deletions

View File

@@ -1,34 +0,0 @@
package org.springframework.modulith.events.jpa;
import jakarta.persistence.Entity;
import jakarta.persistence.Table;
import java.time.Instant;
import java.util.UUID;
/**
* JPA entity to represent archived event publications.
*
* @author Oliver Drotbohm
*/
@Entity
@Table(name = "EVENT_PUBLICATION_ARCHIVE")
class ArchivedJpaEventPublication extends JpaEventPublication {
/**
* Creates a new {@link ArchivedJpaEventPublication} for the given publication date, listener id, serialized event and
* event type.
*
* @param id
* @param publicationDate must not be {@literal null}.
* @param listenerId must not be {@literal null} or empty.
* @param serializedEvent must not be {@literal null} or empty.
* @param eventType must not be {@literal null}.
*/
public ArchivedJpaEventPublication(UUID id, Instant publicationDate, String listenerId, String serializedEvent,
Class<?> eventType) {
super(id, publicationDate, listenerId, serializedEvent, eventType);
}
public ArchivedJpaEventPublication() {}
}

View File

@@ -15,11 +15,17 @@
*/
package org.springframework.modulith.events.jpa;
import jakarta.persistence.*;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.MappedSuperclass;
import jakarta.persistence.Table;
import java.time.Instant;
import java.util.Objects;
import java.util.UUID;
import org.springframework.modulith.events.support.CompletionMode;
import org.springframework.util.Assert;
/**
@@ -28,11 +34,10 @@ import org.springframework.util.Assert;
* @author Oliver Drotbohm
* @author Dmitry Belyaev
* @author Björn Kieling
* @author Cora Iberkleid
*/
@Entity
@Table(name = "EVENT_PUBLICATION")
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
class JpaEventPublication {
@MappedSuperclass
abstract class JpaEventPublication {
final @Id @Column(length = 16) UUID id;
final Instant publicationDate;
@@ -51,7 +56,8 @@ class JpaEventPublication {
* @param serializedEvent must not be {@literal null} or empty.
* @param eventType must not be {@literal null}.
*/
JpaEventPublication(UUID id, Instant publicationDate, String listenerId, String serializedEvent, Class<?> eventType) {
private JpaEventPublication(UUID id, Instant publicationDate, String listenerId, String serializedEvent,
Class<?> eventType) {
Assert.notNull(id, "Identifier must not be null!");
Assert.notNull(publicationDate, "Publication date must not be null!");
@@ -75,6 +81,19 @@ class JpaEventPublication {
this.eventType = null;
}
static JpaEventPublication of(UUID id, Instant publicationDate, String listenerId, String serializedEvent,
Class<?> eventType) {
return new DefaultJpaEventPublication(id, publicationDate, listenerId, serializedEvent, eventType);
}
static Class<? extends JpaEventPublication> getIncompleteType() {
return DefaultJpaEventPublication.class;
}
static Class<? extends JpaEventPublication> getCompletedType(CompletionMode mode) {
return mode == CompletionMode.ARCHIVE ? ArchivedJpaEventPublication.class : DefaultJpaEventPublication.class;
}
ArchivedJpaEventPublication archive(Instant instant) {
var result = new ArchivedJpaEventPublication(id, publicationDate, listenerId, serializedEvent, eventType);
@@ -82,4 +101,57 @@ class JpaEventPublication {
return result;
}
/*
* (non-Javadoc)
* @see java.lang.Object#equals(java.lang.Object)
*/
@Override
public boolean equals(Object obj) {
if (obj == this) {
return true;
}
if (!(obj instanceof JpaEventPublication that)) {
return false;
}
return Objects.equals(this.id, that.id);
}
/*
* (non-Javadoc)
* @see java.lang.Object#hashCode()
*/
@Override
public int hashCode() {
return id.hashCode();
}
@Entity(name = "DefaultJpaEventPublication")
@Table(name = "EVENT_PUBLICATION")
private static class DefaultJpaEventPublication extends JpaEventPublication {
private DefaultJpaEventPublication(UUID id, Instant publicationDate, String listenerId, String serializedEvent,
Class<?> eventType) {
super(id, publicationDate, listenerId, serializedEvent, eventType);
}
@SuppressWarnings("unused")
DefaultJpaEventPublication() {}
}
@Entity(name = "ArchivedJpaEventPublication")
@Table(name = "EVENT_PUBLICATION_ARCHIVE")
private static class ArchivedJpaEventPublication extends JpaEventPublication {
private ArchivedJpaEventPublication(UUID id, Instant publicationDate, String listenerId, String serializedEvent,
Class<?> eventType) {
super(id, publicationDate, listenerId, serializedEvent, eventType);
}
@SuppressWarnings("unused")
ArchivedJpaEventPublication() {}
}
}

View File

@@ -29,6 +29,7 @@ import org.springframework.modulith.events.core.EventSerializer;
import org.springframework.modulith.events.core.PublicationTargetIdentifier;
import org.springframework.modulith.events.core.TargetEventPublication;
import org.springframework.modulith.events.support.CompletionMode;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.Assert;
@@ -41,11 +42,12 @@ import org.springframework.util.Assert;
* @author Cora Iberkleid
*/
@Transactional
@Repository
class JpaEventPublicationRepository implements EventPublicationRepository {
private static String BY_EVENT_AND_LISTENER_ID = """
select p
from JpaEventPublication p
from DefaultJpaEventPublication p
where
p.serializedEvent = ?1
and p.listenerId = ?2
@@ -63,7 +65,7 @@ class JpaEventPublicationRepository implements EventPublicationRepository {
private static String INCOMPLETE = """
select p
from JpaEventPublication p
from DefaultJpaEventPublication p
where
p.completionDate is null
order by
@@ -72,7 +74,7 @@ class JpaEventPublicationRepository implements EventPublicationRepository {
private static String INCOMPLETE_BEFORE = """
select p
from JpaEventPublication p
from DefaultJpaEventPublication p
where
p.completionDate is null
and p.publicationDate < ?1
@@ -81,7 +83,7 @@ class JpaEventPublicationRepository implements EventPublicationRepository {
""";
private static final String MARK_COMPLETED_BY_EVENT_AND_LISTENER_ID = """
update JpaEventPublication p
update DefaultJpaEventPublication p
set p.completionDate = ?3
where p.serializedEvent = ?1
and p.listenerId = ?2
@@ -89,26 +91,26 @@ class JpaEventPublicationRepository implements EventPublicationRepository {
""";
private static final String MARK_COMPLETED_BY_ID = """
update JpaEventPublication p
update DefaultJpaEventPublication p
set p.completionDate = ?2
where p.id = ?1
""";
private static final String DELETE = """
delete
from JpaEventPublication p
from DefaultJpaEventPublication p
where p.id in ?1
""";
private static final String DELETE_BY_EVENT_AND_LISTENER_ID = """
delete JpaEventPublication p
delete DefaultJpaEventPublication p
where p.serializedEvent = ?1
and p.listenerId = ?2
""";
private static final String DELETE_BY_ID = """
delete
from JpaEventPublication p
from DefaultJpaEventPublication p
where p.id = ?1
""";
@@ -152,14 +154,12 @@ class JpaEventPublicationRepository implements EventPublicationRepository {
this.serializer = serializer;
this.completionMode = completionMode;
var archiveEntityName = completionMode == CompletionMode.ARCHIVE
? ArchivedJpaEventPublication.class.getSimpleName()
: JpaEventPublication.class.getSimpleName();
var archiveEntityName = getCompletedEntityType().getSimpleName();
this.getCompleted = COMPLETE.formatted(archiveEntityName);
this.deleteCompleted = DELETE_COMPLETED.formatted(archiveEntityName);
this.deleteCompleted = DELETE_COMPLETED.formatted(archiveEntityName);
this.deleteCompletedBefore = DELETE_COMPLETED_BEFORE.formatted(archiveEntityName);
}
}
/*
* (non-Javadoc)
@@ -192,15 +192,13 @@ class JpaEventPublicationRepository implements EventPublicationRepository {
} else if (completionMode == CompletionMode.ARCHIVE) {
var publication = entityManager.createQuery(BY_EVENT_AND_LISTENER_ID, JpaEventPublication.class)
var publication = entityManager.createQuery(BY_EVENT_AND_LISTENER_ID, JpaEventPublication.getIncompleteType())
.setParameter(1, serializedEvent)
.setParameter(2, identifierValue)
.getSingleResult();
var archived = publication.archive(completionDate);
entityManager.remove(publication);
entityManager.persist(archived);
entityManager.persist(publication.archive(completionDate));
} else {
@@ -227,12 +225,10 @@ class JpaEventPublicationRepository implements EventPublicationRepository {
} else if (completionMode == CompletionMode.ARCHIVE) {
var publication = entityManager.find(JpaEventPublication.class, identifier);
var archived = publication.archive(completionDate);
var publication = entityManager.find(JpaEventPublication.getIncompleteType(), identifier);
entityManager.remove(publication);
entityManager.persist(archived);
entityManager.persist(publication.archive(completionDate));
} else {
@@ -251,7 +247,7 @@ class JpaEventPublicationRepository implements EventPublicationRepository {
@Transactional(readOnly = true)
public List<TargetEventPublication> findIncompletePublications() {
return entityManager.createQuery(INCOMPLETE, JpaEventPublication.class)
return entityManager.createQuery(INCOMPLETE, JpaEventPublication.getIncompleteType())
.getResultStream()
.map(this::entityToDomain)
.toList();
@@ -265,7 +261,7 @@ class JpaEventPublicationRepository implements EventPublicationRepository {
@Transactional(readOnly = true)
public List<TargetEventPublication> findIncompletePublicationsPublishedBefore(Instant instant) {
return entityManager.createQuery(INCOMPLETE_BEFORE, JpaEventPublication.class)
return entityManager.createQuery(INCOMPLETE_BEFORE, JpaEventPublication.getIncompleteType())
.setParameter(1, instant)
.getResultStream()
.map(this::entityToDomain)
@@ -292,9 +288,7 @@ class JpaEventPublicationRepository implements EventPublicationRepository {
@Override
public List<TargetEventPublication> findCompletedPublications() {
var type = completionMode == CompletionMode.ARCHIVE
? ArchivedJpaEventPublication.class
: JpaEventPublication.class;
var type = getCompletedEntityType();
return entityManager.createQuery(getCompleted, type)
.getResultList()
@@ -338,12 +332,22 @@ class JpaEventPublicationRepository implements EventPublicationRepository {
.executeUpdate();
}
private Optional<JpaEventPublication> findEntityBySerializedEventAndListenerIdAndCompletionDateNull( //
/**
* Returns the type representing completed event publications.
*
* @return will never be {@literal null}.
* @since 1.3
*/
public Class<? extends JpaEventPublication> getCompletedEntityType() {
return JpaEventPublication.getCompletedType(completionMode);
}
private Optional<? extends JpaEventPublication> findEntityBySerializedEventAndListenerIdAndCompletionDateNull( //
Object event, PublicationTargetIdentifier listenerId) {
var serializedEvent = serializeEvent(event);
var query = entityManager.createQuery(BY_EVENT_AND_LISTENER_ID, JpaEventPublication.class)
var query = entityManager.createQuery(BY_EVENT_AND_LISTENER_ID, JpaEventPublication.getIncompleteType())
.setParameter(1, serializedEvent)
.setParameter(2, listenerId.getValue());
@@ -355,9 +359,11 @@ class JpaEventPublicationRepository implements EventPublicationRepository {
}
private JpaEventPublication domainToEntity(TargetEventPublication domain) {
return new JpaEventPublication(domain.getIdentifier(), domain.getPublicationDate(),
domain.getTargetIdentifier().getValue(),
serializeEvent(domain.getEvent()), domain.getEvent().getClass());
var event = domain.getEvent();
return JpaEventPublication.of(domain.getIdentifier(), domain.getPublicationDate(),
domain.getTargetIdentifier().getValue(), serializeEvent(event), event.getClass());
}
private TargetEventPublication entityToDomain(JpaEventPublication entity) {

View File

@@ -164,7 +164,8 @@ class JpaEventPublicationRepositoryIntegrationTests {
when(eventSerializer.serialize(testEvent)).thenReturn(serializedEvent);
assertThat(repository.findIncompletePublicationsByEventAndTargetIdentifier(testEvent, TARGET_IDENTIFIER)).isEmpty();
assertThat(repository.findIncompletePublicationsByEventAndTargetIdentifier(testEvent, TARGET_IDENTIFIER))
.isEmpty();
}
@Test // GH-25
@@ -204,13 +205,11 @@ class JpaEventPublicationRepositoryIntegrationTests {
repository.markCompleted(testEvent1, TARGET_IDENTIFIER, Instant.now());
repository.deleteCompletedPublications();
assertThat(em.createQuery("select p from JpaEventPublication p", JpaEventPublication.class).getResultList())
.hasSize(1) //
assertThat(getIncompletePublications()).hasSize(1) //
.element(0).extracting(it -> it.serializedEvent).isEqualTo(serializedEvent2);
if (completionMode == CompletionMode.ARCHIVE) {
assertThat(em.createQuery("select p from ArchivedJpaEventPublication p", ArchivedJpaEventPublication.class).getResultList())
.hasSize(0);
assertThat(getArchivedPublications()).hasSize(0);
}
}
@@ -252,15 +251,9 @@ class JpaEventPublicationRepositoryIntegrationTests {
repository.deleteCompletedPublicationsBefore(now.minusSeconds(15));
var entityName = completionMode == CompletionMode.ARCHIVE
? ArchivedJpaEventPublication.class.getSimpleName()
: JpaEventPublication.class.getSimpleName();
var type = repository.getCompletedEntityType();
var type = completionMode == CompletionMode.ARCHIVE
? ArchivedJpaEventPublication.class
: JpaEventPublication.class;
assertThat(em.createQuery("select p from " + entityName + " p", type).getResultList())
assertThat(em.createQuery("select p from " + type.getSimpleName() + " p", type).getResultList())
.hasSize(1) //
.element(0).extracting(it -> it.serializedEvent).isEqualTo(serializedEvent2);
@@ -333,11 +326,8 @@ class JpaEventPublicationRepositoryIntegrationTests {
repository.markCompleted(publication, Instant.now());
assertThat(repository.findCompletedPublications())
.hasSize(1);
assertThat(em.createQuery("select p from JpaEventPublication p", JpaEventPublication.class).getResultList())
.hasSize(0);
assertThat(repository.findCompletedPublications()).hasSize(1);
assertThat(getIncompletePublications()).hasSize(0);
}
@Test // GH 806
@@ -350,11 +340,16 @@ class JpaEventPublicationRepositoryIntegrationTests {
repository.markCompleted(publication.getIdentifier(), Instant.now());
assertThat(repository.findCompletedPublications())
.hasSize(1);
assertThat(repository.findCompletedPublications()).hasSize(1);
assertThat(getIncompletePublications()).hasSize(0);
}
assertThat(em.createQuery("select p from JpaEventPublication p", JpaEventPublication.class).getResultList())
.hasSize(0);
private List<JpaEventPublication> getIncompletePublications() {
return em.createQuery("select p from DefaultJpaEventPublication p", JpaEventPublication.class).getResultList();
}
private List<JpaEventPublication> getArchivedPublications() {
return em.createQuery("select p from ArchivedJpaEventPublication p", JpaEventPublication.class).getResultList();
}
private TargetEventPublication createPublication(Object event) {
@@ -368,11 +363,10 @@ class JpaEventPublicationRepositoryIntegrationTests {
}
private void savePublicationAt(LocalDateTime date) {
em.persist(new JpaEventPublication(UUID.randomUUID(), date.toInstant(ZoneOffset.UTC), "", "", Object.class));
em.persist(JpaEventPublication.of(UUID.randomUUID(), date.toInstant(ZoneOffset.UTC), "", "", Object.class));
}
private record TestEvent(String eventId) {
}
private record TestEvent(String eventId) {}
}
@Nested