New EntityConverter POC
antibrumm.mfrey0.bluewin.ch Jan 21, 2011 4:25 AMHello
I did some investigation on our very slow pages this week and after digging around i found multiple threads stating that the EntityConverter is alot of times a bottleneck. We have 3 selectboxes for selecting a user in our editor. That is 3 times the same content. I found out that the entityconverter keeps his own map to identify the selected entity later. (Thanks Tobias for the timinginterceptor! again :D )
So i've created my own entityConverter and would like to get some ideas from you if you see some problems with it and what you think about it's performance. It uses the EntityManager directly (possible issue?) and keeps only the class and @Id field definition instead of the whole entity.
My testpage contains 20 selectboxes (10 times the userselect and 10 times the scopeselect).
For the SelectItem version 10x this code
<h:selectOneMenu value="#{requirementInstance.owner}" style="width:300px;" required="true"> <f:selectItems value="#{activeUsersSI}" /> </h:selectOneMenu> <h:selectOneMenu value="#{requirementInstance.scope}" style="width:300px;" required="true"> <f:selectItems value="#{scopeListAllScopeItems}" /> </h:selectOneMenu> <br />
For the EntityConverter version 10x this code
<h:selectOneMenu value="#{requirementInstance.owner}" style="width:300px;"> <s:selectItems var="user" label="#{user.fullName}" noSelectionLabel="Select a user" value="#{activeUsers}" /> <s:convertEntity /> </h:selectOneMenu> <h:selectOneMenu value="#{requirementInstance.scope}" style="width :300px;"> <s:selectItems var="scope" noSelectionLabel="Select a Scope" label="#{scope.label}" value="#{allScopes}" /> <s:convertEntity /> </h:selectOneMenu> <br />
I compared first the SelectItem version with the original EntityConverter and i got some timing values of:
- SI: 780ms - 1070ms
- EC: 7800ms - 8360ms (nope there is not a 0 to much)
So i created this code:
@Name("org.jboss.seam.ui.EntityConverter") @Scope(CONVERSATION) @Install(precedence = Install.APPLICATION) @Converter @BypassInterceptors public class EntityConverter implements javax.faces.convert.Converter, Serializable { private class EntityDef { protected Class<?> entityClass = null; protected Field pkField = null; protected String simpleName = null; public EntityDef(Class<?> entityClass) { super(); simpleName = entityClass.getSimpleName(); this.entityClass = entityClass; for (Field field : ReflectionHelper.getFields(entityClass)) { if (field.isAnnotationPresent(Id.class)) { pkField = field; break; } } } } /** * */ private static final long serialVersionUID = 1L; private EntityDef entityDef; public Object getAsObject(FacesContext facesContext, UIComponent cmp, String value) throws ConverterException { if (value == null) { return null; } EntityManager entityManager = (EntityManager) Component.getInstance("entityManager"); String simpleClassName = value.substring(0, value.indexOf(":")); String id = value.substring(value.indexOf(":") + 1).trim(); if (!simpleClassName.equals(entityDef.simpleName)) { Map<String, EntityDef> entityDefinitions = getEntityDefinitions(); entityDef = entityDefinitions.get(simpleClassName); } Object pkValue = null; if (entityDef.pkField.getType().equals(String.class)) { pkValue = id; } else if (entityDef.pkField.getType().equals(Boolean.class)) { pkValue = Boolean.parseBoolean(id); } else if (entityDef.pkField.getType().equals(Long.class)) { pkValue = Long.parseLong(id); } else if (entityDef.pkField.getType().equals(Integer.class)) { pkValue = Integer.parseInt(id); } else if (entityDef.pkField.getType().equals(Float.class)) { pkValue = Float.parseFloat(id); } else if (entityDef.pkField.getType().equals(Double.class)) { pkValue = Double.parseDouble(id); } return entityManager.find(entityDef.entityClass, pkValue); } public String getAsString(FacesContext facesContext, UIComponent cmp, Object value) throws ConverterException { if (value == null) { return null; } if (value instanceof String) { return (String) value; } if (entityDef == null || !value.getClass().equals(entityDef.entityClass)) { String simpleName = value.getClass().getSimpleName(); Map<String, EntityDef> entityDefinitions = getEntityDefinitions(); if (!entityDefinitions.containsKey(value.getClass().getSimpleName())) { entityDefinitions.put(simpleName, new EntityDef(value.getClass())); } entityDef = entityDefinitions.get(simpleName); } boolean acc = entityDef.pkField.isAccessible(); try { if (!acc) { entityDef.pkField.setAccessible(true); } return String.valueOf(entityDef.simpleName + ": " + entityDef.pkField.get(value)); } catch (Exception e) { return "Cannot get PK"; } finally { entityDef.pkField.setAccessible(acc); } } @SuppressWarnings("unchecked") private Map<String, EntityDef> getEntityDefinitions() { Map<String, EntityDef> entityDefinitions = (Map<String, EntityDef>) Contexts.getSessionContext().get( "entityDefinitions"); if (entityDefinitions == null) { Contexts.getSessionContext().set("entityDefinitions", new HashMap<String, EntityDef>()); } return entityDefinitions; } }
And now i got timings like this:
- SI: 780ms - 1070ms
- EC: 7800ms - 8360ms (nope there is not a 0 to much)
- New EC: 920ms - 1300ms (once 2100ms)
My Entityconverter uses the SimpleClassName (bandwidth) and PrimaryKey as a select value instead of keeping the entities somewhere around and reference them through the array location.
This converter works if there are no SimpleClassName for entities in the application and the entity does only have one primary key (@Id).
I would be very interessed if someone could verify this and / or get some other input.
Martin