1 2 Previous Next 15 Replies Latest reply on Jul 12, 2008 1:20 AM by www.supernovasoftware.com

    Entity persist against my will

      I am using Richfaces, and I have a tabPanel using switchType 'client'.
      I want to allow the user to edit information on multiple tabs and hold off on validation until the user clicks a single save button.


      The Save button is tied to action in my page-scoped Seam component which first does a validation of the form fields and if there is an error it aborts without persisting, and an error message is displayed to user so they can fix problem and re-submit.  If validation succeeds, I call persist/update on the EntityHome component to persist its state to the database.


      My problem is that under the hood, I believe Seam is persisting my entity in the stack after I return a failure from my validation. So even though an error is displayed, the current state of the form has already been saved to the database.


      Can anyone shed some light on this? I am confused why something is being persisted without my explicitly causing this to happen.


      Any tips, pointers or references would be much appreciated.


      Regards,


      Mark

        • 1. Re: Entity persist against my will
          tomkramer

          i haven't experienced such problems with the jsf lifecycle and seam. but can you pls post the relevant code sections? otherwise we would just guess.

          • 2. Re: Entity persist against my will

            Ok, I tried to boil code down to just the basics, without getting rid of anything that could be responsible for my problem.


            Here is the .xhtml:


                      <h:form id="user" styleClass="edit" enctype="multipart/form-data">
                           <rich:panel>
                                <rich:tabPanel id="membertabs" selectedTab="#{selectTab}" switchType="client">
                                     <rich:tab name="info"
                                          label="#{messages['com.leagueunited.admin.member.info']}">
                                          <ui:include src="member/memberinfo_tab.xhtml" />
                                     </rich:tab>
            
                                     <rich:tab name="contact"
                                          label="#{messages['com.leagueunited.admin.member.contact']}">
                                          <ui:include src="member/membercontact_tab.xhtml" />
                                     </rich:tab>
            
                                     <rich:tab name="family"
                                          label="#{messages['com.leagueunited.admin.member.family']}">
                                          <ui:include src="member/memberfamily_tab.xhtml" />
                                     </rich:tab>
            
                                     <rich:tab name="account"
                                          rendered="#{user.hasLoginAccount}"
                                          label="#{messages['com.leagueunited.admin.member.account']}">
                                          <ui:include src="member/memberaccount_tab.xhtml" />
                                     </rich:tab>
                                </rich:tabPanel>
            
                                <div style="clear: both"><span class="required">*</span>
                                #{messages['com.leagueunited.prompt.requiredfield']}</div>
            
                                <h:commandButton id="account"
                                     value="#{messages['com.leagueunited.admin.member.createaccount']}"
                                     action="#{memberAction.createLoginAccount}"
                                     rendered="#{!user.hasLoginAccount}"/>
                           </rich:panel>
            
            
                           <div class="actionButtons">
                                <h:commandButton id="save"
                                     value="#{messages['com.leagueunited.btn.save']}"
                                     action="#{memberAction.persist}" disabled="#{!userHome.wired}"
                                     rendered="#{!userHome.managed}"/>
                                <h:commandButton id="update"
                                     value="#{messages['com.leagueunited.btn.save']}"
                                     action="#{memberAction.update}" rendered="#{userHome.managed}"/>
                                <h:commandButton
                                     id="delete" value="#{messages['com.leagueunited.btn.delete']}"
                                     action="#{userHome.remove}" immediate="true"
                                     rendered="#{userHome.managed}"/>
                                <s:button id="done"
                                     value="#{messages['com.leagueunited.btn.cancel']}"
                                     action="adminmenu_members"/>
                           </div>
                      </h:form>
            



            Here is my implementation of the backing component:


            @Name("memberAction")
            @Scope(ScopeType.PAGE)
            public class MemberAction  {
            
                 @In(create=true)
                 protected UserHome userHome;
            
                    @In protected FacesMessages facesMessages;
                 @In private Map<String, String> messages;
            
                 private boolean validateForm() {
                      Boolean bResult = true;
                      
                      // Handle for validation
                      User user = userHome.getInstance();
                      
                      if (user.getUserFname().isEmpty()) {
                          facesMessages.addFromResourceBundle("com.leagueunited.error.requiredtabfield",
                                    messages.get("com.leagueunited.admin.member.info"),
                                    messages.get("com.leagueunited.admin.member.fname"));
                           bResult = false;
                      }
                      if (user.getUserLname().isEmpty()) {
                          facesMessages.addFromResourceBundle("com.leagueunited.error.requiredtabfield",
                                    messages.get("com.leagueunited.admin.member.info"),
                                    messages.get("com.leagueunited.admin.member.lname"));
                           bResult = false;
                      }
            
            
                      return bResult;
                 }
                     public String update() {
                      if (!validateForm()) 
                           return "failed";
                      
                      // If password has been modified re-hash it before storing
                      User user = userHome.getDefinedInstance();
                      if (user != null && user.getHasLoginAccount()) {
                           if (!this.initPassword.equals(user.getUserLogin().getUserPassword()))
                                userHome.hashPassword();
                      }
            
                      return userHome.update();
                 }
                 
                 public String persist() {
                      if (!validateForm()) 
                           return "failed";
                      
                      return userHome.persist();     
                 }
            }
            



            And lastly here are the relevant excerpts from the page.xml for the page:


               <action execute="#{userHome.wire}"/>
               <action execute="#{memberAction.saveInitPassword}"/>
               
               <param name="userId" value="#{userHome.userId}"/>
               <param name="selectTab"/>
               
               <navigation from-action="#{memberAction.update}">
                    <rule if-outcome="updated">
                        <end-conversation before-redirect="true"/>
                         <redirect view-id="/admin/members.xhtml"/>
                    </rule>
                    <rule if-outcome="failed">
                        <begin-conversation join="true"/>
                         <redirect view-id="/admin/member.xhtml"/>
                    </rule>
               </navigation>
               
               <navigation from-action="#{memberAction.persist}">
                    <rule if-outcome="persisted">
                         <end-conversation before-redirect="true"/>
                         <redirect view-id="/admin/members.xhtml"/>
                   </rule>
                    <rule if-outcome="failed">
                        <begin-conversation join="true"/>
                         <redirect view-id="/admin/member.xhtml"/>
                    </rule>
               </navigation>
            



            -Mark

            • 3. Re: Entity persist against my will
              tomkramer

              Hey Mark,


              1. Can you also post your UserHome and User class?


              2. Have you checked your assumption in the database? I saw that you do your validation during the Invoke Appliaction-Phase. That means your Entity will be updated no matter if your validation has failed or not. If you come back to your member.xhtml also the wrong values that have been set to your entity will be shown in your form.


              tom

              • 4. Re: Entity persist against my will

                Tom,


                Thanks for looking at this.


                In answer to your questions -


                2. Yes - you are correct, regardless of whether validation succeeds or fails my Entity is being updated with the bad values.  And yes, I have checked the database.  In the case of validation failing, the database record is being stored with the erroneous data.  For example my validation checks that user has entered a last name.  If I leave blank and submit, validation fails, the member form is redisplayed with an error, but the database record has been stored with a blank name field.


                You mentioned I am doing the validate during the Invoke Application Phase - what would be an alternative to move this validation before this? 


                1. Here is code you asked for (I've dumbed them down to make it manageable)


                Here is UserHome:


                @Name("userHome")
                public class UserHome extends EntityHome<User> {
                      private static final long serialVersionUID = 5002385282902930626L;
                
                     @In(create = true)
                     private SportHome sportHome;
                
                     @In(required=false)
                     private OrganizationHome organizationHome;
                     
                     public void setUserId(Long id) {
                          setId(id);
                     }
                
                     public Long getUserId() {
                          return (Long) getId();
                     }
                
                    @Factory(value = "user", scope = ScopeType.PAGE)
                    public User initUser() { 
                         return super.getInstance();
                    }
                
                    private void adjustUserEmails() {
                         // Slide email fields down so that empty fields are always last
                         User user = getInstance();
                         if (user == null)
                              return;
                         
                         if (user.getUserEmail1() == null || user.getUserEmail1().isEmpty()) {
                              user.setUserEmail1(user.getUserEmail2());
                              user.setUserEmail2(user.getUserEmail3());
                              user.setUserEmail3(null);
                         }
                         if (user.getUserEmail1() == null || user.getUserEmail1().isEmpty()) {
                              user.setUserEmail1(user.getUserEmail2());
                              user.setUserEmail2(null);
                         }
                    }
                    
                    @Override
                    public String update() {
                         adjustUserEmails();
                
                         return super.update();
                    }
                    
                     @Override
                     public String persist() {
                          adjustUserEmails();
                                    
                          // Now persists as normal
                          return super.persist();
                     }
                
                     @Override
                     protected User createInstance() {
                          User user = new User();
                          return user;
                     }
                               
                     public void wire() {
                          // First try org
                          if (organizationHome != null && organizationHome.getDefinedInstance() != null) {
                               getInstance().setUserOrg(organizationHome.getDefinedInstance());
                               return;
                          }
                          
                          // Try wiring using sport
                          Sport sport = sportHome.getDefinedInstance();
                          if (sport != null) {
                               getInstance().setUserOrg(sport.getSportOrg());
                          }
                     }
                
                     public boolean isWired() {
                          if (getInstance().getUserOrg() == null)
                               return false;
                          return true;
                     }
                
                     public User getDefinedInstance() {
                          return isIdDefined() ? getInstance() : null;
                     }
                
                }
                



                and here is User:


                @Entity
                public class User  implements java.io.Serializable {
                       private static final long serialVersionUID = 70389255274496297L;
                     private Long userId;
                     private String userFname;
                     private String userMname;
                     private String userLname;
                     private Date userDob;
                     private Address userAddress;
                     private String userEmail1;
                     private String userEmail2;
                     private String userEmail3;
                     private Organization userOrg;
                
                     // Transients
                     private boolean selected;
                     
                    public User() {
                         this.userAddress = new Address();
                         this.selected = false;
                    }
                        
                    @Id  
                     @GeneratedValue(strategy = IDENTITY)
                     @Column(name="USER_ID", unique=true, nullable=false)
                    public Long getUserId() {
                        return this.userId;
                    }
                    
                    public void setUserId(Long userId) {
                        this.userId = userId;
                    }
                        
                    @Column(name="USER_FNAME", nullable=false, length=45)
                    @NotNull
                    @Length(max=45)
                    public String getUserFname() {
                        return this.userFname;
                    }
                    
                    public void setUserFname(String userFname) {
                        this.userFname = userFname;
                    }
                    
                    @Column(name="USER_MNAME", length=45)
                    @Length(max=45)
                    public String getUserMname() {
                        return this.userMname;
                    }
                    
                    public void setUserMname(String userMname) {
                        this.userMname = userMname;
                    }
                    
                    @Column(name="USER_LNAME", nullable=false, length=45)
                    @NotNull
                    @Length(max=45)
                    public String getUserLname() {
                        return this.userLname;
                    }
                    
                    public void setUserLname(String userLname) {
                        this.userLname = userLname;
                    }
                    @Temporal(TemporalType.DATE)
                    @Column(name="USER_DOB", length=0)
                    public Date getUserDob() {
                        return this.userDob;
                    }
                
                    public void setUserDob(Date userDob) {
                        this.userDob = userDob;
                    }
                
                     @Embedded
                     @AttributeOverrides({
                          @AttributeOverride(name="street1", column=@Column(name="USER_STREET_1", length=100, nullable=false)),
                          @AttributeOverride(name="street2", column=@Column(name="USER_STREET_2", length=100)),
                          @AttributeOverride(name="city", column=@Column(name="USER_CITY", length=40, nullable=false)),
                          @AttributeOverride(name="stateProv", column=@Column(name="USER_STATEPROV", length=10, nullable=false)),
                          @AttributeOverride(name="postCode", column=@Column(name="USER_POSTCODE", length=10, nullable=false)),
                          @AttributeOverride(name="country", column=@Column(name="USER_COUNTRY", length=2, nullable=false))
                     })
                     public Address getUserAddress() {
                          return this.userAddress;
                     }
                     
                     public void setUserAddress(Address address) {
                          this.userAddress = address;
                     }
                
                     @Column(name="USER_EMAIL1", nullable=true, length=45)
                    @Length(max=45)
                    public String getUserEmail1() {
                        return this.userEmail1;
                    }
                    
                    public void setUserEmail1(String userEmail1) {
                        this.userEmail1 = userEmail1;
                    }
                
                     @Column(name="USER_EMAIL2", nullable=true, length=45)
                    @Length(max=45)
                    public String getUserEmail2() {
                        return this.userEmail2;
                    }
                    
                    public void setUserEmail2(String userEmail2) {
                        this.userEmail2 = userEmail2;
                    }
                
                     @Column(name="USER_EMAIL3", nullable=true, length=45)
                    @Length(max=45)
                    public String getUserEmail3() {
                        return this.userEmail3;
                    }
                    
                    public void setUserEmail3(String userEmail3) {
                        this.userEmail3 = userEmail3;
                    }
                
                     @ManyToOne(fetch = FetchType.LAZY)
                     @JoinColumn(name = "ORG_ID", nullable = false)
                     @NotNull
                     public Organization getUserOrg() {
                          return this.userOrg;
                     }
                
                     public void setUserOrg(Organization organization) {
                          this.userOrg = organization;
                     }    
                    
                     // Transient Helper Methods
                    
                    @Transient
                    public boolean isSelected() {
                         return this.selected;
                    }
                    
                    @Transient
                    public String getUserEmail() {
                         // Return first email address field
                         return this.userEmail1;
                    }
                    
                    @Transient
                    public String getAllUserEmail() {
                         // Return concatenation of all email addresses
                         String email = this.userEmail1;
                         if (this.userEmail2 != null && !this.userEmail2.isEmpty())
                              email.concat(";" + this.userEmail2);
                         if (this.userEmail3 != null && !this.userEmail3.isEmpty())
                              email.concat(";" + this.userEmail3);
                         
                         return email;
                    }
                
                    public void setSelected(boolean selected) {
                         this.selected = selected;
                    }
                    
                }
                


                • 5. Re: Entity persist against my will
                  admin.admin.email.tld

                  Are you using a LRC (long running conversation) with a conversation-scoped component?


                  I haven't used this particular Richfaces component, but it sounds like in your use case, you don't want any CRUD operations/transactions happening until user clicks the save button (i.e. the data is not persisted to db when user clicks next tab).


                  So if this is true, then you need a conversation-scoped SFSB or POJO to handle the action methods instead of the page-scoped MemberAction POJO you have.  Make sure you use SMPC EntityManager and @Begin/@End annotations accordingly.


                  unless I'm totally missing something here.....

                  • 6. Re: Entity persist against my will
                    admin.admin.email.tld

                    Also, check out chapter 17 of the Bauer and King book.  You may be experiencing premature flushing of the persistence context.



                    Note that the persistence context spans the conversation, but that flushing and commits may occur during the conversation.  Hence, the whole conversation isn't atomic.  You can disable automatic flushing with @Begin(flushMode=flushModeType.MANUAL) when a conversation is promoted to be long-running; you then have to call flush() manually when the conversation ends (usually in the method marked with @End).

                    flushModeType MANUAL is not part of JSR220 (this is a Seam extension).


                    from Seam 2.0.1.GA ref pdf:



                    Seam lets you specify FlushModeType.MANUAL when beginning a conversation. Currently, this works only when
                    Hibernate is the underlying persistence provider, but we plan to support other equivalent vendor extensions.
                    • 7. Re: Entity persist against my will
                      tomkramer

                      Mark,


                      I had the same idea as Arbi this morning. It must have smth. to do with your page-rule:


                      <rule if-outcome="failed">
                          <begin-conversation join="true"/>
                          <redirect view-id="/admin/member.xhtml"/>
                      </rule>
                      



                      it seems that the begin-conversation or the redirect statement is 
                      responsible for flushing the changes on your entity to the database.


                      So you're running into this problem for two reasons.


                      1. You pass the Process Validation - Phase without throwing any Validation errors. So your model will be updated during the Update Model Values-Phase.


                      2. The changes on your model will be flushed to the database because of the conversation demarcation.


                      Id didn't see anything in your page.xml where you explicitly open a long runnuning conversation but in the failed-rule. In the persist-rule you're trying to close a long running conversation. (which by the way should cause problems if you never run into the failed-rule before.)


                      Maybe you can solve the problem with the following changes:


                      To 1.
                      Seam extends the jsf-validation with Annotation-based form validation. Your Entity is annotated with field value constraints, so your form is predestined to use this feature. That means that your hibernate annotation could also be used on ui-level. to use this feature you have to surround your input-field area with


                      <s:validateAll>
                       ...
                      </s:validateAll>
                      



                      In combination with the

                      <s:decorate>

                      tag you should have all need for the validation. That also means that there is no need for your validateForm() Method. If the (JSF/Seam-) validation fails the persist or update method will not be invoked. A click on the save button with bad values means that the Process Validation Phase redirects you directly to the Render Response Phase. So your page will be displayed again with additional validation messages. And you don't have to care for that in any page-rule.


                      To 2.
                      To prevent your model to be updated against your will, you should open the conversation when you enter the page with the form and only close it when you leave the page (after a cancel or intended persist). But to me it seems that you don't need a long running conversation as long as you are using client-side tab switching and the jsf/seam-validation. if this isn't sufficient for your case then try to handle your conversation with the

                      flushMode=flushModeType.MANUAL

                      statement as Arbi suggested.

                      • 8. Re: Entity persist against my will
                        dan.j.allen

                        And now that people can understand the value of manual flushing, can people in the community please start bugging the crap out of the JPA 2.0 committee to finally make this feature part of JPA!! I intend to do a blog entry on this in the near future.

                        • 9. Re: Entity persist against my will
                          admin.admin.email.tld

                          I'd like to know why they thought (and still may think) that AUTO and COMMIT are sufficient in cases of conversations/stateful programming...

                          • 10. Re: Entity persist against my will

                            Thanks Tom and Arbi for your replies.


                            As you suggested, I switched over to using form validation and this does of course solve the problem.
                            There are some fields that need more advanced (custom) validation, and I see that I can write my own JSF validator to handle this.


                            I am several months into a project using Seam/JSF for the first time and while I feel like I've come a long way, there is still a bit of a learning hurdle in certain areas.


                            Dan's upcoming book (I am on early access list) has been a fantastic resource, as have folks like yourselves on the forum.


                            Thanks again,


                            -Mark

                            • 11. Re: Entity persist against my will
                              admin.admin.email.tld

                              With a few years of J2EE (EJB2/Struts) dev experience, it took me about 6 months of SOLID experimenting, coding, reading, posting on the forums, etc. to catch up to the API and concepts in JSF/RF/SEAM/EJB3.


                              The JBoss dev support with 2 day SLA response has been very helpful as well...

                              • 12. Re: Entity persist against my will
                                www.supernovasoftware.com

                                I have been thinking hard about purchasing dev support, but I am hesitant.


                                Could you give some examples of questions answered and/or problems solved using the dev support?

                                • 13. Re: Entity persist against my will
                                  admin.admin.email.tld

                                  1) Currently I am using JBossWS wsconsume utility to create WSDL2Java classes and client code in my Seam app to consume/call a webMethods web service endpoint.  Getting some exceptions when that call happens and they're helping me (after I attached the WSDL, client code, stack trace to the case).  The goal is to identify the root cause and fix it as I'm new to web services.


                                  2) Helping me understand how to go about setting up a simple 2-node JBoss cluster (they basically pointed me to some public docs).


                                  3) How to use the Seam Conversation API to manually manage fine-grained conversations (e.g. when a newly selected value in a drop-down box starts a new conversation).


                                  4) General advice on JVM and perm gen issues.


                                  5) conditional page re-direction how-to.


                                  6) CSS/Richfaces how-to with components and tag attributes I wasn't so familiar with.


                                  7) deployment best practices (e.g. how many apps per JBoss instance, 32-bit vs. 64-bit and memory allocation)


                                  8) having them do the research if there's a bug instead of doing it yourself and finding out 8 hrs later it's a bug in framework(s), not your app


                                  9) not getting confused about which forum to post your question to (EJB, Hibernate, Seam??)


                                  10) help understanding which service/software to use when setting up a JBoss windows service and how to install/configure


                                  many, many more.


                                  Keep in mind that you will not be getting any special or private documentation.  According to their techs, all JBoss user docs are public.  Although I'm sure you won't be able to view my personal case data, for example.

                                  • 14. Re: Entity persist against my will
                                    admin.admin.email.tld

                                    If you're serious about Seam it's a no-brainer.  for $3500 you get unlimited cases per year.  snatch it before they wake up...

                                    1 2 Previous Next