When using Spring with Hibernate or JPA, it can be very convenient to use objects as method parameters and automatically convert primary keys to those objects. For example:
@RequestMapping(value = "/person/{personId}", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
@ResponseBody
public PersonDto getById(@PathVariable Person person) {
return person;
}
instead of:
@RequestMapping(value = "/person/{personId}", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
@ResponseBody
public PersonDto getById(@PathVariable String personId) {
return personService.getById(personId);
}
Using this approach throughout a project (in controllers, services, etc) can lead to much more readable, shorter code.
Spring Data JPA provides a similar feature via DomainClassConverter. In my case, Spring Data wasn’t an option.
Here’s how to make this happen. First, set up a @Configuration class:
package com.integralblue.candrews.config;
import javax.annotation.PostConstruct;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;
import com.integralblue.candrews.converter.EntityToIdConverter;
import com.integralblue.candrews.converter.IdToEntityConverter;
@Configuration
public class WebMvcConfig extends WebMvcConfigurationSupport {
@Bean
protected IdToEntityConverter idToEntityConverter() {
return new IdToEntityConverter();
}
@Bean
protected EntityToIdConverter entityToIdConverter() {
return new EntityToIdConverter();
}
@PostConstruct
public void addConverters() {
mvcConversionService().addConverter(idToEntityConverter());
mvcConversionService().addConverter(entityToIdConverter());
}
}
Now add these 2 classes:
package com.integralblue.candrews.converter;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.persistence.metamodel.Attribute;
import javax.persistence.metamodel.EntityType;
import javax.persistence.metamodel.SingularAttribute;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.core.convert.converter.ConditionalGenericConverter;
import org.springframework.format.support.FormattingConversionService;
/**
* Converts an entity to its ID (the field in the entity annotated with
* {@link javax.persistence.Id}). This is similar to
* {@link org.springframework.data.repository.support.DomainClassConverter}
* but since we don't use Spring Data, we can't use that.
*/
public class EntityToIdConverter implements ConditionalGenericConverter {
@PersistenceContext
private EntityManager entityManager;
@Autowired
private FormattingConversionService conversionService;
@Override
public Set getConvertibleTypes() {
return Collections.singleton(new ConvertiblePair(Object.class,
Object.class));
}
@SuppressWarnings("unchecked")
@Override
public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) {
EntityType<!--?--> entityType;
try {
entityType = entityManager.getMetamodel().entity(
sourceType.getType());
} catch (IllegalArgumentException e) {
return false;
}
if (entityType == null)
return false;
// In my opinion, this is probably a bug in Hibernate.
// If the class has a @Id that is a complex type (such as another
// entity), then entityType.getIdType() will throw an
// IllegalStateException with the message "No supertype found"
// To work around this, get the idClassAttributes, and if there is
// exactly 1, use the java type of that.
Class<!--?--> idClass;
if (entityType.hasSingleIdAttribute()) {
idClass = entityType.getIdType().getJavaType();
} else {
Set<singularattribute<?, ?="">> idAttributes = new HashSet<>();
@SuppressWarnings("rawtypes")
Set attributes = entityType.getAttributes();
for (Attribute<!--?, ?--> attribute : (Set<attribute<?, ?="">>) attributes) {
if (attribute instanceof SingularAttribute<!--?, ?-->) {
SingularAttribute<!--?, ?--> singularAttribute = (SingularAttribute<!--?, ?-->) attribute;
if (singularAttribute.isId()) {
idAttributes.add(singularAttribute);
}
}
}
if (idAttributes.size() == 1) {
idClass = idAttributes.iterator().next().getJavaType();
} else {
return false;
}
}
return conversionService.canConvert(TypeDescriptor.valueOf(idClass),
targetType);
}
@Override
public Object convert(Object source, TypeDescriptor sourceType,
TypeDescriptor targetType) {
if (sourceType.getType() == targetType.getType()) {
return source;
}
if (source == null) {
return null;
}
EntityType<!--?--> entityType = entityManager.getMetamodel().entity(
sourceType.getType());
Object ret = entityManager.getEntityManagerFactory()
.getPersistenceUnitUtil().getIdentifier(source);
if (ret == null)
return null;
return conversionService.convert(ret,
TypeDescriptor.valueOf(entityType.getIdType().getJavaType()),
targetType);
}
}
</attribute<?,></singularattribute<?,>
package com.integralblue.candrews.converter;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import javax.persistence.EntityManager;
import javax.persistence.NoResultException;
import javax.persistence.PersistenceContext;
import javax.persistence.metamodel.Attribute;
import javax.persistence.metamodel.EntityType;
import javax.persistence.metamodel.SingularAttribute;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.core.convert.converter.ConditionalGenericConverter;
import org.springframework.format.support.FormattingConversionService;
import org.springframework.security.access.prepost.PostAuthorize;
/**
* Converts an ID (the field in the entity annotated with
* {@link javax.persistence.Id}) to the entity itself. This is similar to
* {@link org.springframework.data.repository.support.DomainClassConverter}
* but since we don't use Spring Data, we can't use that.
*/
public class IdToEntityConverter implements ConditionalGenericConverter {
@PersistenceContext
private EntityManager entityManager;
@Autowired
private FormattingConversionService conversionService;
@Override
public Set getConvertibleTypes() {
return Collections.singleton(new ConvertiblePair(Object.class,
Object.class));
}
@SuppressWarnings("unchecked")
@Override
public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) {
EntityType<!--?--> entityType;
try {
entityType = entityManager.getMetamodel().entity(
targetType.getType());
} catch (IllegalArgumentException e) {
return false;
}
if (entityType == null)
return false;
// In my opinion, this is probably a bug in Hibernate.
// If the class has a @Id that is a complex type (such as another
// entity), then entityType.getIdType() will throw an
// IllegalStateException with the message "No supertype found"
// To work around this, get the idClassAttributes, and if there is
// exactly 1, use the java type of that.
Class<!--?--> idClass;
if (entityType.hasSingleIdAttribute()) {
idClass = entityType.getIdType().getJavaType();
} else {
Set<singularattribute<?, ?="">> idAttributes = new HashSet<>();
@SuppressWarnings("rawtypes")
Set attributes = entityType.getAttributes();
for (Attribute<!--?, ?--> attribute : (Set<attribute<?, ?="">>) attributes) {
if (attribute instanceof SingularAttribute<!--?, ?-->) {
SingularAttribute<!--?, ?--> singularAttribute = (SingularAttribute<!--?, ?-->) attribute;
if (singularAttribute.isId()) {
idAttributes.add(singularAttribute);
}
}
}
if (idAttributes.size() == 1) {
idClass = idAttributes.iterator().next().getJavaType();
} else {
return false;
}
}
return conversionService.canConvert(sourceType,
TypeDescriptor.valueOf(idClass));
}
@Override
@PostAuthorize("hasPermission(returnObject, 'view')")
public Object convert(Object source, TypeDescriptor sourceType,
TypeDescriptor targetType) {
if (sourceType.getType() == targetType.getType()) {
return source;
}
EntityType<!--?--> entityType = entityManager.getMetamodel().entity(
targetType.getType());
Object id = conversionService.convert(source, sourceType,
TypeDescriptor.valueOf(entityType.getIdType().getJavaType()));
if (id == null
|| (id instanceof String && StringUtils.isEmpty((String) id))) {
return null;
}
Object ret = entityManager.find(targetType.getType(), id);
if (ret == null) {
throw new NoResultException(targetType.getType().getName()
+ " with id '" + id + "' not found");
}
return ret;
}
}
</attribute<?,></singularattribute<?,>