Avoid extra DB reads in the getAsObject method of converter class by caching data client side?

后端 未结 2 641
梦毁少年i
梦毁少年i 2020-12-18 06:24

I\'m showing a list of suggested items in an autocomplete input element. For that I need to implement a converter to convert the entity

相关标签:
2条回答
  • 2020-12-18 06:48

    The only way I have found to do this so that your converter does not need to access the DB, is to make the Converter a managed bean so that it can access some other bean which stores the list of suggested values of the AutoComplete component.

    Something like this:

    @ManagedBean
    @RequestScoped
    public class EntityConverter implements Converter
    {
      @ManagedProperty(value = "#{autoCompleteBean}")
      private AutoCompleteBean autoCompleteBean;
    
      public void setAutoCompleteBean(AutoCompleteBean autoCompleteBean)
      {
        this.autoCompleteBean = autoCompleteBean;
      }
    
      @Override
      public Object getAsObject(FacesContext context, UIComponent component,
            String value)
      {
        final List<Entity> entities = autoCompleteBean.getCachedSuggestions();
    
        for (final Enity entity : entities)
        {
          if (entity.getIdAsString().equals(value))
          {
            return entity;
          }
        }
        throw new IllegalStateException("Entity was not found!");
      }
    
      @Override
      public String getAsString(FacesContext context, UIComponent component,
            Object value)
      { ... }
    

    In your jsf page, make sure you reference the converter as a bean. ie:

            <p:autoComplete value="#{autoCompleteBean.selectedEntity}"
              completeMethod="#{autoCompleteBean.getSuggestions}" var="theEntity"
              itemValue="#{theEntity}" itemLabel=#{theEntity.someValue}
              converter="#{entityConverter}">
    
    0 讨论(0)
  • 2020-12-18 06:53

    This is indeed "by design" and perhaps a little oversight in the JSF spec. You can in theory perfectly avoid it by extracting the items from the UIComponent argument and comparing against them instead. It's however a bit of work. My colleague Arjan Tijms has written a blog about this: Automatic to-Object conversion in JSF selectOneMenu & Co.

    Here's an extract of relevance; the below is the base converter which you'd need to extend instead:

    public abstract class SelectItemsBaseConverter implements Converter {
        @Override
        public Object getAsObject(FacesContext context, UIComponent component, String value) {        
            return SelectItemsUtils.findValueByStringConversion(context, component, value, this);    
        }    
    }
    

    Here's the SelectItemsUtils class which is partly copied from Mojarra's source:

    public final class SelectItemsUtils {
    
        private SelectItemsUtils() {}
    
        public static Object findValueByStringConversion(FacesContext context, UIComponent component, String value, Converter converter) {
            return findValueByStringConversion(context, component, new SelectItemsIterator(context, component), value, converter);        
        }
    
        private static Object findValueByStringConversion(FacesContext context, UIComponent component, Iterator<SelectItem> items, String value, Converter converter) {
            while (items.hasNext()) {
                SelectItem item = items.next();
                if (item instanceof SelectItemGroup) {
                    SelectItem subitems[] = ((SelectItemGroup) item).getSelectItems();
                    if (!isEmpty(subitems)) {
                        Object object = findValueByStringConversion(context, component, new ArrayIterator(subitems), value, converter);
                        if (object != null) {
                            return object;
                        }
                    }
                } else if (!item.isNoSelectionOption() && value.equals(converter.getAsString(context, component, item.getValue()))) {
                    return item.getValue();
                }
            }        
            return null;
        }
    
        public static boolean isEmpty(Object[] array) {
            return array == null || array.length == 0;    
        }
    
        /**
         * This class is based on Mojarra version
         */
        static class ArrayIterator implements Iterator<SelectItem> {
    
            public ArrayIterator(SelectItem items[]) {
                this.items = items;
            }
    
            private SelectItem items[];
            private int index = 0;
    
            public boolean hasNext() {
                return (index < items.length);
            }
    
            public SelectItem next() {
                try {
                    return (items[index++]);
                }
                catch (IndexOutOfBoundsException e) {
                    throw new NoSuchElementException();
                }
            }
    
            public void remove() {
                throw new UnsupportedOperationException();
            }
        }
    }
    

    Here's how you should use it for your own converter, you only have to implement getAsString() (the getAsObject() is already handled):

    @FacesConverter("someEntitySelectItemsConverter")
    public class SomeEntitySelectItemsConverter extends SelectItemsBaseConverter {
    
        @Override
        public String getAsString(FacesContext context, UIComponent component, Object value) {
            return ((SomeEntity) value).getId().toString();
        }
    }
    

    Update the above concept has ended up in JSF utility library OmniFaces in flavor of the following converters:

    • SelectItemsConverter - for <f:selectItem(s)> based on Object#toString().
    • SelectItemsIndexConverter - for <f:selectItem(s)> based on item's index.
    • ListConverter - for e.g. <p:autoComplete> based on Object#toString()
    • ListIndexConverter - for e.g. <p:autoComplete> based on item's index.
    0 讨论(0)
提交回复
热议问题