marschall / threeten-jpa

JPA attribute converters for JSR-310 dates and times
53 stars 6 forks source link

java.lang.ClassCastException: org.eclipse.persistence.internal.platform.database.oracle.TIMESTAMPTZWrapper cannot be cast to oracle.sql.TIMESTAMPTZ #23

Open maksymgendin opened 7 years ago

maksymgendin commented 7 years ago

I'm am using org.eclipse.persistence:eclipselink:2.6.4 with com.oracle.jdbc:ojdbc7:12.1.0.2 and this is my TIMESTAMP WITH TIMEZONE entity field:

@Temporal(TemporalType.TIMESTAMP)
@Column(name = "CREATION_DATE")
private ZonedDateTime creationDate;

On querying the database I get:

Caused by: java.lang.ClassCastException: org.eclipse.persistence.internal.platform.database.oracle.TIMESTAMPTZWrapper cannot be cast to oracle.sql.TIMESTAMPTZ at com.github.marschall.threeten.jpa.oracle.OracleZonedDateTimeConverter.convertToEntityAttribute(OracleZonedDateTimeConverter.java:15) ~[threeten-jpa-oracle-eclipselink-1.5.1.jar:?] at org.eclipse.persistence.mappings.converters.ConverterClass.convertDataValueToObjectValue(ConverterClass.java:124) ~[org.eclipse.persistence.core-2.6.4.jar:?]

Do you have any ideas how can I fix this without extending the EclipseLink Oracle-specific Platform class and overwriting the methods which wrap TIMESTAMPTZ in TIMESTAMPTZWrapper?

marschall commented 7 years ago

Can you try to remove or comment out the @Temporal annotation?

maksymgendin commented 7 years ago

I unfortunately still get the same error...

marschall commented 7 years ago

What operation do you perform when this happens? Are you using criteria API?

maksymgendin commented 7 years ago

The problem is getObjectFromResultSet:205 in org.eclipse.persistence.platform.database.oracle.Oracle9Platform. It uses getTIMESTAMPTZFromResultSet to wrap the TIMESTAMPTZ object into TIMESTAMPTZWrapper. The comment says that the bug has been fixed in the next version for both streams, but with the current version of the Oracle JDBC Driver 12.1.0.2.0 this code piece is still executed...

maksymgendin commented 7 years ago

I am executing a javax.persistence.TypedQuery constructed via entityManager.createQuery(""); and then it fails on query.getResultList();

marschall commented 7 years ago

That's really interesting, the tests have no dependency on org.eclipse.persistence.oracle and therefore org.eclipse.persistence.platform.database.OraclePlatform is used instead. I haven't tested criteria API though, only EntityManager#find.

TIMESTAMPTZ is certainly serializable in both 11g and 12c. There seems to be a related bug 369617.

maksymgendin commented 7 years ago

If I set eclipselink.target-database property to org.eclipse.persistence.platform.database.OraclePlatform, then indeed my code works...so how we should continue? I could of course override the wrapping with a custom overridden Platform class and set this class as eclipselink.target-database, but maybe there are other solutions?

marschall commented 7 years ago

At this point we have established that it's a bug in EclipseLink so we should get in contact with them and report a bug. They should also be able to recommend the best work around. Coming up with a patch should be doable but a reproducer and test may be more work, I don't know what infrastructure they have in place.

In your typed query do you pass the ZonedDateTime as a parameter?

maksymgendin commented 7 years ago

I will let in on your table 😄

There are two ZonedDateTime field references inside an ORDER BY.

maksymgendin commented 7 years ago

Maybe useful for somebody, here is my extended Platform class, which you can set as eclipselink.target-database:

import com.github.marschall.threeten.jpa.oracle.impl.TimestamptzConverter;
import oracle.sql.TIMESTAMPTZ;
import org.eclipse.persistence.internal.helper.ClassConstants;
import org.eclipse.persistence.internal.sessions.AbstractSession;
import org.eclipse.persistence.platform.database.oracle.Oracle12Platform;

import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.time.ZonedDateTime;
import java.util.GregorianCalendar;

public class Oracle12WrapperlessPlatform extends Oracle12Platform {

    @Override
    public Object getTIMESTAMPTZFromResultSet(final ResultSet resultSet,
                                              final int columnNumber,
                                              final int type,
                                              final AbstractSession session) throws SQLException {
        return resultSet.getObject(columnNumber);
    }

    @Override
    public Object convertObject(final Object sourceObject, final Class javaClass) {
        Object valueToConvert = sourceObject;
        if (sourceObject instanceof TIMESTAMPTZ) {
            final ZonedDateTime zonedDateTime = TimestamptzConverter.timestamptzToZonedDateTime((TIMESTAMPTZ) valueToConvert);
            if (javaClass == ClassConstants.CALENDAR || javaClass == ClassConstants.GREGORIAN_CALENDAR) {
                valueToConvert = GregorianCalendar.from(zonedDateTime);
            } else {
                valueToConvert = new Timestamp(zonedDateTime.toInstant().getEpochSecond() * 1000L);
            }
        }
        return super.convertObject(valueToConvert, javaClass);
    }
}
marschall commented 7 years ago

I did some additional testing and the issue is limited to criteria API but reading in general. When I use

<property name="eclipselink.target-database" value="Oracle"/>

I can no longer reproduce the issue. I don't know what other features you use so I don't know if this is an option for you.

marschall commented 7 years ago

I filed bug 511999, let's see where it goes from here.

maksymgendin commented 7 years ago

Thanks for your effort. I will follow the opened bug.

marschall commented 7 years ago

@maksymgendin I did some additional testing and simply calling #getObject was enough for my tests, in which cases was additionally overriding #convertObject also necessary?

My platforms look like this:

maksymgendin commented 7 years ago

@marschall I think this happens if you are mixing usage of ZonedDateTime AND Calendar in your entities. The exception then appears if you are reading the entity with Calendar field.

marschall commented 7 years ago

@maksymgendin is there a specific reason why you're doing that or did you simply for testing or migration purposes simply start with just one column?

maksymgendin commented 7 years ago

@marschall We unfortunately have some older shared entity classes with Calendar fields. At some point we will migrate them to ZonedDateTime.

marschall commented 7 years ago

@maksymgendin do you use Calendar for TIMESTAMP or for TIMESTAMP WITH TIME ZONE columns?

maksymgendin commented 7 years ago

TIMESTAMP WITH TIME ZONE

marschall commented 7 years ago

I now understand why you had to implement convertObject. The more I dig into this issue the more I question whether TIMESTAMPTZWrapper really exists because of TIMESTAMPTZ not being serializable or rather because in convertObject there is no access to the connection. Unfortunately the bug references don't seem to point to bugs in the Eclipse bug tracker.

marschall commented 7 years ago

@maksymgendin the following path in your fix

new Timestamp(zonedDateTime.toInstant().getEpochSecond() * 1000L);

is limited to second precision, TIMESTAMP WITH TIME ZONE on Oracle defaults to microsecond precision but can go up to nanosecond precision. java.sql.Timestamp actually supports nanosecond precision. I went for this instead:

Timestamp.valueOf(zonedDateTime.withZoneSameInstant(ZoneId.systemDefault()).toLocalDateTime());

which should give you nanosecond precision. The case is a bit theoretical though, mapping TIMESTAMP WITH TIME ZONE to java.sql.Timestamp.

maksymgendin commented 7 years ago

@marschall You're so right! Thank you very much for that point. You may have saved me from future headaches with finding bugs :)

LumnitzF commented 6 years ago

When I use the fixed approach, the Converter cannot be auto applied anymore, as the following Exception is thrown:

Exception [EclipseLink-3002] (Eclipse Persistence Services - 2.6.0.v20150309-bf26070): org.eclipse.persistence.exceptions.ConversionException Exception Description: The object [22.03.18 13:48], of class [class java.sql.Timestamp], from mapping [org.eclipse.persistence.mappings.DirectToFieldMapping[TIMESTAMP_WITH_TIME_ZONE_zonedDateTime-->TEST_TIMES.FIELD_TSTZ]] with descriptor [RelationalDescriptor(jpa.entities.TimezoneTestEntity --> [DatabaseTable(TEST_TIMES)])], could not be converted to [class [B]. at org.eclipse.persistence.exceptions.ConversionException.couldNotBeConverted(ConversionException.java:78) at org.eclipse.persistence.internal.helper.ConversionManager.convertObjectToByteArray(ConversionManager.java:351) at org.eclipse.persistence.internal.helper.ConversionManager.convertObject(ConversionManager.java:138) at org.eclipse.persistence.internal.databaseaccess.DatasourcePlatform.convertObject(DatasourcePlatform.java:179) at org.eclipse.persistence.platform.database.oracle.Oracle9Platform.convertObject(Oracle9Platform.java:432) at com.github.marschall.threeten.jpa.oracle.PatchedOracle12Platform.convertObject(PatchedOracle12Platform.java:42)

When I annotate the field with @Convert(converter = ZonedDateTimeConverter.class) everything works fine again

marschall commented 6 years ago

@frl7082 unless you have exactly the same setup and issue as OP would you mind opening a separate issue instead?

LumnitzF commented 6 years ago

nevermind, was an error in my project setup