Home > Software Development > Custom User Types with JPA and Spring

Custom User Types with JPA and Spring

Thanks to Spring and JPA, one can easily and intuitively bind plain old Java objects to HTML forms, HTTP requests, and database tables. This beloved combination eliminates the tedium and droves of boiler plate code associated with developing data management applications. These tools support all Java primitives, wrappers, and even several high-level types natively. Fortunately, it is easy to add support for your own custom types.

In this example, I want to create a custom type called PhoneNumber. This type simply wraps a string representation of a phone number and provides a number of convenient methods and properties (e.g. accessing the area code) as well as validation (e.g. ensures the number is valid and converts a variety of formats into a canonical representation). Ideally, I would like to be able to:

  • Define PhoneNumber properties in my JPA Entities,
  • Get automatic validation with Bean Validation,
  • Get data binding and error binding with Spring.

Persistence

The JPA’s official answer to custom user types is the Embeddable (called a component in Hibernate terminology). Embeddable classes are merged into the tables of their containing entities and have no independent lifecycle. We could start by implementing PhoneNumber as an Embeddable:

@Embeddable
public class PhoneNumber {
	@Size(max = 20)
	private String value;

	public PhoneNumber(String value) {
		this.value = value;
	}

	public PhoneNumber() {
	}

	public String getValue() {
		return value;
	}

	public void setValue(String value) {
		this.value = value;
	}
}

Unfortunately, this has several major drawbacks:

  • The containing entity will have a column named “value”. Not only is this unintelligible, it prevents an entity from having multiple PhoneNumbers without explicitly defining the column name for each property.
  • Any queries that use phone numbers will have to dereference the value property and compare it with a String, not another PhoneNumber.
  • All validation failures will be related to value property of PhoneNumber.

These limitations force the consumer of PhoneNumber to understand its internal model which breaks encapsulation. For small internal project, this may be fine. It is far from ideal for making reusable types.

The JPA specification does not provide an alternative solution. However, if we are using Hibernate as our JPA implementation, we can take advantage of Hibernate’s support for user types. Let us start by creating a new implementation of PhoneNumber:

public class PhoneNumber implements Serializable {
	private String value;

	public PhoneNumber(String value) {
		this.value = value;
	}

	@Override
	public String toString() {
		return value;
	}

	@Override
	public boolean equals(Object object) {
		if (!(object instanceof PhoneNumber))
			return false;
		PhoneNumber that = (PhoneNumber) object;
		return value != null && that.value != null && value.equals(that.value);
	}

	@Override
	public int hashCode() {
		return value.hashCode();
	}

	public String getValue() {
		return value;
	}

	public void setValue(String value) {
		this.value = value;
	}
}

The next step is to create a UserType class that will be responsible for mapping database columns to and from instances of PhoneNumber. Here are a couple of important points:

  • While it is possible to map a single type to multiple columns (i.e. a composite type) I would strongly recommend the use of Embeddable instead. PhoneNumber is a special situation where we have a class with a single String representation that feels like a primitive.
  • It is much easier to handle and more efficient to work with immutable user-defined types. As such, our PhoneNumber implementation is immutable. While it is possible and not much more difficult to support mutable types, one should really only use this strategy for “primitive” concepts, which are almost always immutable.
public class PhoneNumberType implements UserType {
	@Override
	public Object nullSafeGet(ResultSet rs, String[] names, SessionImplementor session, Object owner) throws HibernateException, SQLException {
		String value = (String) StringType.INSTANCE.get(rs, names[0], session);
		if (value == null)
			return null;
		else
			return new PhoneNumber(value);
	}

	@Override
	public void nullSafeSet(PreparedStatement st, Object value, int index, SessionImplementor session) throws HibernateException, SQLException {
		if (value == null)
			StringType.INSTANCE.set(st, null, index, session);
		else
			StringType.INSTANCE.set(st, value.toString(), index, session);
	}

	@Override
	public int[] sqlTypes() {
		return new int[] { VarcharTypeDescriptor.INSTANCE.getSqlType() };
	}

	@Override
	public Class<PhoneNumber> returnedClass() {
		return PhoneNumber.class;
	}

	@Override
	public boolean equals(Object x, Object y) throws HibernateException {
		return x.equals(y);
	}

	@Override
	public int hashCode(Object x) throws HibernateException {
		return x.hashCode();
	}

	@Override
	public Object deepCopy(Object value) throws HibernateException {
		return value;
	}

	@Override
	public boolean isMutable() {
		return false;
	}

	@Override
	public Serializable disassemble(Object value) throws HibernateException {
		return (Serializable) value;
	}

	@Override
	public Object assemble(Serializable cached, Object owner) throws HibernateException {
		return cached;
	}

	@Override
	public Object replace(Object original, Object target, Object owner) throws HibernateException {
		return original;
	}
}

While there are a lot of methods, most of them are self-explanatory and I will leave you to the JavaDoc if you want more information. The next step is to tell Hibernate that whenever we encounter a property of type PhoneNumber, we want to use PhoneNumberType to processes it. This is accomplished by creating a TypeDef annotation.

@TypeDef(defaultForType = PhoneNumber.class, typeClass = PhoneNumberType.class)

This annotation either needs to be put on package or in any Entity class. Unfortunately, putting annotations on packages is very ugly in Java and, since these annotations apply globally, putting them on a single entity feels inappropriate. Unfortunately, the solution is a bit of a hack itself.

@MappedSuperclass
@TypeDef(defaultForType = PhoneNumber.class, typeClass = PhoneNumberType.class)
public class PhoneNumberType implements UserType {
}

Putting this annotation on the PhoneNumberType seems intuitive. But, we need to make Hibernate aware of its existence. By annotating PhoneNumberType as a MappedSuperclass, it will get processed by Hibernate. However, MappedSuperclasses will not get map directly to tables. In effect, it will be ignored. I admit this is a hack. If you do not like it then feel free to put the TypeDef on a package somewhere.

Regardless of how you create the type definition, any properties of type PhoneNumber will be mapped by the column’s name to a single VARCHAR field. You can control the length of the field with a Column annotation (on the containing property) as you would any other type.

Furthermore, when constructing queries you simply compare the PhoneNumber property with instances of PhoneNumber. Presumably, you would use JPQL string literals for PhoneNumber literals. I use the criteria query API so I do not have a test of this at hand.

Data Binding

The next step is to instruct Spring how to map Strings, generally coming from user input such as forms, to PhoneNumbers. This is accomplished through Spring’s Formatter Registry API. We can do quickly create a Formatter:

public class PhoneNumberFormatter implements Formatter<PhoneNumber>{
	@Override
	public PhoneNumber parse(String text, Locale locale) throws ParseException {
		return new PhoneNumber(text);
	}

	@Override
	public String print(PhoneNumber object, Locale locale) {
		return object.toString();
	}
}

Then we register it with Spring:

@Configuration
@EnableWebMvc
public class PhoneNumberConfig extends WebMvcConfigurerAdapter {
	@Override
	public void addFormatters(FormatterRegistry registry) {
		registry.addFormatter(new PhoneNumberFormatter());
	}
}

However, we succeeded in getting Hibernate to automatically recognize the PhoneNumberType, so, we might want to make Spring automatically recognize the PhoneNumberFormatter. We can do this by annotating the Formatter thusly:

@Component
public class PhoneNumberFormatter implements Formatter<PhoneNumber> {
}

and then updating the registration process to use package scanning:

@Configuration
@EnableWebMvc
public class PhoneNumberConfig extends WebMvcConfigurerAdapter {
	@Inject
	private List<Formatter<?>> formatters;

	@Override
	public void addFormatters(FormatterRegistry registry) {
		for (Formatter<?> formatter : formatters)
			registry.addFormatter(formatter);
	}
}

Now, Spring can convert from a String to a PhoneNumber and will do so automatically any time in encounters a ModelAttribute with a property of type PhoneNumber. Furthermore, any input fields (e.g. in a form) will automatically render the PhoneNumber properties properly.

Combining the Two

Both the PhoneNumberType and the PhoneNumberFormatter serve very similar functions, work in tandem to create a new “primitive” throughout the entire stack, and have very similar implementations. We could combine the two.

@Component
@MappedSuperclass
@TypeDef(defaultForType = PhoneNumber.class, typeClass = PhoneNumberType.class)
public class PhoneNumberType implements UserType, Formatter<PhoneNumber> {
	@Override
	public PhoneNumber parse(String text, Locale locale) {
		return new PhoneNumber(text);
	}

	@Override
	public String print(PhoneNumber object, Locale locale) {
		return object.toString();
	}

	@Override
	public Object nullSafeGet(ResultSet rs, String[] names, SessionImplementor session, Object owner) throws HibernateException, SQLException {
		String value = (String) StringType.INSTANCE.get(rs, names[0], session);
		if (value == null)
			return null;
		else
			return parse(value, null);
	}

	@Override
	public void nullSafeSet(PreparedStatement st, Object value, int index, SessionImplementor session) throws HibernateException, SQLException {
		if (value == null)
			StringType.INSTANCE.set(st, null, index, session);
		else
			StringType.INSTANCE.set(st, print((PhoneNumber) value, null), index, session);
	}

	...
}

Some people might call this tight coupling of independent concerns. That is valid. However, this setup is very conducive to abstraction making it trivial to create further primitives. The choice is yours.

Validation

Finally, we need to resolve the validation issue. When an instance of an object containing a PhoneNumber property is validated, we want any errors to originate from the PhoneNumber property, not the value property within the PhoneNumber class itself. This is actually not possible with Java Beams Validation. But, it turns out that is the wrong tool for the job anyway.

Consider another primitive: an integer. If someone attempts to submit a sequence of letters for a form field that maps to an integer property, Spring will fail when attempting to create integer and report this fact in its error tracking system. Since we are creating primitive like types, we want the same thing to happen for our PhoneNumber. If someone submits an invalid phone number, we want Spring to fail. We do not want Spring to create an invalid PhoneNumber. Since they are immutable, it does not make sense to have an invalid PhoneNumber. As such, the trick is not to use Beans Validation, but rather to implement validation in the constructor.

While this may seem tedious at first (after all, Beans Validation is so much fun). However, most primitives have either no validation or very complicated validation (one the motivations to use them in the first place). It turns out, in my use cases anyway, that Beans Validation is not incredibly useful for this situation.

So, we update the constructor of PhoneNumber with any validation. Simply throw Spring’s Assert methods will throw an IllegalArgumentException if the condition fails. This is sufficient for Spring to terminate binding and register an error.

public PhoneNumber(String value) {
	Assert.isTrue(...);
	this.value = value;
}

Conclusion

Sorry I do not have a sample project for you to download on this one. If I get a chance in the future I will create one. However, if you are using Hibernate/JPA and Spring is pretty trivial to experiment.

  1. No comments yet.