2 Replies Latest reply on Apr 2, 2009 11:25 PM by dhinojosa

    Checking for duplicates before EntityHome.update() fails

    dhinojosa

      I can't believe I have encountered this yet.




      I have a unique constraint on a field called 'value' in an Entity called 'Assumption'. I can successfully determine if the a duplicate is being entered for persist().


      @SuppressWarnings({"UnusedAssignment"})
          @Override
          @Transactional
          public String persist() {
              try {
                  Query query = getEntityManager().createQuery("SELECT a from Assumption a where a.value = :value");
                  query.setParameter("value", getInstance().getValue());
                  query.getSingleResult();
                  getStatusMessages().addFromResourceBundleOrDefault(
                          StatusMessage.Severity.ERROR,
                          "assumptionNotUnique",
                          "The assumption \'{0}\' has already been entered",
                          getInstance().getValue());
                  return "failure";
              } catch (NoResultException nre) {
                  getInstance().setCreatedDate(calendar);
                  getInstance().setUpdatedDate(calendar);
                  return super.persist();
              }
          }
      



      but when I use similar logic for update() I get an exception, and it is perplexing.


       @Override
          @Transactional
          public String update() {
              try {
                  Query query = getEntityManager().createQuery("SELECT a from Assumption a where a.value = :value");
                  query.setParameter("value", getInstance().getValue());
                  Assumption assumption = (Assumption) query.getSingleResult();
                  if (!(getInstance().getId().equals(assumption.getId()))) {
                      getStatusMessages().addFromResourceBundleOrDefault(
                              StatusMessage.Severity.ERROR,
                              "assumptionNotUnique",
                              "The assumption \'{0}\' has already been entered",
                              getInstance().getValue());
                      return "failure";
                  }
      
                  getInstance().setUpdatedDate(calendar);
                  return super.update();
              } catch (NoResultException nre) {
                  getInstance().setUpdatedDate(calendar);
                  return super.update();
              } catch (RuntimeException re) {
                  re.printStackTrace();
                  return "failure";
              }
          }
      



      The issue is that Assumption assumption = (Assumption) query.getSingleResult(); is not running nor does it provide any message as to why it doesn't.  Any ideas?


        • 1. Re: Checking for duplicates before EntityHome.update() fails
          dan.j.allen

          If everything I describe below is setup correctly, what is likely happening is that the navigation rule is not checking the logical outcome of your action method and redirecting you in the detail page even in the failure scenario, where you see the dirty entity being passed off as the actual record. However, the values shown in the UI does not necessarily reflect what is in the database.


          Fix your navigation rule as follows:


          <navigation from-action="#{assumptionHome.update}">
             <rule if-outcome="updated">
                <end-conversation/>
                <redirect view-id="/Assumption.xhtml"/>
             </rule>
          </navigation>



          You should probably clear invalid property value before returning from the method to avoid confusion.


          Now for the details. You have to recall some general rules of JPA to understand what is going on here.


          First of all, how the query is executed depends on the flush mode setting. If you are in manual or commit flush mode, it will run against just the results in the database. If flush mode is auto, JPA will flush before executing the query (I think, check on that), so that the results are accurate.


          Second, there is no explicit update command. If the flush model is auto or commit, the update occurs automatically when the transaction ends due to dirty checking of the managed entity's properties. So regardless of what you return, it will happen anyway.


          Once again, we see a perfect use case for manual flushing. If you have manual flushing enabled, then your code should work. You can enable manual flush mode when the conversation begins. If you are using a standard seam-gen CRUD application, just add it to the begin conversation directive in the .page.xml file:


          <begin-conversation join="true" flush-mode="MANUAL"/>



          Btw, you can use an object equality comparison since both the fetched entity and the entity being modified are managed by the same EntityManager:


          if (!getInstance().equals(assumption)) {
              ...
          }



          And recall that on the next page render, the user will still see the transient values set on the entity, even though it hasn't been updated in the database. If you need to start with a fresh copy of the instance from the database, use EntityManager#refresh().

          • 2. Re: Checking for duplicates before EntityHome.update() fails
            dhinojosa

            Excellent the issue was the flush-mode.  Thanks so much.  Here is my solution, so that others can reference:


               @Override
                @Transactional
                public String persist() {
                    try {
                        Query query = getEntityManager().createQuery("SELECT a from Assumption a where a.value = :value");
                        query.setParameter("value", getInstance().getValue());
                        query.getSingleResult();
                        getStatusMessages().addFromResourceBundleOrDefault(
                                StatusMessage.Severity.ERROR,
                                "assumptionNotUnique",
                                "The assumption \'{0}\' has already been entered",
                                getInstance().getValue());
                        return "failure";
                    } catch (NoResultException nre) {
                        getInstance().setCreatedDate(calendar);
                        getInstance().setUpdatedDate(calendar);
                        return super.persist();
                    }
                }
            
                @Override
                @Transactional
                public String update() {
                    try {
                        Query query = getEntityManager().createQuery("SELECT a from Assumption a where a.value = :value");
                        query.setParameter("value", getInstance().getValue());
                        Assumption assumption = (Assumption) query.getSingleResult();
                        if (!(getInstance().getId().equals(assumption.getId()))) {
                            getStatusMessages().addFromResourceBundleOrDefault(
                                    StatusMessage.Severity.ERROR,
                                    "assumptionNotUnique",
                                    "The assumption \'{0}\' has already been entered",
                                    getInstance().getValue());
                            getEntityManager().refresh(getInstance());  //Make sure to REFRESH HERE to ensure that we start from previous state!!!
                            return "failure";
                        }
            
                        getInstance().setUpdatedDate(calendar);
                        return super.update();
                    } catch (NoResultException nre) {
                        getInstance().setUpdatedDate(calendar);
                        return super.update();
                    }
                }
            



            and here is the pages.xml snippet that ensures that flush-mode is MANUAL:


            <page view-id="/management/assumption/update.xhtml">
                    <begin-conversation join="true" flush-mode="MANUAL"/>
                    <param name="id" value="#{assumptionHome.id}"
                           required="true" converterId="javax.faces.Long"/>
                    <action execute="#{assumptionHome.find}"/>
                    <navigation from-action="#{assumptionHome.update}">
                        <rule if-outcome="updated">
                            <end-conversation/>
                            <redirect view-id="/management/assumption/list.xhtml"/>
                        </rule>
                    </navigation>
            </page>
            



            I will disagree with testing equality with equals() though


            if (!getInstance().equals(assumption)) {
                ...
            }
            


            since I never use any id to determine my equals(), hashCode(), toString(), etc.