-
1. Re: Entity persist against my will
tomkramer Jul 9, 2008 3:42 PM (in response to grundor)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
grundor Jul 9, 2008 5:21 PM (in response to grundor)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 Jul 9, 2008 6:19 PM (in response to grundor)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 thewrong
values that have been set to your entity will be shown in your form.tom
-
4. Re: Entity persist against my will
grundor Jul 9, 2008 8:40 PM (in response to grundor)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 Jul 10, 2008 12:54 AM (in response to grundor)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 Jul 10, 2008 1:04 AM (in response to grundor)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 Jul 10, 2008 10:27 AM (in response to grundor)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 withoutthrowing
any Validation errors. So your model will be updated during theUpdate 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 thepersist-rule
you're trying to close a long running conversation. (which by the way should cause problems if you never run into thefailed-rule
before.)Maybe you can solve the problem with the following changes:
To 1.
Seam extends the jsf-validation withAnnotation-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 withbad values
means that theProcess Validation
Phase redirects you directly to theRender 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 updatedagainst 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 theflushMode=flushModeType.MANUAL
statement as Arbi suggested. -
8. Re: Entity persist against my will
dan.j.allen Jul 10, 2008 6:34 PM (in response to grundor)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 Jul 11, 2008 6:18 AM (in response to grundor)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
grundor Jul 11, 2008 3:01 PM (in response to grundor)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 Jul 11, 2008 10:34 PM (in response to grundor)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 Jul 12, 2008 12:20 AM (in response to grundor)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 Jul 12, 2008 12:52 AM (in response to grundor)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
orprivate
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 Jul 12, 2008 12:53 AM (in response to grundor)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...