jpajsf-2entityconverters

Why is getAsObject() method of JSF converter called multiple times?


My database structure couples User entities to Role via a join table with the User class owning the relationship and possessing a field that refers to a Collection of Role entities.

I want to link the User to the relevant Role using a SelectOneMenu but this of course returns a Role object and not a Collection. Although SelectManyMenu would bypass this problem by returning a Collection instead, Users should only be allocated a single Role. I'm using the Omnifaces SelectItemsConverter class to handle Object<>String conversion and thought I could just override getAsObject to return a Collection containing the single selected Role. However, this doesn't work - debugging the processing of the form reveals that the getAsObject method is call three times (with three associated SQL calls querying the Roles table) during the course of JSF validation. The first time, the selected String value is passed, but the next two calls contain an empty String but despite this the Role setter is only called once (during the update model phase) and receives the correct value.

I've worked around this by adding the Role entity to the collection within the code handling persistence of the User entity to the database but this doesn't seem very elegant. I'd rather keep the conversion to the objects required by the User entity all in one place if possible. More importantly, I don't understand why I am seeing this behaviour, whether it is normal or the result of bad code and what impact the multiple database queries are likely to have in a production scenario. Not knowing is bugging me (no pun intended!) as it suggests I am failing to understand the ins-and-outs of JSF lifecycle and servlet processing.

The SelectOneMenu containing Role entities is populated by this method:

public static SelectItem[] getSelectItems(List<?> entities, boolean selectOne) {
    int size = selectOne ? entities.size() + 1 : entities.size();
    SelectItem[] items = new SelectItem[size];
    int i = 0;
    if (selectOne) {
        items[0] = new SelectItem("", "Please select one item");
        i++;
    }
    for (Object x : entities) {
        items[i++] = new SelectItem(x, x.toString());
    }
    return items;
}

The RolesSelectItemConverter.getAsObjec() method is identical to the standard Omnifaces implementation with the exception of a call to Logger to flag when the method is called.

Debug trace:

INFO:   START PHASE PROCESS_VALIDATIONS 3
INFO:   RoleSelectItemsConverter.getAsObject called for UOComponent {javax.faces.component.html.HtmlSelectOneMenu@368dde0b} and value {exhibitor}
INFO:   Attempting find all: com.rms.myconferenceprofile.entity.Role
INFO:   [EL Fine]: sql: 2013-12-02 19:59:50.036--ServerSession(1830578621)--Connection(2011154278)--Thread(Thread[http-listener-1(1),5,main])--SELECT ID, DESCRIPTION, ROLE_NAME FROM ROLES
INFO:   Attempting find all: com.rms.myconferenceprofile.entity.Role
INFO:   [EL Fine]: sql: 2013-12-02 19:59:50.039--ServerSession(1830578621)--Connection(1600432374)--Thread(Thread[http-listener-1(1),5,main])--SELECT ID, DESCRIPTION, ROLE_NAME FROM ROLES
INFO:   RoleSelectItemsConverter.getAsObject called for UOComponent {javax.faces.component.html.HtmlSelectOneMenu@368dde0b} and value {}
INFO:   Attempting find all: com.rms.myconferenceprofile.entity.Role
INFO:   [EL Fine]: sql: 2013-12-02 19:59:50.042--ServerSession(1830578621)--Connection(947366464)--Thread(Thread[http-listener-1(1),5,main])--SELECT ID, DESCRIPTION, ROLE_NAME FROM ROLES
INFO:   [EL Fine]: sql: 2013-12-02 19:59:50.044--ServerSession(1830578621)--Connection(966514315)--Thread(Thread[http-listener-1(1),5,main])--SELECT t1.ID, t1.EMAIL, t1.FORENAME, t1.INACTIVE, t1.PASSWORD, t1.PHONE_NUMBER, t1.REGISTERED_DATETIME, t1.SURNAME, t1.ADDRESS_ID FROM USER_ROLE_MAP t0, USERS t1 WHERE ((t0.ROLE_ID = ?) AND (t1.ID = t0.USER_ID))
    bind => [2]
INFO:   Attempting find all: com.rms.myconferenceprofile.entity.Role
INFO:   [EL Fine]: sql: 2013-12-02 19:59:50.048--ServerSession(1830578621)--Connection(119432927)--Thread(Thread[http-listener-1(1),5,main])--SELECT ID, DESCRIPTION, ROLE_NAME FROM ROLES
INFO:   RoleSelectItemsConverter.getAsObject called for UOComponent {javax.faces.component.html.HtmlSelectOneMenu@368dde0b} and value {}
INFO:   Attempting find all: com.rms.myconferenceprofile.entity.Role
INFO:   [EL Fine]: sql: 2013-12-02 19:59:50.051--ServerSession(1830578621)--Connection(1342595455)--Thread(Thread[http-listener-1(1),5,main])--SELECT ID, DESCRIPTION, ROLE_NAME FROM ROLES
INFO:   END PHASE PROCESS_VALIDATIONS 3
INFO:   START PHASE UPDATE_MODEL_VALUES 4
INFO:   SelectedRole setter called. Value: exhibitor
INFO:   END PHASE UPDATE_MODEL_VALUES 4

I've just obtained some more debug info looking at how many times Role.equals() is being called and what the comparison object is. It makes interesting reading! I can understand the first set of iterations through the available Role entities (iteration stops when the Role selected by the user is identified) but the second set of iterations through the whole Role table puzzles me. If were looking at a big table (e.g. countries or states) it would seem that my code is very inefficient!

INFO:   START PHASE PROCESS_VALIDATIONS 3
INFO:   RoleSelectItemsConverter.getAsObject called for UOComponent {javax.faces.component.html.HtmlSelectOneMenu@18a45031} and value {eventOrg}
INFO:   Attempting find all: com.rms.myconferenceprofile.entity.Role
INFO:   [EL Fine]: sql: 2013-12-02 20:54:37.596--ServerSession(1165546789)--Connection(544502930)--Thread(Thread[http-listener-1(3),5,main])--SELECT ID, DESCRIPTION, ROLE_NAME FROM ROLES
INFO:   Attempting find all: com.rms.myconferenceprofile.entity.Role
INFO:   [EL Fine]: sql: 2013-12-02 20:54:37.599--ServerSession(1165546789)--Connection(1032878258)--Thread(Thread[http-listener-1(3),5,main])--SELECT ID, DESCRIPTION, ROLE_NAME FROM ROLES
INFO:   RoleSelectItemsConverter.getAsObject called for UOComponent {javax.faces.component.html.HtmlSelectOneMenu@18a45031} and value {}
INFO:   Attempting find all: com.rms.myconferenceprofile.entity.Role
INFO:   [EL Fine]: sql: 2013-12-02 20:54:47.789--ServerSession(1165546789)--Connection(948440937)--Thread(Thread[http-listener-1(3),5,main])--SELECT ID, DESCRIPTION, ROLE_NAME FROM ROLES
INFO:   equals() called - comparison object {}
INFO:   equals() called - comparison object {delegate}
INFO:   equals() called - comparison object {exhibitor}
INFO:   Attempting find all: com.rms.myconferenceprofile.entity.Role
INFO:   equals() called - comparison object {eventOrg}
INFO:   [EL Fine]: sql: 2013-12-02 20:54:47.793--ServerSession(1165546789)--Connection(1537445637)--Thread(Thread[http-listener-1(3),5,main])--SELECT ID, DESCRIPTION, ROLE_NAME FROM ROLES
INFO:   RoleSelectItemsConverter.getAsObject called for UOComponent {javax.faces.component.html.HtmlSelectOneMenu@18a45031} and value {}
INFO:   Attempting find all: com.rms.myconferenceprofile.entity.Role
INFO:   [EL Fine]: sql: 2013-12-02 20:55:08.661--ServerSession(1165546789)--Connection(697639746)--Thread(Thread[http-listener-1(3),5,main])--SELECT ID, DESCRIPTION, ROLE_NAME FROM ROLES
INFO:   equals() called - comparison object {}
INFO:   equals() called - comparison object {delegate}
INFO:   equals() called - comparison object {exhibitor}
INFO:   equals() called - comparison object {eventOrg}
INFO:   equals() called - comparison object {venueOrg}
INFO:   equals() called - comparison object {ADMIN}
INFO:   END PHASE PROCESS_VALIDATIONS 3

Additional info following assist from Xtreme Biker:

I am not using JSF Managed Beans as I understand support will be withdrawn after the next release. The getSelectItems method is contained in a utility class that is not explicitly scoped - as I'm using CDI, I guess the only scope that would be equivalent to @ViewScope would be @ConversationScope? This method is lifted from NetBeans auto generated code so is probably not optimal.


Solution

  • Your provided code shows you're performing persistence layer access in an inappropriate place. Assuming your managed bean is @ViewScoped the way to go should be using preRenderView event or @PostConstruct to call the persistence layer and keep all the loaded values in variables. Later on, you'll return just that variables in getter methods. There's no need to hit the DB again till the end user performs another request.

    Consequently, doing such operations in getter methods is considered a bad practice. JSF will call that method every time it needs to access a property because it doesn't cache that values (you need to do that in your own bean and that is done storing them in variables).

    Also hitting the DB in Converter classes is considered as bad, but it's supposed to be avoided using Omnifaces Converters.

    You seem to use getSelectItems method to populate your available values. However, that's not what you're calling from the view, as it has two parameters. What method are you calling it from? Are you loading List<?> entities once and again?

    Unrelated to the problem I suggest you to use Java name conventions. Methods that begin with get are getters and they don't receive any parameter. The setter ones receive only one parameter, corresponding with what they have to set. Any different method shouldn't start with get or set (you could use load or prepare prefixes).

    See also: