/*
* Copyright (c) 2010-2016. Axon Framework
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.axonframework.eventhandling.saga.repository.jpa;
import org.axonframework.common.Assert;
import org.axonframework.common.jpa.EntityManagerProvider;
import org.axonframework.eventhandling.saga.AssociationValue;
import org.axonframework.eventhandling.saga.AssociationValues;
import org.axonframework.eventhandling.saga.repository.SagaStore;
import org.axonframework.eventsourcing.eventstore.TrackingToken;
import org.axonframework.serialization.Serializer;
import org.axonframework.serialization.SimpleSerializedObject;
import org.axonframework.serialization.xml.XStreamSerializer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityNotFoundException;
import java.nio.charset.Charset;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
import java.util.stream.Collectors;
/**
* JPA implementation of the Saga Store. It uses an {@link javax.persistence.EntityManager} to persist the actual saga
* in a backing store in serialized form.
* <p/>
* After each operations that modified the backing store, {@link javax.persistence.EntityManager#flush()} is invoked to
* ensure the store contains the last modifications. To override this behavior, see {@link
* #setUseExplicitFlush(boolean)}
*
* @author Allard Buijze
* @since 3.0
*/
public class JpaSagaStore implements SagaStore<Object> {
private static final Logger logger = LoggerFactory.getLogger(JpaSagaStore.class);
// Saga Queries, non-final to inject the return type and table name.
private final String LOAD_SAGA_QUERY =
"SELECT new "+ serializedObjectType().getName() + "(" +
"se.serializedSaga, se.sagaType, se.revision) " + "FROM " + sagaEntryEntityName() + " se " +
"WHERE se.sagaId = :sagaId " + "AND se.sagaType = :sagaType";
private final String DELETE_SAGA_QUERY = "DELETE FROM " + sagaEntryEntityName() + " se WHERE se.sagaId = :id";
private final String UPDATE_SAGA_QUERY =
"UPDATE " + sagaEntryEntityName() + " s SET s.serializedSaga = :serializedSaga, s.revision = :revision " +
"WHERE s.sagaId = :sagaId AND s.sagaType = :sagaType";
// Association Queries
private static final String DELETE_ASSOCIATION_QUERY =
"DELETE FROM AssociationValueEntry ae " + "WHERE ae.associationKey = :associationKey " +
"AND ae.associationValue = :associationValue " + "AND ae.sagaType = :sagaType " +
"AND ae.sagaId = :sagaId";
private static final String FIND_ASSOCIATION_IDS_QUERY =
"SELECT ae.sagaId FROM AssociationValueEntry ae " + "WHERE ae.associationKey = :associationKey " +
"AND ae.associationValue = :associationValue " + "AND ae.sagaType = :sagaType";
private static final String FIND_ASSOCIATIONS_QUERY =
"SELECT ae FROM AssociationValueEntry ae " + "WHERE ae.sagaType = :sagaType " + "AND ae.sagaId = :sagaId";
private static final String DELETE_ASSOCIATIONS_QUERY =
"DELETE FROM AssociationValueEntry ae WHERE ae.sagaId = :sagaId";
private static final String LOAD_SAGA_NAMED_QUERY = "LOAD_SAGA_NAMED_QUERY";
private static final String DELETE_ASSOCIATION_NAMED_QUERY = "DELETE_ASSOCIATION_NAMED_QUERY";
private static final String FIND_ASSOCIATION_IDS_NAMED_QUERY = "FIND_ASSOCIATION_IDS_NAMED_QUERY";
private static final String FIND_ASSOCIATIONS_NAMED_QUERY = "FIND_ASSOCIATIONS_NAMED_QUERY";
private static final String DELETE_ASSOCIATIONS_NAMED_QUERY = "DELETE_ASSOCIATIONS_NAMED_QUERY";
private static final String DELETE_SAGA_NAMED_QUERY = "DELETE_SAGA_NAMED_QUERY";
private static final String UPDATE_SAGA_NAMED_QUERY = "UPDATE_SAGA_NAMED_QUERY";
private final EntityManagerProvider entityManagerProvider;
private final Serializer serializer;
private volatile boolean useExplicitFlush = true;
/**
* Initializes a Saga Repository with an {@link XStreamSerializer} and given {@code entityManagerProvider}.
*
* @param entityManagerProvider The EntityManagerProvider providing the EntityManager instance for this repository
*/
public JpaSagaStore(EntityManagerProvider entityManagerProvider) {
this(new XStreamSerializer(), entityManagerProvider);
}
/**
* Initializes a Saga Repository with the given {@code serializer} and {@code entityManagerProvider}.
*
* @param serializer The serializer to serialize saga instances with
* @param entityManagerProvider The EntityManagerProvider providing the EntityManager instance for this repository
*/
public JpaSagaStore(Serializer serializer, EntityManagerProvider entityManagerProvider) {
Assert.notNull(entityManagerProvider, () -> "entityManagerProvider may not be null");
this.entityManagerProvider = entityManagerProvider;
this.serializer = serializer;
EntityManager entityManager = this.entityManagerProvider.getEntityManager();
EntityManagerFactory entityManagerFactory = entityManager.getEntityManagerFactory();
entityManagerFactory.addNamedQuery(LOAD_SAGA_NAMED_QUERY, entityManager.createQuery(LOAD_SAGA_QUERY));
entityManagerFactory
.addNamedQuery(DELETE_ASSOCIATION_NAMED_QUERY, entityManager.createQuery(DELETE_ASSOCIATION_QUERY));
entityManagerFactory
.addNamedQuery(FIND_ASSOCIATION_IDS_NAMED_QUERY, entityManager.createQuery(FIND_ASSOCIATION_IDS_QUERY));
entityManagerFactory
.addNamedQuery(DELETE_ASSOCIATIONS_NAMED_QUERY, entityManager.createQuery(DELETE_ASSOCIATIONS_QUERY));
entityManagerFactory
.addNamedQuery(FIND_ASSOCIATIONS_NAMED_QUERY, entityManager.createQuery(FIND_ASSOCIATIONS_QUERY));
entityManagerFactory.addNamedQuery(DELETE_SAGA_NAMED_QUERY, entityManager.createQuery(DELETE_SAGA_QUERY));
entityManagerFactory.addNamedQuery(UPDATE_SAGA_NAMED_QUERY, entityManager.createQuery(UPDATE_SAGA_QUERY));
}
@Override
public <S> Entry<S> loadSaga(Class<S> sagaType, String sagaIdentifier) {
EntityManager entityManager = entityManagerProvider.getEntityManager();
final Class<? extends SimpleSerializedObject<?>> serializedObjectType = serializedObjectType();
List<? extends SimpleSerializedObject<?>> serializedSagaList =
entityManager.createNamedQuery(LOAD_SAGA_NAMED_QUERY, serializedObjectType)
.setParameter("sagaId", sagaIdentifier).setParameter("sagaType", getSagaTypeName(sagaType))
.setMaxResults(1).getResultList();
if (serializedSagaList == null || serializedSagaList.isEmpty()) {
return null;
}
final SimpleSerializedObject<?> serializedSaga = serializedSagaList.get(0);
S loadedSaga = serializer.deserialize(serializedSaga);
Set<AssociationValue> associationValues = loadAssociationValues(entityManager, sagaType, sagaIdentifier);
if (logger.isDebugEnabled()) {
logger.debug("Loaded saga id [{}] of type [{}]", sagaIdentifier, serializedSaga.getType().getName());
}
return new EntryImpl<>(associationValues, loadedSaga);
}
/**
* Loads the {@link AssociationValue association values} of the saga with given {@code sagaIdentifier} and {@code
* sagaType}.
*
* @param entityManager the entity manager instance to use for the query
* @param sagaType the saga instance class
* @param sagaIdentifier the saga identifier
* @return the associations of the given saga
*/
protected Set<AssociationValue> loadAssociationValues(EntityManager entityManager, Class<?> sagaType,
String sagaIdentifier) {
List<AssociationValueEntry> associationValueEntries =
entityManager.createNamedQuery(FIND_ASSOCIATIONS_NAMED_QUERY, AssociationValueEntry.class)
.setParameter("sagaType", getSagaTypeName(sagaType)).setParameter("sagaId", sagaIdentifier)
.getResultList();
return associationValueEntries.stream().map(AssociationValueEntry::getAssociationValue)
.collect(Collectors.toCollection(HashSet::new));
}
/**
* Removes the given {@code associationValue} of the saga with given {@code sagaIdentifier} and {@code sagaType}.
*
* @param entityManager the entity manager instance to use for the query
* @param sagaType the saga instance class
* @param sagaIdentifier the saga identifier
* @param associationValue the association value to remove
*/
protected void removeAssociationValue(EntityManager entityManager, Class<?> sagaType, String sagaIdentifier,
AssociationValue associationValue) {
int updateCount = entityManager.createNamedQuery(DELETE_ASSOCIATION_NAMED_QUERY)
.setParameter("associationKey", associationValue.getKey())
.setParameter("associationValue", associationValue.getValue())
.setParameter("sagaType", getSagaTypeName(sagaType)).setParameter("sagaId", sagaIdentifier)
.executeUpdate();
if (updateCount == 0 && logger.isWarnEnabled()) {
logger.warn("Wanted to remove association value, but it was already gone: sagaId= {}, key={}, value={}",
sagaIdentifier, associationValue.getKey(), associationValue.getValue());
}
}
/**
* Stores the given {@code associationValue} of the saga with given {@code sagaIdentifier} and {@code sagaType}.
*
* @param entityManager the entity manager instance to use for the query
* @param sagaType the saga instance class
* @param sagaIdentifier the saga identifier
* @param associationValue the association value to add
*/
protected void storeAssociationValue(EntityManager entityManager, Class<?> sagaType, String sagaIdentifier,
AssociationValue associationValue) {
entityManager.persist(new AssociationValueEntry(getSagaTypeName(sagaType), sagaIdentifier, associationValue));
}
private String getSagaTypeName(Class<?> sagaType) {
return serializer.typeForClass(sagaType).getName();
}
@Override
public Set<String> findSagas(Class<?> sagaType, AssociationValue associationValue) {
EntityManager entityManager = entityManagerProvider.getEntityManager();
List<String> entries = entityManager.createNamedQuery(FIND_ASSOCIATION_IDS_NAMED_QUERY, String.class)
.setParameter("associationKey", associationValue.getKey())
.setParameter("associationValue", associationValue.getValue())
.setParameter("sagaType", getSagaTypeName(sagaType)).getResultList();
return new TreeSet<>(entries);
}
@Override
public void deleteSaga(Class<?> sagaType, String sagaIdentifier, Set<AssociationValue> associationValues) {
EntityManager entityManager = entityManagerProvider.getEntityManager();
try {
entityManager.createNamedQuery(DELETE_ASSOCIATIONS_NAMED_QUERY).setParameter("sagaId", sagaIdentifier)
.executeUpdate();
entityManager.createNamedQuery(DELETE_SAGA_NAMED_QUERY).setParameter("id", sagaIdentifier).executeUpdate();
} catch (EntityNotFoundException e) {
logger.info("Could not delete {} {}, it appears to have already been deleted.", sagaEntryEntityName(), sagaIdentifier);
}
if (useExplicitFlush) {
entityManager.flush();
}
}
@Override
public void updateSaga(Class<?> sagaType, String sagaIdentifier, Object saga, TrackingToken token,
AssociationValues associationValues) {
EntityManager entityManager = entityManagerProvider.getEntityManager();
AbstractSagaEntry<?> entry = createSagaEntry(saga, sagaIdentifier, serializer);
if (logger.isDebugEnabled()) {
logger.debug("Updating saga id {} as {}", sagaIdentifier, entry instanceof SagaEntry ?
new String((byte[]) entry.getSerializedSaga(), Charset.forName("UTF-8")) : "[Custom serializtion format (not visible)]");
}
int updateCount = entityManager.createNamedQuery(UPDATE_SAGA_NAMED_QUERY)
.setParameter("serializedSaga", entry.getSerializedSaga())
.setParameter("revision", entry.getRevision()).setParameter("sagaId", entry.getSagaId())
.setParameter("sagaType", entry.getSagaType()).executeUpdate();
for (AssociationValue associationValue : associationValues.addedAssociations()) {
storeAssociationValue(entityManager, sagaType, sagaIdentifier, associationValue);
}
for (AssociationValue associationValue : associationValues.removedAssociations()) {
removeAssociationValue(entityManager, sagaType, sagaIdentifier, associationValue);
}
if (updateCount == 0) {
logger.warn("Expected to be able to update a Saga instance, but no rows were found.");
}
if (useExplicitFlush) {
entityManager.flush();
}
}
@Override
public void insertSaga(Class<?> sagaType, String sagaIdentifier, Object saga, TrackingToken token,
Set<AssociationValue> associationValues) {
EntityManager entityManager = entityManagerProvider.getEntityManager();
AbstractSagaEntry<?> entry = createSagaEntry(saga, sagaIdentifier, serializer);
entityManager.persist(entry);
for (AssociationValue associationValue : associationValues) {
storeAssociationValue(entityManager, sagaType, sagaIdentifier, associationValue);
}
if (logger.isDebugEnabled()) {
logger.debug("Storing saga id {} as {}", sagaIdentifier,
entry instanceof SagaEntry ?
new String((byte[]) entry.getSerializedSaga(), Charset.forName("UTF-8")) : "[Custom serializtion format (not visible)]");
}
if (useExplicitFlush) {
entityManager.flush();
}
}
/**
* Sets whether or not to do an explicit {@link javax.persistence.EntityManager#flush()} after each data modifying
* operation on the backing storage. Default to {@code true}
*
* @param useExplicitFlush {@code true} to force flush, {@code false} otherwise.
*/
public void setUseExplicitFlush(boolean useExplicitFlush) {
this.useExplicitFlush = useExplicitFlush;
}
/**
* Intended for clients to override. Defaults to {@link SagaEntry}.
*
* @param sagaIdentifier The identifier of the Saga
* @param saga The Saga instance
* @param serializer The serializer to serialize to the {@link SagaEntry#getSerializedSaga()}
* @return An instanceof @{@link SagaEntry}
*/
protected AbstractSagaEntry<?> createSagaEntry(Object saga, String sagaIdentifier, Serializer serializer) {
return new SagaEntry<>(saga, sagaIdentifier, serializer);
}
/**
* Intended for clients to override. Defaults to 'SagaEntry'.
*
* @return the name of the Jpa event entity
*/
protected String sagaEntryEntityName() {
return SagaEntry.class.getSimpleName();
}
/**
* Intended for clients to override. Defaults to {@link SerializedSaga#getClass() SerialzedSaga.class}
* @return
*/
protected Class<? extends SimpleSerializedObject<?>> serializedObjectType(){
return SerializedSaga.class;
}
private static class EntryImpl<S> implements Entry<S> {
private final Set<AssociationValue> associationValues;
private final S loadedSaga;
public EntryImpl(Set<AssociationValue> associationValues, S loadedSaga) {
this.associationValues = associationValues;
this.loadedSaga = loadedSaga;
}
@Override
public TrackingToken trackingToken() {
return null;
}
@Override
public Set<AssociationValue> associationValues() {
return associationValues;
}
@Override
public S saga() {
return loadedSaga;
}
}
}