From 9e034e6daddf082550cdeb514d5f296e755da9e8 Mon Sep 17 00:00:00 2001 From: Martin Braun Date: Tue, 28 Jul 2015 11:02:42 +0200 Subject: [PATCH] #91 refactorings and Hibernate ORM events implemented --- .../db/events/index/impl/IndexUpdater.java | 38 ++- .../test/db/events/IndexUpdaterTest.java | 8 +- jpa/pom.xml | 8 +- .../hibernate/search/genericjpa/Setup.java | 4 + .../impl/DummyReusableEntityProvider.java | 34 +++ ...eLinkSynchronizedUpdateSourceProvider.java | 15 +- .../impl/EclipseLinkUpdateSource.java | 166 +++++------ .../impl/EventSourceTransactionContext.java | 217 ++++++++++++++ ...rnateSynchronizedUpdateSourceProvider.java | 81 +++++ .../hibernate/impl/HibernateUpdateSource.java | 278 ++++++++++++++++++ .../impl/JPASearchFactoryAdapter.java | 25 +- .../SynchronizedUpdateSourceProvider.java | 3 +- .../jpa/ManualUpdateIntegrationTest.java | 38 +-- .../genericjpa/test/jpa/entities/Place.java | 20 ++ .../test/jpa/entities/Sorcerer.java | 20 ++ ...ernateAutomaticUpdatesIntegrationTest.java | 238 +++++++++++++++ ...seLinkAutomaticUpdatesIntegrationTest.java | 86 +++++- .../test/resources/META-INF/persistence.xml | 19 ++ pom.xml | 7 + 19 files changed, 1161 insertions(+), 144 deletions(-) create mode 100644 jpa/src/main/java/org/hibernate/search/genericjpa/db/events/eclipselink/impl/DummyReusableEntityProvider.java create mode 100644 jpa/src/main/java/org/hibernate/search/genericjpa/db/events/hibernate/impl/EventSourceTransactionContext.java create mode 100644 jpa/src/main/java/org/hibernate/search/genericjpa/db/events/hibernate/impl/HibernateSynchronizedUpdateSourceProvider.java create mode 100644 jpa/src/main/java/org/hibernate/search/genericjpa/db/events/hibernate/impl/HibernateUpdateSource.java create mode 100644 jpa/src/test/java/org/hibernate/search/genericjpa/test/jpa/mysql/MySQLHibernateAutomaticUpdatesIntegrationTest.java diff --git a/db/src/main/java/org/hibernate/search/genericjpa/db/events/index/impl/IndexUpdater.java b/db/src/main/java/org/hibernate/search/genericjpa/db/events/index/impl/IndexUpdater.java index 9e6c2dd..b71f91b 100644 --- a/db/src/main/java/org/hibernate/search/genericjpa/db/events/index/impl/IndexUpdater.java +++ b/db/src/main/java/org/hibernate/search/genericjpa/db/events/index/impl/IndexUpdater.java @@ -141,7 +141,10 @@ public void updateEvent(List updateInfos, EntityProvider provid break; } case EventType.DELETE: { - IndexUpdater.this.indexWrapper.delete( entityClass, inIndexOf, id, tx ); + IndexUpdater.this.indexWrapper.delete( + entityClass, inIndexOf, id, this.entityProvider, + tx + ); break; } default: { @@ -195,13 +198,35 @@ public void updateEvent(List updateInfos, EntityProvider provid } + public void delete( + Class entityClass, + List> inIndexOf, + Object id, + EntityProvider entityProvider, + Transaction tx) { + this.indexWrapper.delete( entityClass, inIndexOf, id, entityProvider , tx ); + } + + public void update(Object entity, Transaction tx) { + this.indexWrapper.update( entity, tx ); + } + + public void index(Object entity, Transaction tx) { + this.indexWrapper.update( entity, tx ); + } + public void close() { this.exec.shutdown(); } public interface IndexWrapper { - void delete(Class entityClass, List> inIndexOf, Object id, Transaction tx); + void delete( + Class entityClass, + List> inIndexOf, + Object id, + EntityProvider entityProvider, + Transaction tx); void update(Object entity, Transaction tx); @@ -218,7 +243,12 @@ public DefaultIndexWrapper(ExtendedSearchIntegrator searchIntegrator) { } @Override - public void delete(Class entityClass, List> inIndexOf, Object id, Transaction tx) { + public void delete( + Class entityClass, + List> inIndexOf, + Object id, + EntityProvider entityProvider, + Transaction tx) { for ( Class indexClass : inIndexOf ) { RehashedTypeMetadata metadata = IndexUpdater.this.metadataForIndexRoot.get( indexClass ); List fields = metadata.getIdFieldNamesForType().get( entityClass ); @@ -271,7 +301,7 @@ public void delete(Class entityClass, List> inIndexOf, Object id, Tr ).maxResults( HSQUERY_BATCH ) .queryEntityInfos() ) { Serializable originalId = (Serializable) entityInfo.getProjection()[0]; - Object original = IndexUpdater.this.entityProvider.get( indexClass, originalId ); + Object original = entityProvider.get( indexClass, originalId ); if ( original != null ) { this.update( original, tx ); } diff --git a/db/src/test/java/org/hibernate/search/genericjpa/test/db/events/IndexUpdaterTest.java b/db/src/test/java/org/hibernate/search/genericjpa/test/db/events/IndexUpdaterTest.java index 3feea8d..4bb5cbf 100644 --- a/db/src/test/java/org/hibernate/search/genericjpa/test/db/events/IndexUpdaterTest.java +++ b/db/src/test/java/org/hibernate/search/genericjpa/test/db/events/IndexUpdaterTest.java @@ -26,6 +26,7 @@ import org.hibernate.search.genericjpa.db.events.UpdateConsumer.UpdateEventInfo; import org.hibernate.search.genericjpa.db.events.index.impl.IndexUpdater; import org.hibernate.search.genericjpa.db.events.index.impl.IndexUpdater.IndexWrapper; +import org.hibernate.search.genericjpa.entity.EntityProvider; import org.hibernate.search.genericjpa.entity.ReusableEntityProvider; import org.hibernate.search.genericjpa.factory.StandaloneSearchConfiguration; import org.hibernate.search.genericjpa.factory.Transaction; @@ -102,7 +103,12 @@ public void testWithoutIndex() { IndexWrapper indexWrapper = new IndexWrapper() { @Override - public void delete(Class entityClass, List> inIndexOf, Object id, Transaction tx) { + public void delete( + Class entityClass, + List> inIndexOf, + Object id, + EntityProvider entityProvider, + Transaction tx) { Object obj = IndexUpdaterTest.this.obj( entityClass ); System.out.println( entityClass ); System.out.println( updateInfoSet ); diff --git a/jpa/pom.xml b/jpa/pom.xml index 3fc8be4..91aae50 100644 --- a/jpa/pom.xml +++ b/jpa/pom.xml @@ -94,6 +94,13 @@ provided + + org.hibernate + hibernate-entitymanager + provided + + + com.squareup @@ -105,7 +112,6 @@ javaee-api provided - diff --git a/jpa/src/main/java/org/hibernate/search/genericjpa/Setup.java b/jpa/src/main/java/org/hibernate/search/genericjpa/Setup.java index de50006..aff39e8 100644 --- a/jpa/src/main/java/org/hibernate/search/genericjpa/Setup.java +++ b/jpa/src/main/java/org/hibernate/search/genericjpa/Setup.java @@ -19,6 +19,7 @@ import org.hibernate.search.genericjpa.annotations.CustomUpdateEntityProvider; import org.hibernate.search.genericjpa.annotations.InIndex; import org.hibernate.search.genericjpa.db.events.eclipselink.impl.EclipseLinkSynchronizedUpdateSourceProvider; +import org.hibernate.search.genericjpa.db.events.hibernate.impl.HibernateSynchronizedUpdateSourceProvider; import org.hibernate.search.genericjpa.db.events.triggers.TriggerSQLStringSource; import org.hibernate.search.genericjpa.entity.EntityManagerEntityProvider; import org.hibernate.search.genericjpa.exception.SearchException; @@ -178,6 +179,9 @@ else if ( "manual-updates".equals( type ) ) { else if ( "eclipselink".equals( type ) ) { synchronizedUpdateSourceProvider = new EclipseLinkSynchronizedUpdateSourceProvider(); } + else if ( "hibernate".equals( type ) ) { + synchronizedUpdateSourceProvider = new HibernateSynchronizedUpdateSourceProvider(); + } else { throw new SearchException( "unrecognized " + SEARCH_FACTORY_TYPE_KEY + ": " + type ); } diff --git a/jpa/src/main/java/org/hibernate/search/genericjpa/db/events/eclipselink/impl/DummyReusableEntityProvider.java b/jpa/src/main/java/org/hibernate/search/genericjpa/db/events/eclipselink/impl/DummyReusableEntityProvider.java new file mode 100644 index 0000000..343e5ba --- /dev/null +++ b/jpa/src/main/java/org/hibernate/search/genericjpa/db/events/eclipselink/impl/DummyReusableEntityProvider.java @@ -0,0 +1,34 @@ +package org.hibernate.search.genericjpa.db.events.eclipselink.impl; + +import java.util.List; +import java.util.Map; + +import org.hibernate.AssertionFailure; +import org.hibernate.search.genericjpa.entity.ReusableEntityProvider; + +/** + * Created by Martin on 28.07.2015. + */ +public class DummyReusableEntityProvider implements ReusableEntityProvider { + + @Override + public void close() { + throw new AssertionFailure( "should not have been used" ); + } + + @Override + public void open() { + throw new AssertionFailure( "should not have been used" ); + } + + @Override + public Object get(Class entityClass, Object id, Map hints) { + throw new AssertionFailure( "should not have been used" ); + } + + @Override + public List getBatch(Class entityClass, List id, Map hints) { + throw new AssertionFailure( "should not have been used" ); + } + +} diff --git a/jpa/src/main/java/org/hibernate/search/genericjpa/db/events/eclipselink/impl/EclipseLinkSynchronizedUpdateSourceProvider.java b/jpa/src/main/java/org/hibernate/search/genericjpa/db/events/eclipselink/impl/EclipseLinkSynchronizedUpdateSourceProvider.java index af5e870..abbbc39 100644 --- a/jpa/src/main/java/org/hibernate/search/genericjpa/db/events/eclipselink/impl/EclipseLinkSynchronizedUpdateSourceProvider.java +++ b/jpa/src/main/java/org/hibernate/search/genericjpa/db/events/eclipselink/impl/EclipseLinkSynchronizedUpdateSourceProvider.java @@ -16,8 +16,12 @@ import org.eclipse.persistence.jpa.JpaEntityManager; import org.eclipse.persistence.sessions.Session; +import org.hibernate.search.engine.integration.impl.ExtendedSearchIntegrator; import org.hibernate.search.genericjpa.JPASearchFactoryController; +import org.hibernate.search.genericjpa.db.events.index.impl.IndexUpdater; +import org.hibernate.search.genericjpa.entity.ReusableEntityProvider; import org.hibernate.search.genericjpa.events.impl.SynchronizedUpdateSource; +import org.hibernate.search.genericjpa.impl.JPASearchFactoryAdapter; import org.hibernate.search.genericjpa.impl.SynchronizedUpdateSourceProvider; import org.hibernate.search.genericjpa.metadata.impl.RehashedTypeMetadata; @@ -28,7 +32,7 @@ public class EclipseLinkSynchronizedUpdateSourceProvider implements Synchronized @Override public SynchronizedUpdateSource getUpdateSource( - JPASearchFactoryController searchFactoryController, + ExtendedSearchIntegrator searchIntegrator, Map, RehashedTypeMetadata> rehashedTypeMetadataPerIndexRoot, Map, List>> containedInIndexOf, Properties properties, @@ -39,8 +43,15 @@ public SynchronizedUpdateSource getUpdateSource( try { Session session = entityManager.getServerSession(); + IndexUpdater indexUpdater = new IndexUpdater( + rehashedTypeMetadataPerIndexRoot, + containedInIndexOf, + new DummyReusableEntityProvider(), + searchIntegrator + ); + EclipseLinkUpdateSource eclipseLinkUpdateSource = new EclipseLinkUpdateSource( - searchFactoryController, + indexUpdater, indexRelevantEntities, rehashedTypeMetadataPerIndexRoot, containedInIndexOf diff --git a/jpa/src/main/java/org/hibernate/search/genericjpa/db/events/eclipselink/impl/EclipseLinkUpdateSource.java b/jpa/src/main/java/org/hibernate/search/genericjpa/db/events/eclipselink/impl/EclipseLinkUpdateSource.java index e8643c4..6ba7fd5 100644 --- a/jpa/src/main/java/org/hibernate/search/genericjpa/db/events/eclipselink/impl/EclipseLinkUpdateSource.java +++ b/jpa/src/main/java/org/hibernate/search/genericjpa/db/events/eclipselink/impl/EclipseLinkUpdateSource.java @@ -6,9 +6,8 @@ */ package org.hibernate.search.genericjpa.db.events.eclipselink.impl; -import javax.persistence.EntityManagerFactory; +import java.io.IOException; import java.io.Serializable; -import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Set; @@ -17,8 +16,6 @@ import org.eclipse.persistence.descriptors.DescriptorEvent; import org.eclipse.persistence.descriptors.DescriptorEventAdapter; -import org.eclipse.persistence.expressions.Expression; -import org.eclipse.persistence.internal.sessions.UnitOfWorkImpl; import org.eclipse.persistence.queries.ObjectLevelReadQuery; import org.eclipse.persistence.queries.ReadObjectQuery; import org.eclipse.persistence.sessions.Session; @@ -28,21 +25,20 @@ import org.hibernate.annotations.common.reflection.XProperty; import org.hibernate.search.backend.spi.SingularTermDeletionQuery; -import org.hibernate.search.backend.spi.Work; -import org.hibernate.search.backend.spi.WorkType; import org.hibernate.search.bridge.FieldBridge; import org.hibernate.search.bridge.StringBridge; import org.hibernate.search.engine.ProjectionConstants; import org.hibernate.search.engine.metadata.impl.DocumentFieldMetadata; import org.hibernate.search.genericjpa.JPASearchFactoryController; +import org.hibernate.search.genericjpa.db.events.index.impl.IndexUpdater; +import org.hibernate.search.genericjpa.entity.EntityProvider; import org.hibernate.search.genericjpa.events.impl.SynchronizedUpdateSource; +import org.hibernate.search.genericjpa.exception.AssertionFailure; import org.hibernate.search.genericjpa.factory.Transaction; import org.hibernate.search.genericjpa.factory.impl.SubClassSupportInstanceInitializer; import org.hibernate.search.genericjpa.metadata.impl.RehashedTypeMetadata; import org.hibernate.search.jpa.FullTextEntityManager; import org.hibernate.search.jpa.FullTextQuery; -import org.hibernate.search.query.engine.spi.EntityInfo; -import org.hibernate.search.query.engine.spi.HSQuery; import org.hibernate.search.spi.InstanceInitializer; /** @@ -58,7 +54,7 @@ public class EclipseLinkUpdateSource implements SynchronizedUpdateSource { private static final InstanceInitializer INSTANCE_INITIALIZER = SubClassSupportInstanceInitializer.INSTANCE; - private final JPASearchFactoryController searchFactoryController; + private final IndexUpdater indexUpdater; private final Set> indexRelevantEntities; private final Map, RehashedTypeMetadata> rehashedTypeMetadataPerIndexRoot; private final Map, List>> containedInIndexOf; @@ -66,14 +62,14 @@ public class EclipseLinkUpdateSource implements SynchronizedUpdateSource { final DescriptorEventAspect descriptorEventAspect; final SessionEventAspect sessionEventAspect; - private final ConcurrentHashMap fullTextEntityManagers = new ConcurrentHashMap<>(); + private final ConcurrentHashMap transactions = new ConcurrentHashMap<>(); public EclipseLinkUpdateSource( - JPASearchFactoryController searchFactoryController, + IndexUpdater indexUpdater, Set> indexRelevantEntities, Map, RehashedTypeMetadata> rehashedTypeMetadataPerIndexRoot, Map, List>> containedInIndexOf) { - this.searchFactoryController = searchFactoryController; + this.indexUpdater = indexUpdater; this.indexRelevantEntities = indexRelevantEntities; this.descriptorEventAspect = new DescriptorEventAspect(); this.sessionEventAspect = new SessionEventAspect(); @@ -93,8 +89,8 @@ public void postInsert(DescriptorEvent event) { if ( session.isUnitOfWork() ) { session = ((UnitOfWork) session).getParent(); } - FullTextEntityManager fem = EclipseLinkUpdateSource.this.fullTextEntityManagers.get( session ); - fem.index( entity ); + Transaction tx = EclipseLinkUpdateSource.this.transactions.get( session ); + EclipseLinkUpdateSource.this.indexUpdater.index( entity, tx ); } } @@ -108,8 +104,8 @@ public void postUpdate(DescriptorEvent event) { if ( session.isUnitOfWork() ) { session = ((UnitOfWork) session).getParent(); } - FullTextEntityManager fem = EclipseLinkUpdateSource.this.fullTextEntityManagers.get( session ); - fem.index( entity ); + Transaction tx = EclipseLinkUpdateSource.this.transactions.get( session ); + EclipseLinkUpdateSource.this.indexUpdater.update( entity, tx ); } } @@ -125,86 +121,45 @@ public void postDelete(DescriptorEvent event) { Class entityClass = INSTANCE_INITIALIZER.getClass( entity ); if ( EclipseLinkUpdateSource.this.indexRelevantEntities.contains( entityClass ) ) { LOGGER.fine( "Delete Event for " + entity ); - Session session = event.getSession(); - if ( session.isUnitOfWork() ) { - session = ((UnitOfWork) session).getParent(); + Session tmp = event.getSession(); + if ( tmp.isUnitOfWork() ) { + tmp = ((UnitOfWork) tmp).getParent(); } - FullTextEntityManager fem = EclipseLinkUpdateSource.this.fullTextEntityManagers.get( session ); + final Session session = tmp; + Transaction tx = EclipseLinkUpdateSource.this.transactions.get( session ); List> inIndexOf = EclipseLinkUpdateSource.this.containedInIndexOf.get( entityClass ); - for ( Class indexClass : inIndexOf ) { + if ( inIndexOf.size() > 0 ) { + //hack, but works RehashedTypeMetadata metadata = EclipseLinkUpdateSource.this.rehashedTypeMetadataPerIndexRoot.get( - indexClass + inIndexOf.get( 0 ) ); - XProperty idProperty = metadata.getIdPropertyAccessorForType().get( entityClass ); Object id = idProperty.invoke( entity ); + EclipseLinkUpdateSource.this.indexUpdater.delete( + entityClass, inIndexOf, id, new EntityProvider() { - List fields = metadata.getIdFieldNamesForType().get( entityClass ); - for ( String field : fields ) { - DocumentFieldMetadata metaDataForIdField = metadata.getDocumentFieldMetadataForIdFieldName() - .get( - field - ); - - SingularTermDeletionQuery.Type idType = metadata.getSingularTermDeletionQueryTypeForIdFieldName() - .get( entityClass ); - Object idValueForDeletion; - if ( idType == SingularTermDeletionQuery.Type.STRING ) { - FieldBridge fb = metaDataForIdField.getFieldBridge(); - if ( !(fb instanceof StringBridge) ) { - throw new IllegalArgumentException( "no TwoWayStringBridge found for field: " + field ); - } - idValueForDeletion = ((StringBridge) fb).objectToString( id ); - } - else { - idValueForDeletion = id; - } - - if ( indexClass.equals( entityClass ) ) { - fem.purge( entityClass, (Serializable) id ); - } - else { - FullTextQuery fullTextQuery = fem.createFullTextQuery( - fem.getSearchFactory() - .buildQueryBuilder() - .forEntity( indexClass ) - .get() - .keyword() - .onField( field ) - .matching( idValueForDeletion ) - .createQuery(), indexClass - ); - - fullTextQuery.setMaxResults( HSQUERY_BATCH ); - fullTextQuery.setProjection( ProjectionConstants.ID ); - - int count = fullTextQuery.getResultSize(); - int processed = 0; - // this was just contained somewhere - // so we have to update the containing entity - while ( processed < count ) { - fullTextQuery.setFirstResult( processed ); - for ( Object[] projection : (List) fullTextQuery.getResultList() ) { - Serializable originalId = (Serializable) projection[0]; + @Override + public Object get(Class entityClass, Object id, Map hints) { ReadObjectQuery nativeQuery = new ReadObjectQuery(); - nativeQuery.setReferenceClass( indexClass ); - nativeQuery.setSelectionId( originalId ); + nativeQuery.setReferenceClass( entityClass ); + nativeQuery.setSelectionId( id ); nativeQuery.setCacheUsage( ObjectLevelReadQuery.DoNotCheckCache ); Object original = session.executeQuery( nativeQuery ); - if ( original != null ) { - fem.index( original ); - } - else { - // original is not available in the - // database, but it will be deleted by its - // own delete event - // TODO: log this? - } + return original; } - processed += HSQUERY_BATCH; - } - } - } + + @Override + public List getBatch(Class entityClass, List id, Map hints) { + throw new AssertionFailure( "normally not used in IndexUpdater" ); + } + + @Override + public void close() throws IOException { + //no-op + } + + }, tx + ); } } } @@ -216,24 +171,23 @@ private class SessionEventAspect extends SessionEventAdapter { @Override public void postBeginTransaction(SessionEvent event) { Session session = event.getSession(); - FullTextEntityManager fem = EclipseLinkUpdateSource.this.fullTextEntityManagers.get( session ); - if ( fem != null && fem.isSearchTransactionInProgress() ) { + Transaction tx = EclipseLinkUpdateSource.this.transactions.get( session ); + if ( tx != null && tx.isTransactionInProgress() ) { //we are fine } else { - fem = EclipseLinkUpdateSource.this.searchFactoryController.getFullTextEntityManager( null ); - fem.beginSearchTransaction(); - EclipseLinkUpdateSource.this.fullTextEntityManagers.put( session, fem ); + tx = new Transaction(); + EclipseLinkUpdateSource.this.transactions.put( session, tx ); } } @Override public void postCommitTransaction(SessionEvent event) { Session session = event.getSession(); - FullTextEntityManager fem = EclipseLinkUpdateSource.this.fullTextEntityManagers.get( session ); - if ( fem != null && fem.isSearchTransactionInProgress() ) { - fem.commitSearchTransaction(); - EclipseLinkUpdateSource.this.fullTextEntityManagers.remove( session ); + Transaction tx = EclipseLinkUpdateSource.this.transactions.get( session ); + if ( tx != null && tx.isTransactionInProgress() ) { + tx.commit(); + EclipseLinkUpdateSource.this.transactions.remove( session ); } else { LOGGER.warning( @@ -245,10 +199,10 @@ public void postCommitTransaction(SessionEvent event) { @Override public void postRollbackTransaction(SessionEvent event) { Session session = event.getSession(); - FullTextEntityManager fem = EclipseLinkUpdateSource.this.fullTextEntityManagers.get( session ); - if ( fem != null && fem.isSearchTransactionInProgress() ) { - fem.rollbackSearchTransaction(); - EclipseLinkUpdateSource.this.fullTextEntityManagers.remove( session ); + Transaction tx = EclipseLinkUpdateSource.this.transactions.get( session ); + if ( tx != null && tx.isTransactionInProgress() ) { + tx.rollback(); + EclipseLinkUpdateSource.this.transactions.remove( session ); } else { LOGGER.warning( @@ -257,11 +211,27 @@ public void postRollbackTransaction(SessionEvent event) { } } + @Override + public void postLogout(SessionEvent event) { + Session session = event.getSession(); + Transaction tx = EclipseLinkUpdateSource.this.transactions.get( session ); + if ( tx != null && tx.isTransactionInProgress() ) { + LOGGER.warning( + "rolling back transaction because session logged out..." + ); + tx.rollback(); + EclipseLinkUpdateSource.this.transactions.remove( session ); + } + } + } @Override public void close() { + //just to make sure + this.transactions.clear(); + this.indexUpdater.close(); } } diff --git a/jpa/src/main/java/org/hibernate/search/genericjpa/db/events/hibernate/impl/EventSourceTransactionContext.java b/jpa/src/main/java/org/hibernate/search/genericjpa/db/events/hibernate/impl/EventSourceTransactionContext.java new file mode 100644 index 0000000..e1d70da --- /dev/null +++ b/jpa/src/main/java/org/hibernate/search/genericjpa/db/events/hibernate/impl/EventSourceTransactionContext.java @@ -0,0 +1,217 @@ +/* + * Hibernate Search, full-text search for your domain model + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.search.genericjpa.db.events.hibernate.impl; + + +import javax.transaction.Status; +import javax.transaction.Synchronization; +import javax.transaction.TransactionManager; +import java.io.Serializable; + +import org.hibernate.HibernateException; +import org.hibernate.action.spi.AfterTransactionCompletionProcess; +import org.hibernate.action.spi.BeforeTransactionCompletionProcess; +import org.hibernate.engine.spi.ActionQueue; +import org.hibernate.engine.spi.SessionImplementor; +import org.hibernate.event.spi.EventSource; +import org.hibernate.search.backend.TransactionContext; +import org.hibernate.search.util.logging.impl.Log; +import org.hibernate.search.util.logging.impl.LoggerFactory; +import org.hibernate.service.Service; + +/** + * Implementation of the transactional context on top of an EventSource (Session) + * + * @author Navin Surtani - navin@surtani.org + * @author Emmanuel Bernard + * @author Sanne Grinovero + */ +public class EventSourceTransactionContext implements TransactionContext, Serializable { + + private static final Log log = LoggerFactory.make(); + + private final EventSource eventSource; + + //constructor time is too early to define the value of realTxInProgress, + //postpone it, otherwise doing + // " openSession - beginTransaction " + //will behave as "out of transaction" in the whole session lifespan. + private boolean realTxInProgress = false; + private boolean realTxInProgressInitialized = false; + + public EventSourceTransactionContext(EventSource eventSource) { + this.eventSource = eventSource; + } + + @Override + public Object getTransactionIdentifier() { + if ( isRealTransactionInProgress() ) { + return eventSource.getTransaction(); + } + else { + return eventSource; + } + } + + @Override + public void registerSynchronization(Synchronization synchronization) { + if ( isRealTransactionInProgress() ) { + //use {Before|After}TransactionCompletionProcess instead of registerTransaction because it does not + //swallow transactions. + /* + * HSEARCH-540: the pre process must be both a BeforeTransactionCompletionProcess and a TX Synchronization. + * + * In a resource-local tx env, the beforeCommit phase is called after the flush, and prepares work queue. + * Also, any exceptions that occur during that are propagated (if a Synchronization was used, the exceptions + * would be eaten). + * + * In a JTA env, the before transaction completion is called before the flush, so not all changes are yet + * written. However, Synchronization-s do propagate exceptions, so they can be safely used. + */ + final ActionQueue actionQueue = eventSource.getActionQueue(); + boolean isLocal = isLocalTransaction(); + if ( isLocal ) { + //if local tx never use Synchronization + actionQueue.registerProcess( new DelegateToSynchronizationOnBeforeTx( synchronization ) ); + } + else { + //TODO could we remove the action queue registration in this case? + actionQueue.registerProcess( new DelegateToSynchronizationOnBeforeTx( synchronization ) ); + eventSource.getTransaction().registerSynchronization( + new BeforeCommitSynchronizationDelegator( synchronization ) + ); + } + + //executed in all environments + actionQueue.registerProcess( new DelegateToSynchronizationOnAfterTx( synchronization ) ); + } + //else { + // //registerSynchronization is only called if isRealTransactionInProgress or if + // // a flushListener was found; still we might need to find the listener again + // // as it might have been cleared by serialization (is transient). + // flushListener = getIndexWorkFlushEventListener(); + // if ( flushListener != null ) { + // flushListener.addSynchronization( eventSource, synchronization ); + // } + // else { + // //shouldn't happen if the code about serialization is fine: + // throw new SearchException( "AssertionFailure: flushListener not registered any more." ); + // } + //} + } + + private boolean isLocalTransaction() { + TransactionManager transactionManager = eventSource + .getTransactionCoordinator() + .getTransactionContext() + .getTransactionEnvironment() + .getJtaPlatform() + //.canRegisterSynchronization() <- TODO explore: possibly a better option? + .retrieveTransactionManager(); + return transactionManager == null; + } + + private T getService(Class serviceClass) { + return eventSource.getFactory().getServiceRegistry().getService( serviceClass ); + } + + //private FullTextIndexEventListener getIndexWorkFlushEventListener() { + // if ( this.flushListener != null ) { + // //for the "transient" case: might have been nullified. + // return flushListener; + // } + // final Iterable listeners = getService( EventListenerRegistry.class ) + // .getEventListenerGroup( EventType.FLUSH ).listeners(); + // for ( FlushEventListener listener : listeners ) { + // if ( FullTextIndexEventListener.class.isAssignableFrom( listener.getClass() ) ) { + // return (FullTextIndexEventListener) listener; + // } + // } + // log.debug( "FullTextIndexEventListener was not registered as FlushEventListener" ); + // return null; + //} + + //The code is not really fitting the method name; + //(unless you consider a flush as a mini-transaction) + //This is because we want to behave as "inTransaction" if the flushListener is registered. + @Override + public boolean isTransactionInProgress() { + // either it is a real transaction, or if we are capable to manage this in the IndexWorkFlushEventListener + return + //getIndexWorkFlushEventListener() != null || + isRealTransactionInProgress(); + } + + private boolean isRealTransactionInProgress() { + if ( !realTxInProgressInitialized ) { + realTxInProgress = eventSource.isTransactionInProgress(); + realTxInProgressInitialized = true; + } + return realTxInProgress; + } + + private static class DelegateToSynchronizationOnBeforeTx implements BeforeTransactionCompletionProcess { + private final Synchronization synchronization; + + DelegateToSynchronizationOnBeforeTx(Synchronization synchronization) { + this.synchronization = synchronization; + } + + @Override + public void doBeforeTransactionCompletion(SessionImplementor sessionImplementor) { + try { + synchronization.beforeCompletion(); + } + catch (Exception e) { + throw new HibernateException( + "Error while indexing in Hibernate Search (before transaction completion)", + e + ); + } + } + } + + private static class DelegateToSynchronizationOnAfterTx implements AfterTransactionCompletionProcess { + private final Synchronization synchronization; + + DelegateToSynchronizationOnAfterTx(Synchronization synchronization) { + this.synchronization = synchronization; + } + + @Override + public void doAfterTransactionCompletion(boolean success, SessionImplementor sessionImplementor) { + try { + synchronization.afterCompletion( success ? Status.STATUS_COMMITTED : Status.STATUS_ROLLEDBACK ); + } + catch (Exception e) { + throw new HibernateException( + "Error while indexing in Hibernate Search (after transaction completion)", + e + ); + } + } + } + + private static class BeforeCommitSynchronizationDelegator implements Synchronization { + private final Synchronization synchronization; + + public BeforeCommitSynchronizationDelegator(Synchronization sync) { + this.synchronization = sync; + } + + @Override + public void beforeCompletion() { + this.synchronization.beforeCompletion(); + } + + @Override + public void afterCompletion(int status) { + //do not delegate + } + } + +} \ No newline at end of file diff --git a/jpa/src/main/java/org/hibernate/search/genericjpa/db/events/hibernate/impl/HibernateSynchronizedUpdateSourceProvider.java b/jpa/src/main/java/org/hibernate/search/genericjpa/db/events/hibernate/impl/HibernateSynchronizedUpdateSourceProvider.java new file mode 100644 index 0000000..0b20327 --- /dev/null +++ b/jpa/src/main/java/org/hibernate/search/genericjpa/db/events/hibernate/impl/HibernateSynchronizedUpdateSourceProvider.java @@ -0,0 +1,81 @@ +/* + * Hibernate Search, full-text search for your domain model + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.search.genericjpa.db.events.hibernate.impl; + +import javax.persistence.EntityManagerFactory; +import javax.transaction.TransactionManager; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.Set; + +import org.hibernate.event.service.spi.DuplicationStrategy; +import org.hibernate.event.service.spi.EventListenerRegistry; +import org.hibernate.event.spi.EventType; +import org.hibernate.internal.SessionFactoryImpl; +import org.hibernate.jpa.HibernateEntityManagerFactory; +import org.hibernate.search.engine.integration.impl.ExtendedSearchIntegrator; +import org.hibernate.search.genericjpa.events.impl.SynchronizedUpdateSource; +import org.hibernate.search.genericjpa.impl.SynchronizedUpdateSourceProvider; +import org.hibernate.search.genericjpa.metadata.impl.RehashedTypeMetadata; +import org.hibernate.service.ServiceRegistry; + +/** + * Created by Martin on 28.07.2015. + */ +public class HibernateSynchronizedUpdateSourceProvider implements SynchronizedUpdateSourceProvider { + + @Override + public SynchronizedUpdateSource getUpdateSource( + ExtendedSearchIntegrator searchIntegrator, + Map, RehashedTypeMetadata> rehashedTypeMetadataPerIndexRoot, + Map, List>> containedInIndexOf, + Properties properties, + EntityManagerFactory emf, + TransactionManager transactionManager, + Set> indexRelevantEntities) { + HibernateEntityManagerFactory hibernateEntityManagerFactory = + (HibernateEntityManagerFactory) emf; + SessionFactoryImpl sessionFactory = (SessionFactoryImpl) hibernateEntityManagerFactory.getSessionFactory(); + ServiceRegistry serviceRegistry = sessionFactory.getServiceRegistry(); + EventListenerRegistry listenerRegistry = serviceRegistry.getService( EventListenerRegistry.class ); + + HibernateUpdateSource updateSource = new HibernateUpdateSource(); + updateSource.initialize( searchIntegrator ); + + listenerRegistry.addDuplicationStrategy( new DuplicationStrategyImpl( HibernateUpdateSource.class ) ); + + listenerRegistry.appendListeners( EventType.POST_INSERT, updateSource ); + listenerRegistry.appendListeners( EventType.POST_UPDATE, updateSource ); + listenerRegistry.appendListeners( EventType.POST_DELETE, updateSource ); + listenerRegistry.appendListeners( EventType.POST_COLLECTION_RECREATE, updateSource ); + listenerRegistry.appendListeners( EventType.POST_COLLECTION_REMOVE, updateSource ); + listenerRegistry.appendListeners( EventType.POST_COLLECTION_UPDATE, updateSource ); + + return updateSource; + } + + public static class DuplicationStrategyImpl implements DuplicationStrategy { + private final Class checkClass; + + public DuplicationStrategyImpl(Class checkClass) { + this.checkClass = checkClass; + } + + @Override + public boolean areMatch(Object listener, Object original) { + // not isAssignableFrom since the user could subclass + return checkClass == original.getClass() && checkClass == listener.getClass(); + } + + @Override + public DuplicationStrategy.Action getAction() { + return Action.KEEP_ORIGINAL; + } + } + +} diff --git a/jpa/src/main/java/org/hibernate/search/genericjpa/db/events/hibernate/impl/HibernateUpdateSource.java b/jpa/src/main/java/org/hibernate/search/genericjpa/db/events/hibernate/impl/HibernateUpdateSource.java new file mode 100644 index 0000000..e65da9a --- /dev/null +++ b/jpa/src/main/java/org/hibernate/search/genericjpa/db/events/hibernate/impl/HibernateUpdateSource.java @@ -0,0 +1,278 @@ +/* + * Hibernate Search, full-text search for your domain model + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.search.genericjpa.db.events.hibernate.impl; + +import javax.transaction.Synchronization; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.Serializable; +import java.lang.reflect.Field; +import java.util.Map; +import java.util.logging.Logger; + +import org.hibernate.Session; +import org.hibernate.collection.spi.PersistentCollection; +import org.hibernate.engine.spi.EntityEntry; +import org.hibernate.event.spi.AbstractCollectionEvent; +import org.hibernate.event.spi.AbstractEvent; +import org.hibernate.event.spi.PostCollectionRecreateEvent; +import org.hibernate.event.spi.PostCollectionRecreateEventListener; +import org.hibernate.event.spi.PostCollectionRemoveEvent; +import org.hibernate.event.spi.PostCollectionRemoveEventListener; +import org.hibernate.event.spi.PostCollectionUpdateEvent; +import org.hibernate.event.spi.PostCollectionUpdateEventListener; +import org.hibernate.event.spi.PostDeleteEvent; +import org.hibernate.event.spi.PostDeleteEventListener; +import org.hibernate.event.spi.PostInsertEvent; +import org.hibernate.event.spi.PostInsertEventListener; +import org.hibernate.event.spi.PostUpdateEvent; +import org.hibernate.event.spi.PostUpdateEventListener; +import org.hibernate.persister.entity.EntityPersister; +import org.hibernate.search.backend.spi.Work; +import org.hibernate.search.backend.spi.WorkType; +import org.hibernate.search.engine.integration.impl.ExtendedSearchIntegrator; +import org.hibernate.search.engine.spi.AbstractDocumentBuilder; +import org.hibernate.search.engine.spi.EntityIndexBinding; +import org.hibernate.search.genericjpa.events.impl.SynchronizedUpdateSource; +import org.hibernate.search.util.impl.Maps; +import org.hibernate.search.util.impl.ReflectionHelper; + +/** + * Hibernate ORM event listener called by various ORM life cycle events. This listener must be registered in order + * to enable automatic index updates. + * + * @author Gavin King + * @author Emmanuel Bernard + * @author Mattias Arbin + * @author Sanne Grinovero + * @author Hardy Ferentschik + * @author Martin Braun (adaption) + */ +public class HibernateUpdateSource implements SynchronizedUpdateSource, PostDeleteEventListener, + PostInsertEventListener, PostUpdateEventListener, + PostCollectionRecreateEventListener, PostCollectionRemoveEventListener, + PostCollectionUpdateEventListener, + Serializable { + + private static final Logger LOGGER = Logger.getLogger( HibernateUpdateSource.class.getName() ); + + private boolean disabled = false; + private boolean skipDirtyChecks = true; + private ExtendedSearchIntegrator extendedIntegrator; + + @Override + public void onPostDelete(PostDeleteEvent event) { + if ( disabled ) { + return; + } + + final Object entity = event.getEntity(); + if ( getDocumentBuilder( entity ) != null ) { + // FIXME The engine currently needs to know about details such as identifierRollbackEnabled + // but we should not move the responsibility to figure out the proper id to the engine + boolean identifierRollbackEnabled = event.getSession() + .getFactory() + .getSettings() + .isIdentifierRollbackEnabled(); + processWork( + entity, + event.getId(), + WorkType.DELETE, + event, + identifierRollbackEnabled + ); + } + } + + @Override + public void onPostInsert(PostInsertEvent event) { + if ( disabled ) { + return; + } + + final Object entity = event.getEntity(); + if ( getDocumentBuilder( entity ) != null ) { + Serializable id = event.getId(); + processWork( entity, id, WorkType.ADD, event, false ); + } + } + + @Override + public void onPostUpdate(PostUpdateEvent event) { + if ( disabled ) { + return; + } + + final Object entity = event.getEntity(); + final AbstractDocumentBuilder docBuilder = getDocumentBuilder( entity ); + if ( docBuilder != null && (skipDirtyChecks || docBuilder.isDirty( + getDirtyPropertyNames( + event + ) + )) ) { + Serializable id = event.getId(); + processWork( entity, id, WorkType.UPDATE, event, false ); + } + } + + @Override + public void onPostRecreateCollection(PostCollectionRecreateEvent event) { + processCollectionEvent( event ); + } + + @Override + public void onPostRemoveCollection(PostCollectionRemoveEvent event) { + processCollectionEvent( event ); + } + + @Override + public void onPostUpdateCollection(PostCollectionUpdateEvent event) { + processCollectionEvent( event ); + } + + public ExtendedSearchIntegrator getExtendedSearchFactoryIntegrator() { + return extendedIntegrator; + } + + public String[] getDirtyPropertyNames(PostUpdateEvent event) { + EntityPersister persister = event.getPersister(); + final int[] dirtyProperties = event.getDirtyProperties(); + if ( dirtyProperties != null && dirtyProperties.length > 0 ) { + String[] propertyNames = persister.getPropertyNames(); + int length = dirtyProperties.length; + String[] dirtyPropertyNames = new String[length]; + for ( int i = 0; i < length; i++ ) { + dirtyPropertyNames[i] = propertyNames[dirtyProperties[i]]; + } + return dirtyPropertyNames; + } + else { + return null; + } + } + + /** + * Initialize method called by Hibernate Core when the SessionFactory starts + */ + public void initialize(ExtendedSearchIntegrator extendedIntegrator) { + this.extendedIntegrator = extendedIntegrator; + + if ( !disabled ) { + skipDirtyChecks = !extendedIntegrator.isDirtyChecksEnabled(); + LOGGER.fine( "Hibernate Search dirty checks " + (skipDirtyChecks ? "disabled" : "enabled") ); + } + } + + protected void processWork(Object entity, + Serializable id, + WorkType workType, + AbstractEvent event, + boolean identifierRollbackEnabled) { + Work work = new Work( entity, id, workType, identifierRollbackEnabled ); + final EventSourceTransactionContext transactionContext = new EventSourceTransactionContext( event.getSession() ); + extendedIntegrator.getWorker().performWork( work, transactionContext ); + } + + protected void processCollectionEvent(AbstractCollectionEvent event) { + if ( disabled ) { + return; + } + + Object entity = event.getAffectedOwnerOrNull(); + if ( entity == null ) { + //Hibernate cannot determine every single time the owner especially in case detached objects are involved + // or property-ref is used + //Should log really but we don't know if we're interested in this collection for indexing + return; + } + PersistentCollection persistentCollection = event.getCollection(); + final String collectionRole; + if ( persistentCollection != null ) { + if ( !persistentCollection.wasInitialized() ) { + // non-initialized collections will still trigger events, but we want to skip them + // as they won't contain new values affecting the index state + return; + } + collectionRole = persistentCollection.getRole(); + } + else { + collectionRole = null; + } + AbstractDocumentBuilder documentBuilder = getDocumentBuilder( entity ); + + if ( documentBuilder != null && documentBuilder.collectionChangeRequiresIndexUpdate( collectionRole ) ) { + Serializable id = getId( entity, event ); + if ( id == null ) { + LOGGER.fine( "id could not be extracted :" + event.getAffectedOwnerEntityName() ); + return; + } + processWork( entity, id, WorkType.COLLECTION, event, false ); + } + } + + private Serializable getId(Object entity, AbstractCollectionEvent event) { + Serializable id = event.getAffectedOwnerIdOrNull(); + if ( id == null ) { + // most likely this recovery is unnecessary since Hibernate Core probably try that + EntityEntry entityEntry = event.getSession().getPersistenceContext().getEntry( entity ); + id = entityEntry == null ? null : entityEntry.getId(); + } + return id; + } + + private void writeObject(ObjectOutputStream os) throws IOException { + os.defaultWriteObject(); + } + + //needs to implement custom readObject to restore the transient fields + + private void readObject(ObjectInputStream is) + throws IOException, ClassNotFoundException, NoSuchFieldException, IllegalAccessException { + is.defaultReadObject(); + Class cl = HibernateUpdateSource.class; + Field f = cl.getDeclaredField( "flushSynch" ); + ReflectionHelper.setAccessible( f ); + Map flushSynch = Maps.createIdentityWeakKeyConcurrentMap( 64, 32 ); + // setting a final field by reflection during a readObject is considered as safe as in a constructor: + f.set( this, flushSynch ); + } + + /** + * It is not suggested to extend FullTextIndexEventListener, but when needed to implement special + * use cases implementors might need this method. If you have to extent this, please report + * your use case so that better long term solutions can be discussed. + * + * @param instance the object instance for which to retrieve the document builder + * + * @return the {@code DocumentBuilder} for the specified object + */ + protected AbstractDocumentBuilder getDocumentBuilder(final Object instance) { + Class clazz = instance.getClass(); + EntityIndexBinding entityIndexBinding = extendedIntegrator.getIndexBinding( clazz ); + if ( entityIndexBinding != null ) { + return entityIndexBinding.getDocumentBuilder(); + } + else { + return extendedIntegrator.getDocumentBuilderContainedEntity( clazz ); + } + } + + /** + * Required since Hibernate ORM 4.3 + */ + public boolean requiresPostCommitHanding(EntityPersister persister) { + // TODO Tests seem to pass using _false_ but we might be able to take + // advantage of this new hook? + return false; + } + + @Override + public void close() { + + } +} diff --git a/jpa/src/main/java/org/hibernate/search/genericjpa/impl/JPASearchFactoryAdapter.java b/jpa/src/main/java/org/hibernate/search/genericjpa/impl/JPASearchFactoryAdapter.java index 7c820b6..c874d84 100644 --- a/jpa/src/main/java/org/hibernate/search/genericjpa/impl/JPASearchFactoryAdapter.java +++ b/jpa/src/main/java/org/hibernate/search/genericjpa/impl/JPASearchFactoryAdapter.java @@ -164,6 +164,13 @@ public final void init() { this.searchIntegrator = impl.unwrap( ExtendedSearchIntegrator.class ); this.searchFactory = new StandaloneSearchFactoryImpl( this.searchIntegrator ); + JPAReusableEntityProvider entityProvider = new JPAReusableEntityProvider( + this.emf, + this.idProperties, + this.transactionManager, + this.customUpdateEntityProviders + ); + this.asyncUpdateSource = this.asyncUpdateSourceProvider.getUpdateSource( this.updateDelay, TimeUnit.MILLISECONDS, @@ -173,22 +180,16 @@ public final void init() { this.transactionManager ); if ( this.asyncUpdateSource != null ) { + this.indexUpdater = new IndexUpdater( + this.rehashedTypeMetadataForIndexRoot, this.containedInIndexOf, entityProvider, + impl.unwrap( ExtendedSearchIntegrator.class ) + ); //TODO: we could allow this, but then we would need to change //the way we get the entityProvider. it's safest to keep it like this if ( this.emf == null ) { throw new AssertionFailure( "emf may not be null when using an AsyncUpdateSource" ); } - JPAReusableEntityProvider entityProvider = new JPAReusableEntityProvider( - this.emf, - this.idProperties, - this.transactionManager, - this.customUpdateEntityProviders - ); - this.indexUpdater = new IndexUpdater( - this.rehashedTypeMetadataForIndexRoot, this.containedInIndexOf, entityProvider, - impl.unwrap( ExtendedSearchIntegrator.class ) - ); this.asyncUpdateSource.setUpdateConsumers( Arrays.asList( this.indexUpdater::updateEvent, this @@ -197,7 +198,7 @@ public final void init() { this.asyncUpdateSource.start(); } this.synchronizedUpdateSource = this.synchronizedUpdateSourceProvider.getUpdateSource( - this, + impl.unwrap( ExtendedSearchIntegrator.class ), this.rehashedTypeMetadataForIndexRoot, this.containedInIndexOf, this.properties, @@ -365,7 +366,7 @@ public void close() { if ( this.asyncUpdateSource != null ) { this.asyncUpdateSource.stop(); } - if ( this.synchronizedUpdateSource != null) { + if ( this.synchronizedUpdateSource != null ) { this.synchronizedUpdateSource.close(); } if ( this.indexUpdater != null ) { diff --git a/jpa/src/main/java/org/hibernate/search/genericjpa/impl/SynchronizedUpdateSourceProvider.java b/jpa/src/main/java/org/hibernate/search/genericjpa/impl/SynchronizedUpdateSourceProvider.java index 88ae5ef..0873c6e 100644 --- a/jpa/src/main/java/org/hibernate/search/genericjpa/impl/SynchronizedUpdateSourceProvider.java +++ b/jpa/src/main/java/org/hibernate/search/genericjpa/impl/SynchronizedUpdateSourceProvider.java @@ -13,6 +13,7 @@ import java.util.Properties; import java.util.Set; +import org.hibernate.search.engine.integration.impl.ExtendedSearchIntegrator; import org.hibernate.search.genericjpa.JPASearchFactoryController; import org.hibernate.search.genericjpa.db.events.index.impl.IndexUpdater; import org.hibernate.search.genericjpa.events.impl.SynchronizedUpdateSource; @@ -24,7 +25,7 @@ public interface SynchronizedUpdateSourceProvider { SynchronizedUpdateSource getUpdateSource( - JPASearchFactoryController searchFactoryController, + ExtendedSearchIntegrator searchIntegrator, Map, RehashedTypeMetadata> rehashedTypeMetadataPerIndexRoot, Map, List>> containedInIndexOf, Properties properties, diff --git a/jpa/src/test/java/org/hibernate/search/genericjpa/test/db/events/jpa/ManualUpdateIntegrationTest.java b/jpa/src/test/java/org/hibernate/search/genericjpa/test/db/events/jpa/ManualUpdateIntegrationTest.java index 7008fe0..dae9e8d 100644 --- a/jpa/src/test/java/org/hibernate/search/genericjpa/test/db/events/jpa/ManualUpdateIntegrationTest.java +++ b/jpa/src/test/java/org/hibernate/search/genericjpa/test/db/events/jpa/ManualUpdateIntegrationTest.java @@ -130,18 +130,16 @@ public void test() throws SQLException, InterruptedException { } this.deleteAllData( em ); Sleep.sleep( - 100_000, () -> { - return this.assertCount( impl, 0 ); - }, 100, "" + 100_000, () -> + this.assertCount( impl, 0 ) + , 100, "" ); this.writeAllIntoIndex( em, impl ); this.deleteAllData( em ); Sleep.sleep( - 100_000, () -> { - return this.assertCount( impl, 0 ); - }, 100, "" + 100_000, () -> this.assertCount( impl, 0 ), 100, "" ); this.writeAllIntoIndex( em, impl ); @@ -165,10 +163,9 @@ public void test() throws SQLException, InterruptedException { } Sleep.sleep( 100_000, - () -> { - return this.queryPlaceIds( impl, "name", "Valinor" ) - .size() == 0 && this.assertCount( impl, 2 ); - }, + () -> this.queryPlaceIds( impl, "name", "Valinor" ) + .size() == 0 && this.assertCount( impl, 2 ) + , 100, "shouldn't have found \"Valinor\" in the index anymore, but overall count should have been equal to 2!" ); @@ -187,19 +184,18 @@ public void test() throws SQLException, InterruptedException { impl, "sorcerers.name", oldName - ) - .size() + ).size() ); someSorcerer.setName( "Odalbert" ); tx.commit(); } Sleep.sleep( 100_000, - () -> { - return this.queryPlaceIds( impl, "sorcerers.name", oldName ) - .size() == 0 && this - .assertCount( impl, 2 ); - }, 100, + () -> + this.queryPlaceIds( impl, "sorcerers.name", oldName ) + .size() == 0 && this + .assertCount( impl, 2 ) + , 100, "shouldn't have found \"" + oldName + "\" in the index anymore, but overall count should have been equal to 2!" ); } @@ -230,9 +226,7 @@ private void writeAllIntoIndex(EntityManager em, ExtendedSearchIntegrator impl) this.setupData( em ); // wait a bit until the AsyncUpdateSource sent the appropriate events Sleep.sleep( - 100_000, () -> { - return this.assertCount( impl, 2 ); - }, 100, "" + 100_000, () -> this.assertCount( impl, 2 ), 100, "" ); } @@ -255,9 +249,7 @@ private List queryPlaceIds(ExtendedSearchIntegrator impl, String field, .createQuery() ) .queryEntityInfos().stream().map( - (entInfo) -> { - return (Integer) entInfo.getId(); - } + (entInfo) -> (Integer) entInfo.getId() ).collect( Collectors.toList() ); } diff --git a/jpa/src/test/java/org/hibernate/search/genericjpa/test/jpa/entities/Place.java b/jpa/src/test/java/org/hibernate/search/genericjpa/test/jpa/entities/Place.java index b358556..7942f32 100644 --- a/jpa/src/test/java/org/hibernate/search/genericjpa/test/jpa/entities/Place.java +++ b/jpa/src/test/java/org/hibernate/search/genericjpa/test/jpa/entities/Place.java @@ -90,4 +90,24 @@ public String toString() { return "Place [id=" + this.getId() + ", name=" + this.getName() + ", sorcerers=" + sorcerers + "]"; } + @Override + public boolean equals(Object o) { + if ( this == o ) { + return true; + } + if ( o == null || getClass() != o.getClass() ) { + return false; + } + + Place place = (Place) o; + + return !(id != null ? !id.equals( place.id ) : place.id != null); + + } + + @Override + public int hashCode() { + return id != null ? id.hashCode() : 0; + } + } diff --git a/jpa/src/test/java/org/hibernate/search/genericjpa/test/jpa/entities/Sorcerer.java b/jpa/src/test/java/org/hibernate/search/genericjpa/test/jpa/entities/Sorcerer.java index c9f2bd2..e73a835 100644 --- a/jpa/src/test/java/org/hibernate/search/genericjpa/test/jpa/entities/Sorcerer.java +++ b/jpa/src/test/java/org/hibernate/search/genericjpa/test/jpa/entities/Sorcerer.java @@ -82,4 +82,24 @@ public void setPlace(Place place) { this.place = place; } + @Override + public boolean equals(Object o) { + if ( this == o ) { + return true; + } + if ( o == null || getClass() != o.getClass() ) { + return false; + } + + Sorcerer sorcerer = (Sorcerer) o; + + return !(id != null ? !id.equals( sorcerer.id ) : sorcerer.id != null); + + } + + @Override + public int hashCode() { + return id != null ? id.hashCode() : 0; + } + } diff --git a/jpa/src/test/java/org/hibernate/search/genericjpa/test/jpa/mysql/MySQLHibernateAutomaticUpdatesIntegrationTest.java b/jpa/src/test/java/org/hibernate/search/genericjpa/test/jpa/mysql/MySQLHibernateAutomaticUpdatesIntegrationTest.java new file mode 100644 index 0000000..ed65200 --- /dev/null +++ b/jpa/src/test/java/org/hibernate/search/genericjpa/test/jpa/mysql/MySQLHibernateAutomaticUpdatesIntegrationTest.java @@ -0,0 +1,238 @@ +/* + * Hibernate Search, full-text search for your domain model + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.search.genericjpa.test.jpa.mysql; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Properties; + +import org.apache.lucene.search.MatchAllDocsQuery; + +import org.hibernate.search.genericjpa.Constants; +import org.hibernate.search.genericjpa.Setup; +import org.hibernate.search.genericjpa.db.events.triggers.MySQLTriggerSQLStringSource; +import org.hibernate.search.genericjpa.impl.JPASearchFactoryAdapter; +import org.hibernate.search.genericjpa.test.jpa.AutomaticUpdatesIntegrationTest; +import org.hibernate.search.genericjpa.test.jpa.entities.ID; +import org.hibernate.search.genericjpa.test.jpa.entities.MultipleColumnsIdEntity; +import org.hibernate.search.genericjpa.test.jpa.entities.NonJPAEntity; +import org.hibernate.search.genericjpa.test.jpa.entities.Place; +import org.hibernate.search.genericjpa.test.jpa.entities.Sorcerer; +import org.hibernate.search.jpa.FullTextEntityManager; + +import org.junit.Before; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +/** + * Created by Martin on 28.07.2015. + */ +public class MySQLHibernateAutomaticUpdatesIntegrationTest extends AutomaticUpdatesIntegrationTest { + + @Before + public void setup() { + this.setup( "hibernate", "Hibernate_MySQL", MySQLTriggerSQLStringSource.class ); + } + + //TODO: test this for object hierarchies + + @Test + public void testNativeEvents() { + if ( "sql".equals( this.searchFactoryType ) ) { + System.out.println( "skipping native event test for searchFactoryType (useless for this type): " + this.searchFactoryType ); + return; + } + Properties properties = new Properties(); + properties.setProperty( Constants.SEARCH_FACTORY_NAME_KEY, "testCustomUpdatedEntity" ); + properties.setProperty( Constants.ADDITIONAL_INDEXED_TYPES_KEY, NonJPAEntity.class.getName() ); + //we do manual updates, so this will be ignored, but let's keep it here + //if we change our mind later + properties.setProperty( + Constants.TRIGGER_SOURCE_KEY, + this.triggerSourceClass.getName() + ); + properties.setProperty( + Constants.TRIGGER_CREATION_STRATEGY_KEY, + Constants.TRIGGER_CREATION_STRATEGY_DROP_CREATE + ); + properties.setProperty( Constants.BATCH_SIZE_FOR_UPDATES_KEY, "2" ); + properties.setProperty( Constants.SEARCH_FACTORY_TYPE_KEY, this.searchFactoryType ); + JPASearchFactoryAdapter searchFactory = (JPASearchFactoryAdapter) Setup.createSearchFactoryController( + this.emf, + properties + ); + try { + for ( int times = 0; times < 100; ++times ) { + this.em.getTransaction().begin(); + for ( int i = 0; i < 5; ++i ) { + MultipleColumnsIdEntity ent = new MultipleColumnsIdEntity(); + ent.setFirstId( "first" + i ); + ent.setSecondId( "second" + i ); + ent.setInfo( "info" + i ); + this.em.persist( ent ); + this.em.flush(); + } + this.em.getTransaction().rollback(); + } + assertEquals( + 0, searchFactory.getFullTextEntityManager( this.em ) + .createFullTextQuery( new MatchAllDocsQuery(), MultipleColumnsIdEntity.class ) + .getResultSize() + ); + + { + this.em.getTransaction().begin(); + MultipleColumnsIdEntity ent = new MultipleColumnsIdEntity(); + ent.setFirstId( "first" ); + ent.setSecondId( "second" ); + ent.setInfo( "info" ); + this.em.persist( ent ); + this.em.flush(); + this.em.getTransaction().commit(); + + assertEquals( + 1, searchFactory.getFullTextEntityManager( this.em ) + .createFullTextQuery( new MatchAllDocsQuery(), MultipleColumnsIdEntity.class ) + .getResultSize() + ); + } + + { + this.em.getTransaction().begin(); + MultipleColumnsIdEntity ent = this.em.find( + MultipleColumnsIdEntity.class, new ID( + "first", + "second" + ) + ); + ent.setInfo( "info_new" ); + this.em.getTransaction().commit(); + + assertEquals( + 1, searchFactory.getFullTextEntityManager( this.em ) + .createFullTextQuery( new MatchAllDocsQuery(), MultipleColumnsIdEntity.class ) + .getResultSize() + ); + + FullTextEntityManager fem = searchFactory.getFullTextEntityManager( this.em ); + + assertEquals( + 1, fem.createFullTextQuery( + fem.getSearchFactory().buildQueryBuilder().forEntity( + MultipleColumnsIdEntity.class + ).get().keyword().onField( "info" ).matching( "info_new" ).createQuery(), + MultipleColumnsIdEntity.class + ).getResultSize() + ); + } + + { + this.em.getTransaction().begin(); + MultipleColumnsIdEntity ent = this.em.find( + MultipleColumnsIdEntity.class, new ID( + "first", + "second" + ) + ); + this.em.remove( ent ); + this.em.flush(); + this.em.getTransaction().commit(); + + assertEquals( + 0, searchFactory.getFullTextEntityManager( this.em ) + .createFullTextQuery( new MatchAllDocsQuery(), MultipleColumnsIdEntity.class ) + .getResultSize() + ); + } + + { + this.em.getTransaction().begin(); + Place place = new Place(); + place.setCool( true ); + place.setName( "name" ); + Sorcerer sorcerer = new Sorcerer(); + sorcerer.setName( "sorcname" ); + sorcerer.setPlace( place ); + place.setSorcerers( new HashSet<>( Arrays.asList( sorcerer ) ) ); + this.em.persist( place ); + this.em.flush(); + this.em.getTransaction().commit(); + + assertEquals( + 1, searchFactory.getFullTextEntityManager( this.em ) + .createFullTextQuery( new MatchAllDocsQuery(), Place.class ) + .getResultSize() + ); + } + + { + this.em.getTransaction().begin(); + Sorcerer sorc = (Sorcerer) this.em.createQuery( "SELECT a FROM Sorcerer a" ).getResultList().get( 0 ); + sorc.setName( "newname" ); + this.em.getTransaction().commit(); + + assertEquals( + 1, searchFactory.getFullTextEntityManager( this.em ) + .createFullTextQuery( new MatchAllDocsQuery(), Place.class ) + .getResultSize() + ); + + FullTextEntityManager fem = searchFactory.getFullTextEntityManager( this.em ); + + assertEquals( + 1, fem.createFullTextQuery( + fem.getSearchFactory() + .buildQueryBuilder() + .forEntity( Place.class ) + .get() + .keyword() + .onField( "sorcerers.name" ) + .matching( "newname" ).createQuery(), Place.class + ).getResultSize() + ); + + assertEquals( + 0, fem.createFullTextQuery( + fem.getSearchFactory() + .buildQueryBuilder() + .forEntity( Place.class ) + .get() + .keyword() + .onField( "sorcerers.name" ) + .matching( "sorcname" ).createQuery(), Place.class + ).getResultSize() + ); + } + + { + this.em.getTransaction().begin(); + Sorcerer sorc = (Sorcerer) this.em.createQuery( "SELECT a FROM Sorcerer a" ).getResultList().get( 0 ); + sorc.setName( "sorcname" ); + this.em.flush(); + this.em.getTransaction().rollback(); + + FullTextEntityManager fem = searchFactory.getFullTextEntityManager( this.em ); + + assertEquals( + 0, fem.createFullTextQuery( + fem.getSearchFactory() + .buildQueryBuilder() + .forEntity( Place.class ) + .get() + .keyword() + .onField( "sorcerers.name" ) + .matching( "sorcname" ).createQuery(), Place.class + ).getResultSize() + ); + } + } + finally { + searchFactory.close(); + } + } +} diff --git a/jpa/src/test/java/org/hibernate/search/genericjpa/test/jpa/mysql/MySQLNativeEclipseLinkAutomaticUpdatesIntegrationTest.java b/jpa/src/test/java/org/hibernate/search/genericjpa/test/jpa/mysql/MySQLNativeEclipseLinkAutomaticUpdatesIntegrationTest.java index cc42a34..ba3f064 100644 --- a/jpa/src/test/java/org/hibernate/search/genericjpa/test/jpa/mysql/MySQLNativeEclipseLinkAutomaticUpdatesIntegrationTest.java +++ b/jpa/src/test/java/org/hibernate/search/genericjpa/test/jpa/mysql/MySQLNativeEclipseLinkAutomaticUpdatesIntegrationTest.java @@ -6,6 +6,8 @@ */ package org.hibernate.search.genericjpa.test.jpa.mysql; +import java.util.Arrays; +import java.util.HashSet; import java.util.Properties; import org.apache.lucene.search.MatchAllDocsQuery; @@ -15,10 +17,11 @@ import org.hibernate.search.genericjpa.db.events.triggers.MySQLTriggerSQLStringSource; import org.hibernate.search.genericjpa.impl.JPASearchFactoryAdapter; import org.hibernate.search.genericjpa.test.jpa.AutomaticUpdatesIntegrationTest; -import org.hibernate.search.genericjpa.test.jpa.entities.CustomUpdatedEntity; import org.hibernate.search.genericjpa.test.jpa.entities.ID; import org.hibernate.search.genericjpa.test.jpa.entities.MultipleColumnsIdEntity; import org.hibernate.search.genericjpa.test.jpa.entities.NonJPAEntity; +import org.hibernate.search.genericjpa.test.jpa.entities.Place; +import org.hibernate.search.genericjpa.test.jpa.entities.Sorcerer; import org.hibernate.search.jpa.FullTextEntityManager; import org.junit.Before; @@ -41,7 +44,7 @@ public void setup() { @Test public void testNativeEvents() { if ( "sql".equals( this.searchFactoryType ) ) { - System.out.println( "skipping rollback test for searchFactoryType (useless for this type): " + this.searchFactoryType ); + System.out.println( "skipping native event test for searchFactoryType (useless for this type): " + this.searchFactoryType ); return; } Properties properties = new Properties(); @@ -143,6 +146,85 @@ MultipleColumnsIdEntity.class, new ID( .getResultSize() ); } + + { + this.em.getTransaction().begin(); + Place place = new Place(); + place.setCool( true ); + place.setName( "name" ); + Sorcerer sorcerer = new Sorcerer(); + sorcerer.setName( "sorcname" ); + sorcerer.setPlace( place ); + place.setSorcerers( new HashSet<>( Arrays.asList( sorcerer ) ) ); + this.em.persist( place ); + this.em.getTransaction().commit(); + + assertEquals( + 1, searchFactory.getFullTextEntityManager( this.em ) + .createFullTextQuery( new MatchAllDocsQuery(), Place.class ) + .getResultSize() + ); + } + + { + this.em.getTransaction().begin(); + Sorcerer sorc = (Sorcerer) this.em.createQuery( "SELECT a FROM Sorcerer a" ).getResultList().get( 0 ); + sorc.setName( "newname" ); + this.em.getTransaction().commit(); + + assertEquals( + 1, searchFactory.getFullTextEntityManager( this.em ) + .createFullTextQuery( new MatchAllDocsQuery(), Place.class ) + .getResultSize() + ); + + FullTextEntityManager fem = searchFactory.getFullTextEntityManager( this.em ); + + assertEquals( + 1, fem.createFullTextQuery( + fem.getSearchFactory() + .buildQueryBuilder() + .forEntity( Place.class ) + .get() + .keyword() + .onField( "sorcerers.name" ) + .matching( "newname" ).createQuery(), Place.class + ).getResultSize() + ); + + assertEquals( + 0, fem.createFullTextQuery( + fem.getSearchFactory() + .buildQueryBuilder() + .forEntity( Place.class ) + .get() + .keyword() + .onField( "sorcerers.name" ) + .matching( "sorcname" ).createQuery(), Place.class + ).getResultSize() + ); + } + + { + this.em.getTransaction().begin(); + Sorcerer sorc = (Sorcerer) this.em.createQuery( "SELECT a FROM Sorcerer a" ).getResultList().get( 0 ); + sorc.setName( "sorcname" ); + this.em.getTransaction().rollback(); + + FullTextEntityManager fem = searchFactory.getFullTextEntityManager( this.em ); + + assertEquals( + 0, fem.createFullTextQuery( + fem.getSearchFactory() + .buildQueryBuilder() + .forEntity( Place.class ) + .get() + .keyword() + .onField( "sorcerers.name" ) + .matching( "sorcname" ).createQuery(), Place.class + ).getResultSize() + ); + } } finally { searchFactory.close(); diff --git a/jpa/src/test/resources/META-INF/persistence.xml b/jpa/src/test/resources/META-INF/persistence.xml index 9c14b19..7bf5a04 100644 --- a/jpa/src/test/resources/META-INF/persistence.xml +++ b/jpa/src/test/resources/META-INF/persistence.xml @@ -45,6 +45,25 @@ + + org.hibernate.jpa.HibernatePersistenceProvider + org.hibernate.search.genericjpa.test.jpa.entities.Sorcerer + org.hibernate.search.genericjpa.test.jpa.entities.Place + + org.hibernate.search.genericjpa.test.jpa.entities.CustomUpdatedEntity + + org.hibernate.search.genericjpa.test.jpa.entities.MultipleColumnsIdEntity + + + + + + + + + + org.eclipse.persistence.jpa.PersistenceProvider diff --git a/pom.xml b/pom.xml index 7396270..46abad7 100644 --- a/pom.xml +++ b/pom.xml @@ -100,6 +100,13 @@ test + + org.hibernate + hibernate-entitymanager + 4.3.9.Final + test + + com.squareup