how to use JPA life-cycle events to get entity data
Asked Answered
S

3

9

I have a RESTful API that makes use of an entity class annotated with @EntityListners. And in the EntityListner.java, I have a method annotated with @PostPersist. So, when that event fires, I want to extract all the information regarding the entity that just got persisted to the database. But when I try to do that, Glassfish is generating an exception and the method in EntityListner class is not executing as expected. Here is the code

public class EntityListner {
private final static String QUEUE_NAME = "customer";
@PostUpdate
@PostPersist
public void notifyOther(Customer entity){
    CustomerFacadeREST custFacade = new CustomerFacadeREST(); 
    Integer customerId = entity.getCustomerId();
    String custData = custFacade.find(customerId).toString();
    String successMessage = "Entity added to server";
    try{
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("localhost");
        Connection connection = factory.newConnection();
        Channel channel = connection.createChannel();

        channel.queueDeclare(QUEUE_NAME, false, false, false, null);
      //  channel.basicPublish("", QUEUE_NAME, null, successMessage .getBytes()); 
        channel.basicPublish("", QUEUE_NAME, null, custData.getBytes());  
        channel.close();
        connection.close();


    }
    catch(IOException ex){

    }
    finally{

    }
  }    
} 

If I send the commented out successMessage message instead of custData, everything works fine.

http://www.objectdb.com/java/jpa/persistence/event says the following regarding the entity lifecycle methods, and I am wondering if that is the situation here.

To avoid conflicts with the original database operation that fires the entity lifecycle event (which is still in progress) callback methods should not call EntityMan­ager or Query methods and should not access any other entity objects

Any ideas?

Synovitis answered 28/10, 2012 at 8:19 Comment(2)
What is the exception that you are getting?Wiltz
Include the exception and the stack trace.Kaminsky
S
9

As that paragraph says, the standard does not support calling entity manager methods from inside entity listeners. I strongly recommend building custData from the persisted entity, as Heiko Rupp says in his answer. If that is not feasible, consider:

  • notifying asynchronously. I do not really recommend this as it probably depends on timing to work properly:
public class EntityListener {
    private final static String QUEUE_NAME = "customer";

    private ScheduledExecutorService getExecutorService() {
        // get asynchronous executor service from somewhere
        // you will most likely need a ScheduledExecutorService
        // instance, in order to schedule notification with
        // some delay. Alternatively, you could try Thread.sleep(...)
        // before notifying, but that is ugly.
    }

    private void doNotifyOtherInNewTransaction(Customer entity) {
        // For all this to work correctly,
        // you should execute your notification
        // inside a new transaction. You might
        // find it easier to do this declaratively
        // by invoking some method demarcated
        // with REQUIRES_NEW
        try {
            // (begin transaction)
            doNotifyOther(entity);
            // (commit transaction)
        } catch (Exception ex) {
            // (rollback transaction)
        }
    }

    @PostUpdate
    @PostPersist
    public void notifyOther(final Customer entity) {
        ScheduledExecutorService executor = getExecutorService();
        // This is the "raw" version
        // Most probably you will need to call
        // executor.schedule and specify a delay,
        // in order to give the old transaction some time
        // to flush and commit
        executor.execute(new Runnable() {
            @Override
            public void run() {
                doNotifyOtherInNewTransaction(entity);
            }
        });
    }

    // This is exactly as your original code
    public void doNotifyOther(Customer entity) {
        CustomerFacadeREST custFacade = new CustomerFacadeREST(); 
        Integer customerId = entity.getCustomerId();
        String custData = custFacade.find(customerId).toString();
        String successMessage = "Entity added to server";
        try {
            ConnectionFactory factory = new ConnectionFactory();
            factory.setHost("localhost");
            Connection connection = factory.newConnection();
            Channel channel = connection.createChannel();
            channel.queueDeclare(QUEUE_NAME, false, false, false, null);
            channel.basicPublish("", QUEUE_NAME, null, custData.getBytes());  
            channel.close();
            connection.close();
        }
        catch(IOException ex){
        }
        finally {
        }
    }    
} 
  • registering some post-commit trigger (my recommendation if Heilo Rupp answer is not feasible). This is not timing dependant because it is guaranteed to execute after you have flushed to database. Furthermore, it has the added benefit that you don't notify if you end up rolling back your transaction. The way to do this depends on what you are using for transaction management, but basically you create an instance of some particular instance and then register it in some registry. For example, with JTA it would be:
public class EntityListener {
    private final static String QUEUE_NAME = "customer";

    private Transaction getTransaction() {
        // get current JTA transaction reference from somewhere
    }

    private void doNotifyOtherInNewTransaction(Customer entity) {
        // For all this to work correctly,
        // you should execute your notification
        // inside a new transaction. You might
        // find it easier to do this declaratively
        // by invoking some method demarcated
        // with REQUIRES_NEW
        try {
            // (begin transaction)
            doNotifyOther(entity);
            // (commit transaction)
         } catch (Exception ex) {
            // (rollback transaction)
         }
    }

    @PostUpdate
    @PostPersist
    public void notifyOther(final Customer entity) {
        Transaction transaction = getTransaction();
        transaction.registerSynchronization(new Synchronization() {
            @Override
            public void beforeCompletion() { }

            @Override
            public void afterCompletion(int status) {
                if (status == Status.STATUS_COMMITTED) {
                    doNotifyOtherInNewTransaction(entity);
                }
            }
        });             
    }

    // This is exactly as your original code
    public void doNotifyOther(Customer entity) {
        CustomerFacadeREST custFacade = new CustomerFacadeREST(); 
        Integer customerId = entity.getCustomerId();
        String custData = custFacade.find(customerId).toString();
        String successMessage = "Entity added to server";
        try {
            ConnectionFactory factory = new ConnectionFactory();
            factory.setHost("localhost");
            Connection connection = factory.newConnection();
            Channel channel = connection.createChannel();
            channel.queueDeclare(QUEUE_NAME, false, false, false, null);
            channel.basicPublish("", QUEUE_NAME, null, custData.getBytes());  
            channel.close();
            connection.close();
        }
        catch(IOException ex){
        }
        finally {
        }
    }    
}

If you are using Spring transactions, the code will be very similar, with just some class name changes.

Some pointers:

Shampoo answered 18/12, 2012 at 20:45 Comment(0)
W
3

I guess you may be seeing a NPE, as you may be violating the paragraph you were citing:

String custData = custFacade.find(customerId).toString();

The find seems to implicitly querying for the object (as you describe), which may not be fully synced to the database and thus not yet accessible.

Wiltz answered 28/10, 2012 at 8:25 Comment(3)
Thanks @Heiko; yea, I am getting NPE. I added Thread.sleep(2000) to my code to give the database enough time to sync but to no avail. So, is there a work around?Synovitis
Couldn't you just use the customer object that was passed in?Wiltz
I want to read back the Json representation of the argument that Jersey used to create it. If I use the the argument that I send as is, I merely get its ID.Synovitis
C
1

In his answer, gpeche noted that it's fairly straightforward to translate his option #2 into Spring. To save others the trouble of doing that:

package myapp.entity.listener;

import javax.persistence.PostPersist;
import javax.persistence.PostUpdate;
import org.springframework.transaction.support.TransactionSynchronizationAdapter;
import org.springframework.transaction.support.TransactionSynchronizationManager;
import myapp.util.ApplicationContextProvider;
import myapp.entity.NetScalerServer;
import myapp.service.LoadBalancerService;

public class NetScalerServerListener {

    @PostPersist
    @PostUpdate
    public void postSave(final NetScalerServer server) {
        TransactionSynchronizationManager.registerSynchronization(
            new TransactionSynchronizationAdapter() {

                @Override
                public void afterCommit() { postSaveInNewTransaction(server); }
            });
    }

    private void postSaveInNewTransaction(NetScalerServer server) {
        ApplicationContext appContext =
            ApplicationContextProvider.getApplicationContext();
        LoadBalancer lbService = appContext.getBean(LoadBalancerService.class);
        lbService.updateEndpoints(server);
    }
}

The service method (here, updateEndpoints()) can use the JPA EntityManager (in my case, to issue queries and update entities) without any issue. Be sure to annotate the updateEndpoints() method with @Transaction(propagation = Propagation.REQUIRES_NEW) to ensure that there's a new transaction to perform the persistence operations.

Not directly related to the question, but ApplicationContextProvider is just a custom class to return an app context since JPA 2.0 entity listeners aren't managed components, and I'm too lazy to use @Configurable here. Here it is for completeness:

package myapp.util;

import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;

public class ApplicationContextProvider implements ApplicationContextAware {
    private static ApplicationContext applicationContext;

    public static ApplicationContext getApplicationContext() {
        return applicationContext;
    }

    @Override
    public void setApplicationContext(ApplicationContext appContext)
            throws BeansException {

        applicationContext = appContext;
    }
}
Case answered 13/5, 2014 at 1:41 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.