From d302ad19370b8b93971f9e99cb322c7a347deda7 Mon Sep 17 00:00:00 2001 From: Oliver Drotbohm Date: Thu, 24 Oct 2024 19:36:39 +0200 Subject: [PATCH] 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. --- .../jpa/ArchivedJpaEventPublication.java | 34 -------- .../events/jpa/JpaEventPublication.java | 84 +++++++++++++++++-- .../jpa/JpaEventPublicationRepository.java | 68 ++++++++------- ...PublicationRepositoryIntegrationTests.java | 44 +++++----- 4 files changed, 134 insertions(+), 96 deletions(-) delete mode 100644 spring-modulith-events/spring-modulith-events-jpa/src/main/java/org/springframework/modulith/events/jpa/ArchivedJpaEventPublication.java diff --git a/spring-modulith-events/spring-modulith-events-jpa/src/main/java/org/springframework/modulith/events/jpa/ArchivedJpaEventPublication.java b/spring-modulith-events/spring-modulith-events-jpa/src/main/java/org/springframework/modulith/events/jpa/ArchivedJpaEventPublication.java deleted file mode 100644 index 8c22a2cf..00000000 --- a/spring-modulith-events/spring-modulith-events-jpa/src/main/java/org/springframework/modulith/events/jpa/ArchivedJpaEventPublication.java +++ /dev/null @@ -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() {} -} diff --git a/spring-modulith-events/spring-modulith-events-jpa/src/main/java/org/springframework/modulith/events/jpa/JpaEventPublication.java b/spring-modulith-events/spring-modulith-events-jpa/src/main/java/org/springframework/modulith/events/jpa/JpaEventPublication.java index 71b85b71..f4fce066 100644 --- a/spring-modulith-events/spring-modulith-events-jpa/src/main/java/org/springframework/modulith/events/jpa/JpaEventPublication.java +++ b/spring-modulith-events/spring-modulith-events-jpa/src/main/java/org/springframework/modulith/events/jpa/JpaEventPublication.java @@ -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 getIncompleteType() { + return DefaultJpaEventPublication.class; + } + + static Class 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() {} + } } diff --git a/spring-modulith-events/spring-modulith-events-jpa/src/main/java/org/springframework/modulith/events/jpa/JpaEventPublicationRepository.java b/spring-modulith-events/spring-modulith-events-jpa/src/main/java/org/springframework/modulith/events/jpa/JpaEventPublicationRepository.java index da1a1724..42614500 100644 --- a/spring-modulith-events/spring-modulith-events-jpa/src/main/java/org/springframework/modulith/events/jpa/JpaEventPublicationRepository.java +++ b/spring-modulith-events/spring-modulith-events-jpa/src/main/java/org/springframework/modulith/events/jpa/JpaEventPublicationRepository.java @@ -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 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 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 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 findEntityBySerializedEventAndListenerIdAndCompletionDateNull( // + /** + * Returns the type representing completed event publications. + * + * @return will never be {@literal null}. + * @since 1.3 + */ + public Class getCompletedEntityType() { + return JpaEventPublication.getCompletedType(completionMode); + } + + private Optional 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) { diff --git a/spring-modulith-events/spring-modulith-events-jpa/src/test/java/org/springframework/modulith/events/jpa/JpaEventPublicationRepositoryIntegrationTests.java b/spring-modulith-events/spring-modulith-events-jpa/src/test/java/org/springframework/modulith/events/jpa/JpaEventPublicationRepositoryIntegrationTests.java index e5e14007..3024e07c 100644 --- a/spring-modulith-events/spring-modulith-events-jpa/src/test/java/org/springframework/modulith/events/jpa/JpaEventPublicationRepositoryIntegrationTests.java +++ b/spring-modulith-events/spring-modulith-events-jpa/src/test/java/org/springframework/modulith/events/jpa/JpaEventPublicationRepositoryIntegrationTests.java @@ -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 getIncompletePublications() { + return em.createQuery("select p from DefaultJpaEventPublication p", JpaEventPublication.class).getResultList(); + } + + private List 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