As it was discussed in one of the previous posts JPA: Mapping Enums the right way, JPA 2.1 standardized AttributeConverters, which can, for example, be used to map Java enums in more controlled/predicted way. Let's quickly recap how it can be done.
Using attribute converter to map java enum
Let's assume we have a PhoneType enum and a simple entity that has this enum as a field.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Simple enum | |
public enum PhoneType { | |
HOME( 1, "home" ), CELL( 2, "cell" ), WORK( 3, " work" ),; | |
private Integer id; | |
private String name; | |
PhoneType(int id, String name) { | |
this.id = id; | |
this.name = name; | |
} | |
} | |
// And an entity with that enum: | |
@Entity | |
@Table(name = "some_table") | |
public class SomeEntity { | |
@Id | |
@GeneratedValue | |
private int id; | |
private PhoneType phoneType; | |
// ..... | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public static class PhoneTypeAttributeConverter implements AttributeConverter<PhoneType, Integer> { | |
@Override | |
public Integer convertToDatabaseColumn(PhoneType attribute) { | |
return attribute.getId(); | |
} | |
@Override | |
public PhoneType convertToEntityAttribute(Integer dbData) { | |
if ( dbData == null ) { | |
return null; | |
} | |
return Arrays.stream( PhoneType.values() ) | |
.filter( item -> item.getId().equals( dbData ) ) | |
.findFirst().orElseThrow( IllegalArgumentException::new ); | |
} | |
} |
What if we have one more enum? For example let's add AddressType. The process would be very similar - we need to create an attribute converter class and add @Covert annotation on the corresponding filed.
But what if we have even more enums that should be mapped this way? This would mean that we need to write a converter for each enum and basically duplicate the same logic in each of them...
Handy utility for Enums
That's where an utility class comes. In Java it is not possible to build inheritance hierarchies of enums, but it is possible for enum to implement an interface. So if we have enums that we want to map by their id to a database columns we can create an interface similar to DbEnumConstant:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public interface DbEnumConstant<ID> { | |
ID getId(); | |
String getName(); | |
} |
Now that our enums have something in common we can generalize the operation of finding an enum value by id. For example next utility static method can be introduced:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* Find an enum value by it's {@code id} in {@code values} array. | |
* | |
* @param id an {@code id} of the enum that we search for | |
* @param values an array of values to search | |
* @param <ID> an id type for the enum | |
* @param <T> an actual enum type | |
* @return if found - enum constant value, otherwise throws an {@link IllegalArgumentException} | |
*/ | |
public static <ID, T extends DbEnumConstant<ID>> T findByID(ID id, T[] values) { | |
return Arrays.stream( values ) | |
.filter( item -> item.getId().equals( id ) ) | |
.findFirst().orElseThrow( IllegalArgumentException::new ); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public class DbEnumConstantHelper<ActualEnum extends DbEnumConstant<ID>, ID> { | |
private final Class<ActualEnum> type; | |
private final ActualEnum[] enumConstants; | |
public DbEnumConstantHelper(Class<ActualEnum> type) { | |
this.type = type; | |
this.enumConstants = type.getEnumConstants(); | |
} | |
/** | |
* Finds enum {@link ActualEnum} by id using its {@link DbEnumConstant#getId()} | |
* method. | |
* | |
* @param id an id to search by | |
* @return {@link ActualEnum} enum constant if found. Could throw | |
* {@link NoSuchElementException} | |
* if there's no constant for given {@code id} | |
*/ | |
public ActualEnum byId(ID id) { | |
return byFilter( elem -> elem.getId().equals( id ) ); | |
} | |
/** | |
* Finds enum {@link ActualEnum} by id using its {@link DbEnumConstant#getId()} | |
* method. If one is not found - returns a default value. | |
* | |
* @param id an id to search by | |
* @param defaultValue a default value to return if nothing is found for given | |
* {@code id} | |
* @return {@link ActualEnum} enum constant if found, {@code defaultValue} | |
* otherwise | |
*/ | |
public ActualEnum byIdOrDefault(ID id, ActualEnum defaultValue) { | |
return byFilterOrDefault( elem -> elem.getId().equals( id ), defaultValue ); | |
} | |
/** | |
* Finds enum {@link ActualEnum} by id using its {@link DbEnumConstant#getId()} | |
* method. If one is not found - will throw an exception provided by the passed | |
* {@code exceptionSupplier}. | |
* | |
* @param id an id to search by | |
* @param exceptionSupplier provides an exception that will be thrown in case | |
* nothing is found for a given {@code id} | |
* @param <X> an exception type to be thrown in case nothing is found | |
* @return {@link ActualEnum} enum constant if found, otherwise throws an | |
* exception provided by {@code exceptionSupplier} | |
* | |
* @throws X when nothing is found for a given {@code id} | |
*/ | |
public <X extends Throwable> ActualEnum byIdOrThrow( | |
ID id, | |
Supplier<? extends X> exceptionSupplier) throws X { | |
return byFilterOrThrow( elem -> elem.getId().equals( id ), exceptionSupplier ); | |
} | |
/** | |
* Finds enum {@link ActualEnum} by name using its {@link DbEnumConstant#getName()} | |
* method. | |
* | |
* @param name a name to search by | |
* @return {@link ActualEnum} enum constant if found. Could throw | |
* {@link NoSuchElementException} if there's no constant for given {@code name} | |
*/ | |
public ActualEnum byName(String name) { | |
return byFilter( elem -> elem.getName().equals( name ) ); | |
} | |
/** | |
* Finds enum {@link ActualEnum} by name using its {@link DbEnumConstant#getName()} | |
* method. If one is not found - returns a default value. | |
* | |
* @param name a name to search by | |
* @param defaultValue a default value to return if nothing is found for given | |
* {@code name} | |
* @return {@link ActualEnum} enum constant if found, {@code defaultValue} | |
* otherwise | |
*/ | |
public ActualEnum byNameOrDefault(String name, ActualEnum defaultValue) { | |
return byFilterOrDefault( elem -> elem.getName().equals( name ), defaultValue ); | |
} | |
/** | |
* Finds enum {@link ActualEnum} by name using its {@link DbEnumConstant#getName()} | |
* method. If one is not found - will throw an exception provided by the passed | |
* {@code exceptionSupplier}. | |
* | |
* @param name an name to search by | |
* @param exceptionSupplier provides an exception that will be thrown in case | |
* nothing is found for a given {@code name} | |
* @param <X> an exception type to be thrown in case nothing is found | |
* @return {@link ActualEnum} enum constant if found, otherwise throws an exception | |
* provided by {@code exceptionSupplier} | |
* | |
* @throws X when nothing is found for a given {@code name} | |
*/ | |
public <X extends Throwable> ActualEnum byNameOrThrow( | |
String name, | |
Supplier<? extends X> exceptionSupplier) throws X { | |
return byFilterOrThrow( elem -> elem.getName().equals( name ), exceptionSupplier ); | |
} | |
private ActualEnum byFilter(Predicate<ActualEnum> predicate) { | |
return Arrays.stream( enumConstants ) | |
.filter( predicate ) | |
.findAny() | |
.get(); | |
} | |
private ActualEnum byFilterOrDefault( | |
Predicate<ActualEnum> predicate, | |
ActualEnum defaultValue) { | |
return Arrays.stream( enumConstants ) | |
.filter( predicate ) | |
.findAny() | |
.orElse( defaultValue ); | |
} | |
private <X extends Throwable> ActualEnum byFilterOrThrow( | |
Predicate<ActualEnum> predicate, | |
Supplier<? extends X> exceptionSupplier) throws X { | |
return Arrays.stream( enumConstants ) | |
.filter( predicate ) | |
.findAny() | |
.orElseThrow( exceptionSupplier ); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public enum AddressType implements DbEnumConstant<Long> { | |
HOME( 1L, "home address" ), WORK( 2L, "work address" ),; | |
public static final DbEnumConstantHelper<AddressType, Long> HELPER = new DbEnumConstantHelper<>( AddressType.class ); | |
private Long id; | |
private String name; | |
// ..... | |
} | |
// and PhoneType enum: | |
public enum PhoneType implements DbEnumConstant<Integer> { | |
HOME( 1, "home" ), CELL( 2, "cell" ), WORK( 3, " work" ),; | |
public static final DbEnumConstantHelper<PhoneType, Integer> HELPER = new DbEnumConstantHelper<>( PhoneType.class ); | |
private Integer id; | |
private String name; | |
// ..... | |
} |
AttributeConverter using enum helper
Now that we have all of the above in place we can write an abstract generic attribute converter for our enums with implementations for AddressType and PhoneType:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* A base {@link AttributeConverter} to be implemented by enum converters. | |
*/ | |
public abstract class AbstractAttributeConverter<ActualEnum extends DbEnumConstant<ID>, ID> | |
implements AttributeConverter<ActualEnum, ID> { | |
/** | |
* @return a {@link DbEnumConstantHelper} for an enum converted by this converter | |
*/ | |
protected abstract DbEnumConstantHelper<ActualEnum, ID> helper(); | |
@Override | |
public ID convertToDatabaseColumn(ActualEnum attribute) { | |
return attribute.getId(); | |
} | |
@Override | |
public ActualEnum convertToEntityAttribute(ID dbData) { | |
// if the value in the DB is null we will return null as well | |
if ( dbData == null ) { | |
return null; | |
} | |
// Otherwise, a helper is used to find a matching enum constant, | |
// and if one cannot be found an exception is thrown. | |
return helper().byIdOrThrow( | |
dbData, | |
() -> new IllegalStateException( | |
"There is no corresponding enum for the given value from the database" ) | |
); | |
} | |
} | |
/** | |
* Implementation for AddressType enum. | |
*/ | |
public class AddressTypeAttributeConverter extends AbstractAttributeConverter<AddressType, Long> { | |
@Override | |
protected DbEnumConstantHelper<AddressType, Long> helper() { | |
return AddressType.HELPER; | |
} | |
} | |
/** | |
* Implementation for PhoneType enum. | |
*/ | |
public class PhoneTypeAttributeConverter extends AbstractAttributeConverter<PhoneType, Integer> { | |
@Override | |
protected DbEnumConstantHelper<PhoneType, Integer> helper() { | |
return PhoneType.HELPER; | |
} | |
} |
We can see that all the conversion logic is placed in the abstract converter and implementations just need to pass a helper so the right find operations would be applied.
Conclusions
We looked once more at how Java enums can be mapped in JPA using AttributeConverter and provided a way to reduce code duplication in such converters. Even though, at first, it might not look like this amount of code is less then writing a simple converter. But it really pays of when you have quite a few enums to map in such a way. Also this enum helper, that was introduced, is useful outside the converters as well, it gives an easy access to finding enums by their properties.All the code samples can be found on GitHub.
To be continued ...
COMMENTS