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:
@@ -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() {}
|
||||
}
|
||||
@@ -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() {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user