Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Populate p:selectOneMenu based on another p:selectOneMenu in each row of a p:dataTable

I have a <p:dataTable> with lazy load. In two of the columns, there is a <p:selectOneMenu> in each of them.

The first column holds a list of countries and the second one holds a list of states from a database.

I want the second menu (the one that contains a list of states) to show only those states in each row of the data table which correspond to the country in the first menu in each row of the data table.

During edit mode, when a country in its menu is changed, the states corresponding to that country should be populated in its menu in that current row.

How to load such lists of states that correspond to their countries in each row of the data table?


These two columns in the data table are left incomplete, since I don't have a precise idea about how to achieve this.

<p:column>
    <p:cellEditor>
        <f:facet name="output">
            <h:outputText value="#{row.state.country.countryName}"/>
        </f:facet>

        <f:facet name="input">
            <p:selectOneMenu value="#{row.state.country}">
                <f:selectItems var="country"
                               value="#{cityBean.selectedCountries}"
                               itemLabel="#{country.countryName}"
                               itemValue="#{country}"/>

                <p:ajax update="states" listener="#{cityBean.getStates}"/>
            </p:selectOneMenu>
        </f:facet>
    </p:cellEditor>
</p:column>

<p:column>
    <p:cellEditor>
        <f:facet name="output">
            <h:outputText value="#{row.state.stateName}"/>
        </f:facet>

        <f:facet name="input">
            <p:selectOneMenu id="states">

                <f:selectItems var="state"
                               value="#{cityBean.selectedStates}"
                               itemLabel="#{state.stateName}"
                               itemValue="#{state}"/>
            </p:selectOneMenu>
        </f:facet>
    </p:cellEditor>
</p:column>

cityBean.selectedCountries retrieves all the countries which is necessary but cityBean.selectedStates also retrieves all the states from the database which is unnecessary and should be modified to retrieve only those states which correspond to its country in another menu.

How can I proceed from here?

like image 825
Tiny Avatar asked Jul 12 '13 10:07

Tiny


1 Answers

Whilst your initial solution works, it's in fact inefficient. This approach basically requires the entire object graph of the Country and State tables (even with circular references) to be fully loaded in Java's memory per JSF view or session even though when you simultaneously use only e.g. 5 of 150 countries (and thus theoretically 5 state lists would have been sufficient instead of 150 state lists).

I don't have full insight into your functional and technical requirements. Perhaps you're actually simultaneously using all of those 150 countries. Perhaps you've many pages where all (at least, "many") countries and states are needed. Perhaps you've state of art server hardware with plenty of memory so that all countries and states can effortlessly be duplicated over all JSF views and HTTP sessions in memory.

If that's not the case, then it would be beneficial to not eagerly fetch the state list of every single country (i.e. @OneToMany(fetch=LAZY) should be used on Country#states and State#cities). Given that country and state lists are (likely) static data which changes very few times in a year, at least sufficient to be changed on a per-deploy basis only, it's better to just store them in an application scoped bean which is reused across all views and sessions instead of being duplicated in every JSF view or HTTP session.

Before continuing to the answer, I'd like to remark that there's a logic error in your code. Given the fact that you're editing a list of cities, and thus #{row} is essentially #{city}, it's strange that you reference the country via the state as in #{city.state.country} in the dropdown input value. Whilst that may work for displaying, that wouldn't work for editing/saving. Basically, you're here changing the country on a per-state basis instead of on a per-city basis. The currently selected state would get the new country instead of the currently iterated city. This change would get reflected in all cities of this state!

This is indeed not trivial if you'd like to continue with this data model. Ideally, you'd like to have a separate (virtual) Country property on City so that the changes doesn't affect the city's State property. You could make it just @Transient so that JPA doesn't consider it as a @Column as by default.

@Transient // This is already saved via City#state#country.
private Country country;

public Country getCountry() {
    return (country == null && state != null) ? state.getCountry() : country;
}

public void setCountry(Country country) { 
    this.country = country;

    if (country == null) {
        state = null;
    }
}

All in all, you should ultimately have this (irrelevant/default/obvious attributes omitted for brevity):

<p:dataTable value="#{someViewScopedBean.cities}" var="city">
    ...
    <p:selectOneMenu id="country" value="#{city.country}">
        <f:selectItems value="#{applicationBean.countries}" />
        <p:ajax update="state" />
    </p:selectOneMenu>
    ...
    <p:selectOneMenu id="state" value="#{city.state}">
        <f:selectItems value="#{applicationBean.getStates(city.country)}" />
    </p:selectOneMenu>
    ...
</p:dataTable>

With an #{applicationBean} something like this:

@Named
@ApplicationScoped
public class ApplicationBean {

    private List<Country> countries;
    private Map<Country, List<State>> statesByCountry;

    @EJB
    private CountryService countryService;

    @EJB
    private StateService stateService;

    @PostConstruct
    public void init() {
        countries = countryService.list();
        statesByCountry = new HashMap<>();
    }

    public List<Country> getCountries() {
        return countries;
    }

    public List<State> getStates(Country country) {
        List<State> states = statesByCountry.get(country);

        if (states == null) {
            states = stateService.getByCountry(country);
            statesByCountry.put(country, states);
        }

        return states;
    }

}

(this is the lazy loading approach; you could also immediately fetch them all in @PostConstruct, just see what's better for you)

like image 77
BalusC Avatar answered Oct 16 '22 07:10

BalusC