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

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)

Leave a Comment