I have a Spring Boot application running using JPA and Hibernate to automagically manage my entities. When I created this application, I used an older version of JPA that didn't have support for Java 8 DateTime API. However, without a lot of knowledge about JPA, I used LocalDateTime in my entities and it worked! Not having to know about the underlying database structure was great!
Until now...
I am upgrading JPA to a version that does support LocalDateTime, and I am facing an error with the way JPA is using this field. It used to save this object as a VARBINARY (tinyblob) field in my MySQL database, but now it is smart and expects it to be a TIMESTAMP type. Which means that when I start my application using the configuration spring.jpa.hibernate.ddl-auto=validate I get the error:
...
Caused by: org.hibernate.tool.schema.spi.SchemaManagementException:
Schema-validation: wrong column type encountered in column [answer_time] in table [user_answer];
found [tinyblob (Types#VARBINARY)], but expecting [datetime (Types#TIMESTAMP)]
So now I am kinda lost on how to convert these fields to their new timestamp types. I was thinking about using FlyWay to write a migration script, but I have no idea how JPA stored the object as blob. When print a VARBINARY field as string this is what it looks like:
’ sr
java.time.Ser]º"H² xpw ã
!;:;Ö#x
This is how my entity looks like (which was unchanged during the upgrade):
#Entity
#Table(name = "user_answer")
public class UserAnswer {
private Long id;
private LocalDateTime answerTime;
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public LocalDateTime getAnswerTime() {
return answerTime;
}
public void setAnswerTime(LocalDateTime answerTime) {
this.answerTime = answerTime;
}
}
How can I update my database so it converts the old VARBINARY fields that it used to store LocalDateTime data to TIMESTAMP fields?
What I would try (after backing up the DB!) :
Keep the old JPA API + implementation (Hibernate) versions.
Keep the old LocalDateTime field.
Add another java.sql.Date field to your entity. Make sure to annotate it properly etc. so that Hibernate knows exactly how the column should be defined.
For all entities:
Load each entity, read the LocalDateTime, convert and store it to the DateTime field, merge().
Remove the DateTime field.
Remove the column for the old LocalDateTime from the table.
Change the type of the DateTime field to LocalDateTime.
Upgrade the JPA API + implementation (Hibernate) versions.
JPA impl (Hibernate?) should store the DateTime as TIMESTAMP.
The JDBC driver should be able to pick up the TIMESTAMP into LocalDateTime.
Also, consider ZonedDateTime rather than LocalDateTime.
Related
I'm trying to use the jOOQ fetchInto() method to map to an existing Hibernate model Organization (class and its inheritances are below).
Organization organization = jooq().select().from(ORGANIZATION).fetchOne().into(Organization.class);
The problem I have is that I can't really understand what happens in DefaultRecordMapper as I feel I'm not entirely familiar with all the terms that are used. I'm trying to figure out how it applies to the Hibernate classes that are in my codebase.
So far what I've tried:
Use the jOOQ generated POJO's to see if it retrieves and maps the data at all (works).
Add a constructor, getters and setters to the Organization Hibernate model.
Add #Column annotation to name in the Organization Hibernate model.
What works:
id field gets mapped correctly.
What doesn't work:
name field doesn't get mapped (null).
createdAt and modifiedAt fields do not get mapped (null).
My question is: Is there something I am overlooking with the mapping and what are the things I should look at concerning the classes, fields, constructors and annotations with Hibernate models? I want to eventually map all the Hibernate models in the codebase and use fetchInto to do that.
Thanks! :)
#Entity
public class Organization extends BaseModel {
#Required public String name;
//... a lot of other code
}
#MappedSuperclass
public class BaseModel extends Model {
/** The datetime this entity was first saved. Automatically set by a JPA prePersist */
#NoBinding
#Column
#Type(type = "org.jadira.usertype.dateandtime.joda.PersistentDateTime")
public DateTime createdAt;
/** The datetime this entity was last modified. Automatically set by a JPA preUpdate */
#NoBinding
#Column
#Type(type = "org.jadira.usertype.dateandtime.joda.PersistentDateTime")
public DateTime modifiedAt;
//...
}
#MappedSuperclass
public class Model extends GenericModel { // Both Model and GenericModel are from the Play Framework
#Id
#GeneratedValue
public Long id;
public Model() {
}
public Long getId() {
return this.id;
}
public Object _key() {
return this.getId();
}
}
jOOQ doesn't support all the many JPA and Hibernate specific annotations. Historically, it supported a few JPA annotations (because why not), but full interop would be excessive and investing product development time in the wrong places. jOOQ is by no means a JPA implementation.
Step 0: Why didn't (some) of the mappings work?
As mentioned before, not all JPA specification is implemented. For example, a known issue is that #Column annotations are still mandatory in jOOQ:
https://github.com/jOOQ/jOOQ/issues/4586
There might be other such limitations, which could be considered bugs. Feel free to report them if you want to continue down this path: https://github.com/jOOQ/jOOQ/issues/new/choose
But things like #MappedSuperclass or #Type are unlikely to ever be supported by jOOQ.
Step 1: Do you really need it?
You've decided to create and run your query with jOOQ. I imagine your actual query is much more complex than what you're showing, because for that particular query, you don't need jOOQ.
Do you really need to map to Hibernate entities? Because even when you use Hibernate, the recommended approach is to use entities only when you're going to modify them and store the delta back to the database. If that's the case, see step 2 below. If it's not the case, why not use jOOQ's own mapping functionality to work with any style of jOOQ supported POJO?
Step 2: Use Hibernate to execute the jOOQ query
If you're using jOOQ only to build a rather complex SQL query and you need Hibernate entities as a result, then use Hibernate to execute the jOOQ query as documented here. A small utility should be enough:
public static <E> List<E> nativeQuery(EntityManager em, org.jooq.Query query, Class<E> type) {
Query result = em.createNativeQuery(query.getSQL(), type);
List<Object> values = query.getBindValues();
for (int i = 0; i < values.size(); i++)
result.setParameter(i + 1, values.get(i));
return result.getResultList();
}
I am using Netbeans 12.0, and to create my Entity classes for my DB tables, I use Netbeans Entity Classes from Database. It works great other than the fact it defaults all my date columns to the pre Java 8 Date classes, and I recently discovered removing the #Temporal annotation from my class variables (correspond with columns in my tables) with LocalDate does not cause issues.
This is what Netbeans creates:
#Column(name = "delivery_date")
#Temporal(TemporalType.DATE)
private Date deliveryDate;
Here is my updated version that works:
#Column(name = "delivery_date")
private LocalDate deliveryDate;
Is it possible to make Netbeans use the LocalDate class when creating my entity classes from my database?
I have many tables which have corresponding entities in my Java app, while it is theoretically feasible for me to go through each one and replace the columns with dates, it does not sound like fun especially for future entities.
My project is Java 8, using JPA 2.2 and EclipseLink 2.7.7.
I'm afraid that's not possible, when using regular Netbeans version.
Here is snippet from class JavaPersistenceGenerator responsible for that mapping:
String getMemberType(EntityMember m) {
String memberType = m.getMemberType();
if ("java.sql.Date".equals(memberType)) { //NOI18N
memberType = "java.util.Date";
} else if ("java.sql.Time".equals(memberType)) { //NOI18N
memberType = "java.util.Date";
} else if ("java.sql.Timestamp".equals(memberType)) { //NOI18N
memberType = "java.util.Date";
}
return memberType;
}
As you can see java.util.Date is hard-coded there and cannot be configured.
You can checkout Netbeans sources from GitHub, change java.util.Date to java.time.LocalDate in class:
org.netbeans.modules.j2ee.persistence.wizard.fromdb.JavaPersistenceGenerator
Rebuild Netbeans sources and replace file:
<<NETBEANS_INSTALLATION_FOLDER>>/java/modules/org-netbeans-modules-j2ee-persistence.jar
With corrected version from:
<<NETBEANS_SOURCES_FOLDER>>/nbbuild/netbeans/java/modules
That will lead to the desired result.
My application uses view object-entity pair for the frontend-backend interaction. The frontend only uses VOs, while the backend only talks database with entities. And there are a VO <-> entities conversion
My entity class has 2 timestamp properties, createTimestamp and lastUpdateTimestamp, corresponding to two non-nullable columns in its data table. But the VO never has these 2 properties.
My current problem: since a VO doesn't contain Timestamp properties, the entity converted from the VO will have the 2 Timestamp properties to be null, and when I do that entity update, error occurs from the database because it thinks I am trying to set the Timestamp columns into null which is not permitted.
I like to know how do we deal with this issue. Is there some way to make database ingore these 2 Timestamp on update, or is there an "elegant" way to obtain the Timestamp values before I update the entity? I often need to update a list of entities in one shot.
Solution that I found
I added a attribute "updatable" under the #Column annotation, and it seems to solve my issue.
i.e. #Column(name = "CREATE_STAMP", nullable = false, updatable = false)
Hinted from this post
Creation timestamp and last update timestamp with Hibernate and MySQL
Set the default value against the DB column for timestamp columns, that means if provided in the INSERT query(through VO) it will take it, otherwise it will be default.
Update: You can use an Hibernate interceptor instead, that's what they are for. For example, the entities that need such fields could implement the following interface:
public interface Auditable {
Date getCreated();
void setCreated(Date created);
Date getModified();
void setModified(Date modified);
}
Then the interceptor always sets the modified field on save, and only sets the created field when it's not already set.
I'd like to add support for the Java 8 Date/Time API (JSR-310) in my JPA-enabled application.
It's clear that JPA 2.1 does not support the Java 8 Date/Time API.
As a workaround, the most common advise is to use an AttributeConverter.
In my existing application, I changed my entities to use LocalDate/LocalDateTime types for the column mapping fields and added legacy setter/getters for java.util.Date to them.
I created corresponding AttributeConverter classes.
My application does now fail when using Query.setParameter() with java.util.Date instances (it worked before the transition to the new API). It seems that JPA expects the new date types and does not convert it on the fly.
I expected that if passing an argument to setParameter() of a type for which an AttributeConverter has been registered, it would be automatically converted by the converter.
But this seems to be not the case, at least not using EclipseLink 2.6.2:
java.lang.IllegalArgumentException: You have attempted to set a value of type class java.util.Date for parameter closeDate with expected type of class java.time.LocalDate from query string SELECT obj FROM [...]
at org.eclipse.persistence.internal.jpa.QueryImpl.setParameterInternal(QueryImpl.java:937) ~[eclipselink-2.6.2.jar:2.6.2.v20151217-774c696]
at org.eclipse.persistence.internal.jpa.EJBQueryImpl.setParameter(EJBQueryImpl.java:593) ~[eclipselink-2.6.2.jar:2.6.2.v20151217-774c696]
at org.eclipse.persistence.internal.jpa.EJBQueryImpl.setParameter(EJBQueryImpl.java:1) ~[eclipselink-2.6.2.jar:2.6.2.v20151217-774c696]
[...]
Questions:
Is this behavior expected? Did I miss something?
Is there a way to use the new date types as fields without breaking existing code?
How did you deal with the transition to the new Date/Time API within JPA?
UPDATE:
However, It seems that at least using EclipseLink, custom types for which an AttributeConverter exists, are not fully supported:
Within JPQL queries, neither the actual field type nor the converted database type can be used as a parameter.
When using the converted database type, the exception described above occurs.
When using the actual field type (e.g. LocalDate), it's directly passed to the jdbc driver which doesn't know this type:
Caused by: java.sql.SQLException: Invalid column type
at oracle.jdbc.driver.OraclePreparedStatement.setObjectCritical(OraclePreparedStatement.java:10495)
at oracle.jdbc.driver.OraclePreparedStatement.setObjectInternal(OraclePreparedStatement.java:9974)
at oracle.jdbc.driver.OraclePreparedStatement.setObjectInternal(OraclePreparedStatement.java:10799)
at oracle.jdbc.driver.OraclePreparedStatement.setObject(OraclePreparedStatement.java:10776)
at oracle.jdbc.driver.OraclePreparedStatementWrapper.setObject(OraclePreparedStatementWrapper.java:241)
at org.eclipse.persistence.internal.databaseaccess.DatabasePlatform.setParameterValueInDatabaseCall(DatabasePlatform.java:2506)
I would expect that EclipseLink converts the field type to the java.sql type using the AttributeConverter.
(see also this bug report: https://bugs.eclipse.org/bugs/show_bug.cgi?id=494999 )
Which leads us to the most important question #4:
Is there a workaround/solution to support java 8 date fields using EclipseLink, including the possibility to use a query parameters on such a field?
ADDITIONAL INFO
AttributeConverter used (for LocalDateTime conversion)
Additional information to reproduce exception
Some time ago, I converted a Java EE 7 web app from Java 7 to Java 8, and replaced java.util.Date in entities with LocalDate and LocalDateTime.
Yes, that behavior is expected, because an AttributeConverter only converts between the type used for entity fields and the database type (ie, java.sql.Date, etc.); it does not convert between an entity field type and a java.util.Date used in a query parameter.
As far as I know, no, there is no way to continue using java.util.Date in existing code, after introducing the java.time types into JPA entities.
Besides creating the necessary AttributeConverter implementations, I changed all occurrences of java.util.Date to the appropriate java.time types, not only in entities but also in JPA-QL queries and in business methods.
For item 2, of course you can go some way by using utility methods and getters/setters that convert between java.util and java.time, but it's not going to go all the way. More importantly, I don't quite see the point of introducing java.time types into JPA entity attributes, if you are not willing to convert the remaining code that uses these
attributes. After the conversion work I did in that Java EE app, there were no uses of java.util.Date left anywhere (though I also had to create converters for JSF).
With so many bugs in the provider itself I don't think you have much choice but to use java.util.Date on the mapping level and java 8 dates on the API level.
Assuming you write a utility class for conversion to/from java.util dates called DateUtils, you could define your mappings as follows:
#Entity
public class MyEntity {
#Column("DATE")
private Date date; // java.util.Date
public void setDate(LocalDateTime date) {
this.date = DateUtils.convertToDate(date);
}
public LocalDateTime getDate() {
return DateUtils.convertFromDate(date);
}
}
Then to filter by date in JPQL:
public List<MyEntity> readByDateGreaterThan(LocalDateTime date) {
Query query = em.createQuery("select e from MyEntity e where e.date > :date");
query.setParameter("date", DateTuils.convertToDate(date));
return query.getResultList();
}
So, java.util dates would be used in entities and DAOs (Repositories) internally, while the API exposed by entities and DAOs would take/return java 8 dates, thus enabling the rest of the application to operate with java 8 dates only.
I have the following setup:
EclipseLink v2.6.2
h2 Database v1.4.191
Java 8
The entity class is like:
#Entity
public class MeasuringPoint extends BaseEntity {
#Column(nullable = false)
private LocalDateTime when;
public void setWhen(LocalDateTime when) {
this.when = when;
}
public LocalDateTime getWhen() {
return when;
}
}
The required converter for JPA 2.1 is:
#Converter(autoApply = true)
public class LocalDateTimeConverter implements AttributeConverter<LocalDateTime, Timestamp> {
#Override
public Timestamp convertToDatabaseColumn(LocalDateTime attribute) {
return attribute == null ? null : Timestamp.valueOf(attribute);
}
#Override
public LocalDateTime convertToEntityAttribute(Timestamp dbData) {
return dbData == null ? null : dbData.toLocalDateTime();
}
}
Now I can make a query
List<?> result = em.createQuery(
"SELECT p FROM MeasuringPoint p WHERE p.when = :custDate")
.setParameter("custDate", LocalDateTime.now())
.getResultList();
and it works like a charm. The result contains the expected entities. The conversion to TIMESTAMP is done automatically. When you have queries using the Criteria API, have a look at this answer which shows how to use LocalDateTime within Criteria API queries.
I wonder why it doesn't work with your code. Maybe the H2 JDBC driver does support something your Oracle doesn't.
AttributeConverter is working as designed, as the converter is meant to handle the back and forth between your entity type and the database type. Validation is verifying the type of the parameter doesn't match the type within the entity - just because your attribute converter can handle it, doesn't mean it fits the contract of the attribute converter types. JPA only says it will go through the converter before going to the database, and in this case it doesn't go to the database.
If you don't like Rogério's suggestions, you can
1) modify the EclipseLink code to relax the validation to allow it to go through to your converter, or
2) change your attribute type to 'Object' instead so that all parameter types you may pass in will go to your converter.
I'm experiencing the following very annoying behaviour when using JPA entitys in conjunction with Oracle 10g.
Suppose you have the following entity.
#Entity
#Table(name = "T_Order")
public class TOrder implements Serializable {
private static final long serialVersionUID = 2235742302377173533L;
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private Integer id;
#Column(name = "activationDate")
private Calendar activationDate;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public Calendar getActivationDate() {
return activationDate;
}
public void setActivationDate(Calendar activationDate) {
this.activationDate = activationDate;
}
}
This entity is mapped to Oracle 10g, so in the DB there will be a table T_ORDER with a primary key NUMBER column ID and a TIMESTAMP column activationDate.
Lets suppose I create an instance of this class with the activation date 15. Sep 2008 00:00AM. My local timezone is CEST which is GMT+02:00. When I persist this object and select the data from the table T_ORDER using sqlplus, I find out that in the table actually 14. Sep 2008 22:00 is stored, which is ok so far, because the oracle db timezone is GMT.
But now the annoying part. When I read this entity back into my JAVA program, I find out that the oracle time zone is ignored and I get 14. Sep 2008 22:00 CEST, which is definitly wrong.
So basically, when writing to the DB the timezone information will be used, when reading it will be ignored.
Is there any solution for this out there? The most simple solution I guess would be to set the oracle dbs timezone to GMT+02, but unfortunatly I can't do this because there are other applications using the same server.
We use the following technology
MyEclipse 6.5
JPA with Hibernate 3.2
Oracle 10g thin JDBC Driver
You should not use a Calendar for accessing dates from the database, for this exact reason. You should use java.util.Date as so:
#Temporal(TemporalType.TIMESTAMP)
#Column(name="activationDate")
public Date getActivationDate() {
return this.activationDate;
}
java.util.Date points to a moment in time, irrespective of any timezones. Calendar can be used to format a date for a particular timezone or locale.
I already had my share of problems with JPA and timestamps. I've been reading in the oracle forums and please check the following:
The field in the database should be TIMESTAMP_TZ and not just TIMESTAMP
Try adding the annotation #Temporal(value = TemporalType.TIMESTAMP)
If you don't really need the timezone, put in a date or timestamp field.