Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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<entityName, entityId> to entityName & vice versa. However while implementing that I realized that I had to read the DB more than 1 time to find the corresponding entityId for the chosen entityName(while getAsObject()), I am wondering why isn't this stored somewhere client side so that the entityId could be passed when the entityname is selected.

Is there any way I could avoid this extra read?

like image 865
Rajat Gupta Avatar asked Feb 05 '12 05:02

Rajat Gupta


1 Answers

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.
like image 137
BalusC Avatar answered Oct 19 '22 19:10

BalusC