11 Replies Latest reply on Apr 15, 2010 1:28 AM by Dan Allen

    Transaction for normal beans

    Adam Warski Master

      Hello,


      I searched the forum for this but unless I'm missing something obvious it's not possible to search only the weld forum so it's pretty hard to find useful information ... anyway:


      How can I make sure that every method invoked on a normal (not EJB3) bean is surrounded by a transaction?


      So far I have found two solutions:
      - make the bean an EJB3 bean
      - write an interceptor, interceptor binding and annotate all beans with the annotation


      --
      Adam

        • 2. Re: Transaction for normal beans
          Adam Warski Master

          But I wanted to have @TransactionRequired (or equivalent) by default, like in EJB3 or Seam. But looks like it's not possible. Well, maybe except writing a Filter, though I was wondering if there's a CDI-solution.


          Adam

          • 3. Re: Transaction for normal beans
            Pete Muir Master

            Adam, want to help out on the Persistence/Transactions module for Seam 3 where we will implement this sort of thing? Email me if you are interested.

            • 4. Re: Transaction for normal beans
              Adam Warski Master

              Sure, why not :)


              So far I found a problem with the Filter approach, namely that if an extended persistence context is used (e.g. in long-running conversations and postbacks), the EM doesn's join the transaction; moreover if I inject the EM in the filter, it's a different one, so callling joinTransaction there has no effect.


              I investigated a bit and found it's normally done with a PhaseListener. And I think this won't change much in Seam3? (At least according to http://seamframework.org/Documentation/Seam3FacesModuleDesignNotes).


              Looked in the repo, but I guess there's not much there yet :).


              Adam

              • 5. Re: Transaction for normal beans
                Stuart Douglas Master

                The old ManagedPeristenceContext used the manager component pattern with @Unwrap to manage the entityManager. Every time you injected the entityManager it would check to make sure that the em was enlisted in the transaction.



                I am not sure what the best way to do something similar is in weld, possibly decorate the EM with a decorator that makes sure it is enlisted every time?

                • 6. Re: Transaction for normal beans
                  Adam Warski Master

                  I see. Weld has a different injection model and I'm not sure @Unwrap can be easily simulated. But here's what I have that seems to work fine (so far at least :) ):


                  1. Transactions are started/stopped in a phase listener. There's 1 tx for a GET and 2 txs for a POST (one thing I haven't yet checked is how this behaves with AJAX):


                  package util;
                  
                  import javax.faces.FacesException;
                  import javax.faces.context.FacesContext;
                  import javax.faces.event.*;
                  import javax.naming.InitialContext;
                  import javax.naming.NamingException;
                  import javax.transaction.Status;
                  import javax.transaction.UserTransaction;
                  
                  /**
                   * 1 transaction for GET: (before RESTORE_VIEW, after RENDER_RESPONSE)
                   * 2 transactions for POST: (before RESTORE_VIEW, after INVOKE_APPLICATION), (before RENDER_RESPONSE, after RENDER_RESPONSE)
                   * @author Adam Warski (adam at warski dot org)
                   */
                  public class TransactionPhaseListener implements PhaseListener {
                      @Override
                      public void beforePhase(PhaseEvent event) {
                          // Always starting before RESTORE_VIEW
                          if (PhaseId.RESTORE_VIEW.equals(event.getPhaseId())) {
                              startTransaction(event.getFacesContext());
                          }
                  
                          // Starting before RENDER_RESPONSE in case this is not a postback
                          if (PhaseId.RENDER_RESPONSE.equals(event.getPhaseId()) && !event.getFacesContext().isPostback()) {
                              startTransaction(event.getFacesContext());
                          }
                      }
                  
                      @Override
                      public void afterPhase(PhaseEvent event) {
                          // Always commiting after RENDER_RESPONSE
                          if (PhaseId.RENDER_RESPONSE.equals(event.getPhaseId())) {
                              commitTransaction(event.getFacesContext());
                          }
                  
                          // Commiting after INVOKE_APPLICATION in case of a postback
                          if (PhaseId.INVOKE_APPLICATION.equals(event.getPhaseId()) && event.getFacesContext().isPostback()) {
                              commitTransaction(event.getFacesContext());
                          }
                      }
                  
                      @Override
                      public PhaseId getPhaseId() {
                          return PhaseId.ANY_PHASE;
                      }
                  
                      private UserTransaction getUserTransaction() throws NamingException {
                          return (UserTransaction) new InitialContext().lookup("java:comp/UserTransaction");
                      }
                  
                      private static final String STARTED_TX_KEY = "_started_tx_";
                  
                      private void startTransaction(FacesContext facesContext) {
                          try {
                              UserTransaction utx = getUserTransaction();
                  
                              if (utx.getStatus() != Status.STATUS_ACTIVE) {
                                  utx.begin();
                                  facesContext.getAttributes().put(STARTED_TX_KEY, new Object());
                              }
                          } catch (RuntimeException e) {
                              throw e;
                          } catch (Exception e) {
                              throw new FacesException(e);
                          }
                      }
                  
                      private void commitTransaction(FacesContext facesContext) {
                          try {
                              UserTransaction utx = getUserTransaction();
                  
                              if (utx.getStatus() == Status.STATUS_ACTIVE && facesContext.getAttributes().containsKey(STARTED_TX_KEY)) {
                                  utx.commit();
                                  facesContext.getAttributes().remove(STARTED_TX_KEY);
                              }
                          } catch (RuntimeException e) {
                              throw e;
                          } catch (Exception e) {
                              throw new FacesException(e);
                          }
                      }
                  }
                  



                  2. You need to add the phase listener to faces-config.xml:


                  <lifecycle>
                          <phase-listener>util.TransactionPhaseListener</phase-listener>
                  </lifecycle>



                  3. The EntityManager is managed by a producer. There's a wrapper for the EM which ensures that the EM is enlisted in a transaction before any operation is done:



                  package util;
                  
                  import javax.enterprise.inject.Produces;
                  import javax.persistence.*;
                  import javax.persistence.criteria.CriteriaBuilder;
                  import javax.persistence.criteria.CriteriaQuery;
                  import javax.persistence.metamodel.Metamodel;
                  import java.util.Map;
                  
                  /**
                   * @author Adam Warski (adam at warski dot org)
                   */
                  public class EntityManagerProducer {
                      @PersistenceContext
                      private EntityManager entityManager;
                  
                      @Produces
                      public EntityManager getEntityManager() {
                          entityManager.joinTransaction();
                          return new EntityManagerTxWrapper(entityManager);
                      }
                  
                      public static class EntityManagerTxWrapper implements EntityManager {
                          private EntityManager delegate;
                  
                          private EntityManagerTxWrapper(EntityManager delegate) {
                              this.delegate = delegate;
                          }
                  
                          public void persist(Object entity) {
                              delegate.joinTransaction();
                              delegate.persist(entity);
                          }
                  
                          // etc. for all methods
                      }
                  }



                  4. You can then inject the EM freely:


                  @Inject private EntityManager em;



                  Adam


                  • 7. Re: Transaction for normal beans
                    Adam Warski Master

                    Of course the TX should be started before render response on a postback, not when the request is not a postback.


                    A corrected version below:


                    package util;
                    
                    import javax.faces.FacesException;
                    import javax.faces.context.FacesContext;
                    import javax.faces.event.*;
                    import javax.naming.InitialContext;
                    import javax.naming.NamingException;
                    import javax.transaction.Status;
                    import javax.transaction.UserTransaction;
                    
                    /**
                     * 1 transaction for GET: (before RESTORE_VIEW, after RENDER_RESPONSE)
                     * 2 transactions for POST: (before RESTORE_VIEW, after INVOKE_APPLICATION), (before RENDER_RESPONSE, after RENDER_RESPONSE)
                     * @author Adam Warski (adam at warski dot org)
                     */
                    public class TransactionPhaseListener implements PhaseListener {
                        @Override
                        public void beforePhase(PhaseEvent event) {
                            // Always starting before RESTORE_VIEW
                            if (PhaseId.RESTORE_VIEW.equals(event.getPhaseId())) {
                                startTransaction(event.getFacesContext());
                            }
                    
                            // Starting before RENDER_RESPONSE in case this is a postback
                            if (PhaseId.RENDER_RESPONSE.equals(event.getPhaseId()) && event.getFacesContext().isPostback()) {
                                startTransaction(event.getFacesContext());
                            }
                        }
                    
                        @Override
                        public void afterPhase(PhaseEvent event) {
                            // Always commiting after RENDER_RESPONSE
                            if (PhaseId.RENDER_RESPONSE.equals(event.getPhaseId())) {
                                commitTransaction(event.getFacesContext());
                            }
                    
                            // Commiting after INVOKE_APPLICATION in case of a postback
                            if (PhaseId.INVOKE_APPLICATION.equals(event.getPhaseId()) && event.getFacesContext().isPostback()) {
                                commitTransaction(event.getFacesContext());
                            }
                        }
                    
                        @Override
                        public PhaseId getPhaseId() {
                            return PhaseId.ANY_PHASE;
                        }
                    
                        private UserTransaction getUserTransaction() throws NamingException {
                            return (UserTransaction) new InitialContext().lookup("java:comp/UserTransaction");
                        }
                    
                        private static final String STARTED_TX_KEY = "_started_tx_";
                    
                        private void startTransaction(FacesContext facesContext) {
                            try {
                                UserTransaction utx = getUserTransaction();
                    
                                if (utx.getStatus() != Status.STATUS_ACTIVE) {
                                    utx.begin();
                                    facesContext.getAttributes().put(STARTED_TX_KEY, new Object());
                                }
                            } catch (RuntimeException e) {
                                throw e;
                            } catch (Exception e) {
                                throw new FacesException(e);
                            }
                        }
                    
                        private void commitTransaction(FacesContext facesContext) {
                            try {
                                UserTransaction utx = getUserTransaction();
                    
                                if (utx.getStatus() == Status.STATUS_ACTIVE && facesContext.getAttributes().containsKey(STARTED_TX_KEY)) {
                                    utx.commit();
                                    facesContext.getAttributes().remove(STARTED_TX_KEY);
                                }
                            } catch (RuntimeException e) {
                                throw e;
                            } catch (Exception e) {
                                throw new FacesException(e);
                            }
                        }
                    }
                    



                    Adam

                    • 8. Re: Transaction for normal beans
                      Pete Muir Master

                      Adam, this generally looks very good. I would comment that the entity manager enlister should probably be a decorator, not a producer as this applies the pattern to an existing entity manager (which the user will likely want to produce themselves to give themselves control over the unit name etc.)


                      I think you are working with Dan on getting this committed?

                      • 9. Re: Transaction for normal beans
                        Adam Warski Master

                        Pete Muir wrote on Apr 09, 2010 19:53:


                        Adam, this generally looks very good. I would comment that the entity manager enlister should probably be a decorator, not a producer as this applies the pattern to an existing entity manager (which the user will likely want to produce themselves to give themselves control over the unit name etc.)


                        Ah, right. However, it seems that if a bean is produced, decorators aren't applied to it? I tried this both with EntityManager and a simple Weld SE example, and it doesn't work:



                        public class EntityManagerProducer {
                            @PersistenceContext
                            private EntityManager entityManager;
                        
                            @Produces
                            public EntityManager getEntityManager() {
                                return entityManager;
                            }
                        }
                        




                        @Decorator
                        public class EntityManagerTransactionDecorator implements EntityManager {
                            @Inject
                            @Delegate
                            @Any
                            private EntityManager delegate;
                        
                            public void persist(Object entity) {
                                delegate.joinTransaction();
                                delegate.persist(entity);
                            }
                        
                            // ...
                        



                        Is it by specification? Are decorators only applied to top-level beans?


                        I already asked a similar question for interceptors, and it turned out that they can't be applied to produced beans.



                        I think you are working with Dan on getting this committed?


                        Yes, I'll contact him and work on this :)


                        Adam

                        • 10. Re: Transaction for normal beans
                          Pete Muir Master

                          Adam Warski wrote on Apr 11, 2010 17:44:



                          Pete Muir wrote on Apr 09, 2010 19:53:


                          Adam, this generally looks very good. I would comment that the entity manager enlister should probably be a decorator, not a producer as this applies the pattern to an existing entity manager (which the user will likely want to produce themselves to give themselves control over the unit name etc.)


                          Ah, right. However, it seems that if a bean is produced, decorators aren't applied to it? I tried this both with EntityManager and a simple Weld SE example, and it doesn't work:




                          Ah, yes, good point, so producer it is.

                          • 11. Re: Transaction for normal beans
                            Dan Allen Master

                            Adam Warski wrote on Apr 11, 2010 17:44:



                            Pete Muir wrote on Apr 09, 2010 19:53:


                            I think you are working with Dan on getting this committed?


                            Yes, I'll contact him and work on this :)

                            Adam


                            I'm on it. I'll pull in Lincoln as well since he is Mr. Faces ;)