In my datamodel a have many entities where attributes are mapped to enumerations like this:
#Enumerated(EnumType.STRING)
private MySpecialEnum enumValue;
MySpecialEnum defines some fixed values. The mapping works fine and if the database holds a NULL-value for a column I get NULL in the enumValue-attribute too.
The problem is, that my backend module (where I have no influence on) uses spaces in CHAR-columns to identify that no value is set. So I get an IllegalArgumentException instead of a NULL-value.
So my question is: Is there a JPA-Event where I can change the value read from the database before mapping to the enum-attribute?
For the write-access there is the #PrePersist where I can change Null-values to spaces. I know there is the #PostLoad-event, but this is handled after mapping.
Btw: I am using OpenJpa shipped within WebSphere Application Server.
You could map the enum-type field as #Transient (it will not be persisted) and map another field directly as String, synchronizing them in #PostLoad:
#Transient
private MyEnum fieldProxy;
private String fieldDB;
#PostLoad
public void postLoad() {
if (" ".equals(fieldDB))
fieldProxy = null;
else
fieldProxy = MyEnum.valueOf(fieldDB);
}
Use get/setFieldProxy() in your Java code.
As for synchronizing the other way, I'd do it in a setter, not in a #PreUpdate, as changes to #Transient fields probably do not mark the entity as modified and the update operation might not be triggered (I'm not sure of this):
public void setFieldProxy(MyEnum value) {
fieldProxy = value;
if (fieldProxy == null)
fieldDB = " ";
else
fieldDB = value.name();
}
OpenJPA offers #Externalizer and #Factory to handle "special" database values.
See this: http://ci.apache.org/projects/openjpa/2.0.x/manual/manual.html#ref_guide_pc_extern_values
You might end up with something like this: not tested...
#Factory("MyClass.mySpecialEnumFactory")
private MySpecialEnum special;
...
public static MySpecialEnum mySpecialEnumFactory(String external) {
if(StringUtils.isBlank(external) return null; // or why not MySpecialEnum.NONE;
return MySpecialEnum.valueOf(external);
}
Related
I am using the JOOQ library in order to fetch the result from a select query into my custom DTO class. Because my Custom DTO class has an ENUM Type field mapped as an Integer Column in my database I am using a custom data type converter. The query I perform is just a basic select query:
public TestSuiteDto findByAuditTestSuiteId(Integer auditTestSuiteId) {
TestSuiteJTable ts = TestSuiteJTable.TEST_SUITE;
AuditTestSuiteJTable ats = AuditTestSuiteJTable.AUDIT_TEST_SUITE;
List<TestSuiteDto> result = dsl.select(
ts.ID,
ts.DESCRIPTION,
ts.PROFILE_TYPE_ID,
ts.CREATED,
ts.ACTIVE,
ts.DEPRECATION,
ts.FEEDBACK_QUESTIONNAIRE_ID)
.from(ts
.join(ats).on(ts.ID.eq(ats.TEST_SUITE_ID)))
.where(ats.ID.eq(auditTestSuiteId))
.fetchInto(TestSuiteDto.class);
//do some stuff before returning
return result.get(0);
}
My custom DTO class looks like this
#Data
public class TestSuiteDto {
private Integer id;
private String description;
private ProfileType profileType;
private LocalDateTime created;
private boolean active;
private LocalDateTime deprecation;
private Integer feedbackQuestionnaireId;
}
The problem is that during the fetching process the SETTER of the DTO class is never triggered for the ENUM type field e.g. profileType even though I have configured a custom data type converter:
#Slf4j
public class ProfileTypeConverter implements Converter<Integer, ProfileType> {
#Override
public ProfileType from(Integer databaseObject) {
log.info("ProfileTypeConverter.from {} -> {}", databaseObject, ProfileType.getFromId(databaseObject));
return ProfileType.getFromId(databaseObject);
}
#Override
public Integer to(ProfileType userObject) {
log.info("ProfileTypeConverter.to");
return userObject.getId();
}
#Override
public Class<Integer> fromType() {
log.info("ProfileTypeConverter.fromType");
return Integer.class;
}
#Override
public Class<ProfileType> toType() {
log.info("ProfileTypeConverter.toType");
return ProfileType.class;
}
}
I have added some logs just to check if the converter is triggered at all and I see that the converter is triggered as expected (from method of the Converter class is called during the execution of the JOOQ SQL query). I have also delombok my DTO class in order to add logs in SETTERs and GETTERs and see if those are also properly called. I found out that all the SETTERs are properly called except for the profileType one. Because of that when I retrieve the DTO from the result list the value of the profileType field is null. The column in my database (mysql) that maps to the profileType ENUM is called PROFILE_TYPE_ID and it is of type Integer. I have also configured a forcedType in the pom.xml following the examples on JOOQ documentation webpage.
<forcedType>
<includeExpression>${jdbc.database}.TEST_SUITE.PROFILE_TYPE_ID</includeExpression>
<userType>mypackage.type.ProfileType</userType>
<converter>mypackage.converter.ProfileTypeConverter</converter>
</forcedType>
and this is how I have configured the ProfileType Field in pom.xml
<field>
<expression>${jdbc.database}.TEST_SUITE.PROFILE_TYPE_ID</expression>
<fieldIdentifier>
<expression>PROFILE_TYPE_ID</expression>
</fieldIdentifier>
<fieldMember>
<expression>profileType</expression>
</fieldMember>
<fieldGetter>
<expression>getProfileType</expression>
</fieldGetter>
<fieldSetter>
<expression>setProfileType</expression>
</fieldSetter>
</field>
JOOQ version: 3.14.16, Java 8
Why do things behave this way?
The reason is that you have a name mismatch:
Query
ts.PROFILE_TYPE_ID,
DTO
private ProfileType profileType;
If you want to rely on the reflection based DefaultRecordMapper, then you must name those things accordingly, otherwise, they won't be mapped. The fact that you have a converter is irrelevant, if the names don't match. Imagine you had 20 columns of type ProfileType. You wouldn't want to have DefaultRecordMapper map values purely based on their type.
Regarding your comments:
I have added some logs just to check if the converter is triggered at all and I see that the converter is triggered as expected (from method of the Converter class is called during the execution of the JOOQ SQL query)
Yes of course. The Converter belongs to the projected column. The conversion happens before the mapping (i.e. the into(TestSuiteDto.class) call)
Solutions
There are multiple alternatives to solve this:
Call your DTO attribute profileTypeId, or to add JPA annotations to it to map between SQL names and Java names
Rename your SQL column (in DDL)
Alias your SQL column using PROFILE_TYPE_ID.as("profile_type")
Use a computed column PROFILE_TYPE and attach the converter to that, keeping the PROFILE_TYPE_ID as it is (you can also use client side computed columns for that, in order not to affect your schema)
Use type safe constructor based mapping, rather than reflection based mapping, e.g. using fetch(Records.mapping(TestSuiteDto::new))
There are probably more possible solutions.
I have a field in a class that should only be accessed directly from a getter. As an example...
public class CustomerHelper {
private final Integer customerId;
private String customerName_ = null;
public CustomerHelper(Integer customerId) {
this.customerId = customerId;
}
public String getCustomerName() {
if(customerName_ == null){
// Get data from database.
customerName_ = customerDatabase.readCustomerNameFromId(customerId);
// Maybe do some additional post-processing, like casting to all uppercase.
customerName_ = customerName_.toUpperCase();
}
return customerName_;
}
public String getFormattedCustomerInfo() {
return String.format("%s: %s", customerId, getCustomerName());
}
}
So even within the class itself a function like getFormattedCustomerInfo should not be able to access it via customerName_. Is there a way to enforce a class not access a field directly aside from the provided getter function?
There is no such mechanism in Java (or at least I think there should not be). If you are sure that getFormattedCustomerInfo should be prohibited from direct access to customerName_, create another class and compose them.
I would recommend CustomerInfoFormatter.
Also, I would change customerName_ to customerName as the language supports privacy by explicit declaration and it is not needed to add more indicators.
It looks like you are trying to cache the database value, and want to protect against accessing a value which has yet to be cached.
If this is true, then the variable customerName_ should not exist in the CustomerHelper class; the cached value should exist closer to the database.
The method customerDatabase.readCustomerNameFromId(customerId) should first look at a cache, and if the cache is empty, call the database and cache the result.
Effectively, customerName_ becomes a value in the cache: Map<Integer, String> cache where the key is customerId.
Let's pretend a RESTful service receives a PATCH request to update one or more fields of an entity that might have tens of fields.
#Entity
public class SomeEntity {
#Id
#GeneratedValue
private Long id;
// many other fields
}
One dirty way to patch the corresponding entity is to write something like this:
SomeEntity patch = deserialize(json);
SomeEntity existing = findById(patch.getId());
if (existing != null)
{
if (patch.getField1() != null)
{
existing.setField1(patch.getField1());
}
if (patch.getField2() != null)
{
existing.setField2(patch.getField2());
}
if (patch.getField3() != null)
{
existing.setField3(patch.getField3());
}
}
But this is insane! And if I want to patch 1 to many & other associations of the entity the insanity could even become hazardous!
Is there a sane an elegant way to achieve this task?
Modify the getter's of SomeEntity and apply check, if any value is blank or null just return the corresponding entity object value.
class SomeEntity {
transient SomeEntity existing;
private String name;
public String getName(){
if((name!=null&&name.length()>0)||existing==null){
return name;
}
return existing.getName();
}
}
You can send an array containing the name of the fields you are going to patch. Then, in the server side, by reflection or any field mapping, set each field to the entity. I have already implemented that and it works, thought my best advice is this:
Don't publish an endpoint to perform a "generic" PATCH (modification), but one that performs a specific operation. For instance, if you want to modify an employee's address, publish an endpoint like:
PUT /employees/3/move
that expects a JSON with the new address {"address" : "new address"}.
Instead of reinventing the wheel by writing the logic yourself, why don't you use a mapping library like Dozer? You want to use the 'map-null' mapping property: http://dozer.sourceforge.net/documentation/exclude.html
EDIT I am not sure whether or not it would be possible to map a class onto itself. You could use an intermediary DTO, though.
In an existing Java EE application there is a JPA entity which contains a string array as a field. Bad, but it is what it is. There are no annotations for it whatsoever and running the application results in Hibernate (on JBoss EAP 6.1 Alpha and MySQL 5.6) storing it as TINYBLOB. This obviously fails as soon as the string array contains more than a couple values but yet it works fine as long as the array is not too big.
Is there any way to force via JPA 2 (not Hibernate!) annotation that the field is treated as BLOB instead of TINYBLOB?
Using the following will actually create a column of type BLOB during setup but when trying to store an instance of the entity it still fails with
java.lang.ClassCastException: [Ljava.lang.String; cannot be cast to java.lang.String"
Field annotation definition:
#Lob
#Column(columnDefinition = "blob")
I think you are on a write way. To avoid ClassCastException try to add converter calss and serialize it manually. Add annotation #Convert(converter = YoursConvertor.class) and implement convertor class, something like this:
public class YoursConvertor implements AttributeConverter<String[], String> {
#Override
public String convertToDatabaseColumn(String[] meta) {
String concatString = "";
//... some concatinantion etc
return concatString;
}
#Override
public String[] convertToEntityAttribute(String dbData) {
String[] result = null;
if (dbData != null && dbData.length() > 0) {
//... unconcat
}
return result;
}
}
Maybe it must be String[], byte[] - you should try
I have an existing database that I am now connecting to using hibernate. I cannot change the data in it at the moment and have everything working apart from a single column.
I have a status column that has the values:
new
mailed
in
out
And the column is mapped as follows:
#Column(name = "STATUS", nullable = false, length = 50)
#Enumerated(EnumType.STRING)
private TeamMemberStatus status;
I would REALLY like (for application reasons) to have this column mapped as a Java Enum (TeamMemberStatus), but due to the fact that 'new' is a keyword in Java I cannot have that as an enum member.
If I have the enum contstants NEW, MAILED, IN and OUT hibernate fails as inside EnumType it does a Enum.valueOf().
Is there any way for me to map this to my Enum without having to write a complex UserType?
-- added content
My Enum like this:
public enum TeamMemberStatus {
NEW, MAILED, IN, OUT
}
is a valid Java enum, but not matching the case of the database. If I change it to match the database like:
public enum TeamMemberStatus {
new, mailed, in, out
}
It won't compile as 'new' is a Java reserved word.
If you can use a SQL UPPER statement at database, It will work without using any UserType
UPDATE
Well, It can not be The nicest solution but it solves what you want
#Entity
public class WrapperEntity {
private TeamMemberStatus memberStatus;
#Transient
private TeamMemberStatus getMemberStatus() {
return this.memberStatus;
}
public void setMemberStatus(TeamMemberStatus memberStatus) {
this.memberStatus = memberStatus;
}
#Column(name="STATUS", nullable=false, length=50)
public String getMemberStatusAsString() {
return memberStatus.name().toLowerCase();
}
public void setMemberStatusAsString(String memberStatus) {
this.setsetMemberStatus(TeamMemberStatus.valueOf(memberStatus.toUpperCase()));
}
}
If your Database values are "new", "mailed", "in" and "out" then your Enum need exactly the same names. - I believe that the problem is, that your Enums are in capital letters but your data base values not.