Заполните p: selectOneMenu на основе другого p: selectOneMenu в каждой строке p: dataTable

У меня есть <p:dataTable> с ленивой загрузкой. В двух столбцах в каждом из них есть <p:selectOneMenu>.

В первом столбце содержится список стран, а второй содержит список состояний из базы данных.

Я хочу, чтобы второе меню (тот, который содержит список состояний) отображал только те состояния в каждой строке таблицы данных, которые соответствуют стране в первом меню в каждая строка таблицы данных.

В режиме редактирования, когда страна в своем меню изменена, состояния, соответствующие этой стране, должны быть заполнены в ее меню в этой текущей строке.

Как загрузить такие списки состояний, которые соответствуют их странам в каждой строке таблицы данных?


Эти два столбца в таблице данных остаются неполными, так как у меня нет четкого представления о том, как достичь этого.

<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 извлекает все страны, которые необходимы, но cityBean.selectedStates также извлекает все состояния из базы данных, которая не является необходимой и должна быть изменена, чтобы извлекать только те состояния, которые соответствуют его стране в другом меню.

Как я могу исходить отсюда?

Ответ 1

Пока ваше начальное решение работает, оно фактически неэффективно. Этот подход в основном требует, чтобы весь объектный граф таблиц Country и State (даже с круговыми ссылками) был полностью загружен в память Java на вид или сеанс JSF, даже если вы одновременно используете только, например. 5 из 150 стран (и, следовательно, теоретически 5 государственных списков были бы достаточными вместо 150 государственных списков).

У меня нет полного понимания ваших функциональных и технических требований. Возможно, вы одновременно используете все эти 150 стран. Возможно, у вас много страниц, где нужны все (по крайней мере, "многие" ) страны и государства. Возможно, у вас есть современное серверное оборудование с большим количеством памяти, чтобы все страны и штаты могли легко дублироваться во всех представлениях JSF и сеансах HTTP в памяти.

Если это не так, тогда было бы полезно не желать получать список состояний каждой отдельной страны (т.е. @OneToMany(fetch=LAZY) следует использовать на Country#states и State#cities). Учитывая, что списки стран и штатов (вероятные) статические данные, которые изменяются очень мало раз в год, по крайней мере достаточные для изменения только для каждого развертывания, лучше просто сохранить их в области приложения bean, которая повторно использовать во всех представлениях и сеансах вместо дублирования в каждом представлении JSF или сеансе HTTP.

Прежде чем продолжить ответ, я хотел бы отметить, что в вашем коде есть логическая ошибка. Учитывая тот факт, что вы редактируете список городов, и, следовательно, #{row} по существу #{city}, странно, что вы указываете страну через состояние, как в #{city.state.country} в выпадающем входном значении. Хотя это может работать для отображения, это не будет работать для редактирования/сохранения. В принципе, вы здесь меняете страну на основе штата, а не на уровне города. В настоящее время выбранное государство получит новую страну вместо текущего итерированного города. Это изменение отразилось бы во всех городах этого государства!

Это действительно не тривиально, если вы хотите продолжить эту модель данных. В идеале вы хотели бы иметь отдельное (виртуальное) свойство Country на City, чтобы изменения не влияли на свойство city State. Вы можете сделать это просто @Transient, чтобы JPA не считал его как @Column по умолчанию.

@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;
    }
}

В общем, вы должны в конечном итоге иметь это (несущественные/дефолтные/очевидные атрибуты опущены для краткости):

<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>

С #{applicationBean} что-то вроде этого:

@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;
    }

}

(это ленивый подход к загрузке, вы также можете сразу получить их все в @PostConstruct, просто посмотрите, что лучше для вас)

Ответ 2

В этом случае это довольно просто. Нет необходимости кодировать дальше. В меню состояния, ниже,

<f:selectItems var="state" value="#{cityManagedBean.selectedStates}" 
               itemLabel="#{state.stateName}" itemValue="#{state}" 
               itemLabelEscaped="true" rendered="true"/>

требуется изменить следующим образом.

<f:selectItems var="state" value="#{row.state.country.stateTableSet}" 
               itemLabel="#{state.stateName}" itemValue="#{state}" 
               itemLabelEscaped="true" rendered="true"/>

Так как объект entity (row в этом случае) содержит внедренный объект state, который, в свою очередь, содержит объект country, который, наконец, содержит список состояний , соответствующих этому country только как очевидный.

Итак,

cityManagedBean.selectedStates

дополнительный управляемый метод bean теперь не требуется вообще и нуждается в модификации как

row.state.country.stateTableSet

Где stateTableSet - это Set<StateTable>, который содержит список объектов объекта StateTable.


Кроме того, listener в <p:ajax> больше не требуется. Он должен выглядеть следующим образом.

<p:ajax update="cmbStateMenu"/>

Он предназначен только для обновления меню состояния, когда в меню страны выбран пункт (страна).


Следующий код должен выглядеть следующим образом.

<p:column id="country" headerText="Country" resizable="true" sortBy="#{row.state.country.countryName}" filterBy="#{row.state.country.countryName}" filterMatchMode="contains" filterMaxLength="45">
    <p:cellEditor>
        <f:facet name="output">
            <h:outputLink value="Country.jsf">
                <h:outputText value="#{row.state.country.countryName}"/>
                <f:param name="id" value="#{row.state.country.countryId}"/>
            </h:outputLink>                                                                                
        </f:facet>
        <f:facet name="input">
            <p:selectOneMenu id="cmbCountryMenu" converter="#{countryConverter}" value="#{row.state.country}" label="Country" required="true" filter="true" filterMatchMode="contains" effect="fold" rendered="true" editable="false" style="width:100%;">
                <f:selectItems var="country" value="#{cityManagedBean.selectedCountries}" itemLabel="#{country.countryName}" itemValue="#{country}" itemLabelEscaped="true" rendered="true"/>
                <p:ajax update="cmbStateMenu"/>
            </p:selectOneMenu>
        </f:facet>
    </p:cellEditor>
</p:column>

<p:column id="state" headerText="State" resizable="false" sortBy="#{row.state.stateName}" filterBy="#{row.state.stateName}" filterMatchMode="contains" filterMaxLength="45">
    <p:cellEditor>
        <f:facet name="output">
            <h:outputLink value="State.jsf">
                <h:outputText value="#{row.state.stateName}"/>
                <f:param name="id" value="#{row.state.stateId}"/>
            </h:outputLink>
        </f:facet>
        <f:facet name="input">
            <p:selectOneMenu id="cmbStateMenu" converter="#{stateConverter}" value="#{row.state}" label="State" required="true" filter="true" filterMatchMode="contains" effect="fold" rendered="true" editable="false" style="width:100%;">
                <f:selectItems var="state" value="#{row.state.country.stateTableSet}" itemLabel="#{state.stateName}" itemValue="#{state}" itemLabelEscaped="true" rendered="true"/>
            </p:selectOneMenu>
        </f:facet>
    </p:cellEditor>
</p:column>

Апология: я не упоминал в вопросе о том, что я извлекал список городов.

Ответ 3

Вам нужно получить список состояний на основе выбранной страны. Для этого вам нужен valueChangeListener.

Попробуйте это (я поместил оба выбрать одно меню в том же столбце)

В вашем xhtml

<p:column id="state" headerText="State" sortBy="#{row.state.stateName}" filterBy="#{row.state.stateName}">
<p:cellEditor>  
        <f:facet name="output">  
        <f:facet name="output">
                    <h:outputLink value="State.jsf">
                        <h:outputText value="#{row.state.stateName}"/>
                        <f:param name="id" value="#{row.state.stateId}"/>
                    </h:outputLink>
            </f:facet>
        <f:facet name="input">  
        <h:selectOneMenu id="cmbCountryMenu" style="width:100px" value="#{row.state.country}" converterMessage="Error message." label="Country" valueChangeListener = "#{countryController.handleCountrySelect}" immediate="true"  converter="#{countryConverter}">  
            <f:selectItem itemLabel = "Select" itemValue="#{null}" /> 
            <f:selectItems var="country" value="#{cityManagedBean.selectedCountries}" itemLabel="#{country.countryName}" itemValue="#{country}"/>
            <p:ajax update="cmbStateMenu" />
        </h:selectOneMenu>
        <h:selectOneMenu style="width:100px" value="#{row.state}" valueChangeListener = "#{stateController.handleStateSelect}" immediate="false" id="cmbStateMenu"  converter = "#{stateConverter}">

             <f:selectItems var="state" value="#{row.state.country.stateTableSet}" itemLabel="#{state.stateName}" itemValue="#{state}" itemLabelEscaped="true" rendered="true"/>
            <p:ajax update="@this" />
        </h:selectOneMenu>
        </f:facet>  
    </p:cellEditor> 

в вашем контроллере

  public void handleCountrySelect( ValueChangeEvent event )
{
    setStates( ( ( Country) event.getNewValue() ) );
}