OPENJPA-2713 properly handle WITH TIME ZONE if supported by db

This includes handling the  new java.sql.Types.TIME_WITH_ZONE
and DATE_WITH_ZONE.
This commit is contained in:
Mark Struberg 2019-01-25 16:42:15 +01:00
parent dbfb360e85
commit 6d2544f390
7 changed files with 236 additions and 43 deletions

View File

@ -51,7 +51,6 @@ import java.time.LocalTime;
import java.time.OffsetDateTime;
import java.time.OffsetTime;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
@ -370,6 +369,8 @@ public class DBDictionary
public String structTypeName = "STRUCT";
public String timeTypeName = "TIME";
public String timestampTypeName = "TIMESTAMP";
public String timeWithZoneTypeName = "TIME WITH TIME ZONE";
public String timestampWithZoneTypeName = "TIMESTAMP WITH TIME ZONE";
public String tinyintTypeName = "TINYINT";
public String varbinaryTypeName = "VARBINARY";
public String varcharTypeName = "VARCHAR";
@ -1222,10 +1223,12 @@ public class DBDictionary
*/
public void setDate(PreparedStatement stmnt, int idx, java.sql.Date val, Calendar cal, Column col)
throws SQLException {
if (cal == null)
if (cal == null) {
stmnt.setDate(idx, val);
else
}
else {
stmnt.setDate(idx, val, cal);
}
}
/**
@ -1292,8 +1295,7 @@ public class DBDictionary
/**
* Set the given value as a parameter to the statement.
*/
public void setDouble(PreparedStatement stmnt, int idx, double val,
Column col)
public void setDouble(PreparedStatement stmnt, int idx, double val, Column col)
throws SQLException {
stmnt.setDouble(idx, val);
}
@ -1301,8 +1303,7 @@ public class DBDictionary
/**
* Set the given value as a parameter to the statement.
*/
public void setFloat(PreparedStatement stmnt, int idx, float val,
Column col)
public void setFloat(PreparedStatement stmnt, int idx, float val, Column col)
throws SQLException {
stmnt.setFloat(idx, val);
}
@ -1326,8 +1327,7 @@ public class DBDictionary
/**
* Set the given value as a parameter to the statement.
*/
public void setLocale(PreparedStatement stmnt, int idx, Locale val,
Column col)
public void setLocale(PreparedStatement stmnt, int idx, Locale val, Column col)
throws SQLException {
setString(stmnt, idx, val.getLanguage() + "_" + val.getCountry()
+ "_" + val.getVariant(), col);
@ -1337,8 +1337,7 @@ public class DBDictionary
* Set null as a parameter to the statement. The column
* type will come from {@link Types}.
*/
public void setNull(PreparedStatement stmnt, int idx, int colType,
Column col)
public void setNull(PreparedStatement stmnt, int idx, int colType, Column col)
throws SQLException {
stmnt.setNull(idx, colType);
}
@ -1346,8 +1345,7 @@ public class DBDictionary
/**
* Set the given value as a parameter to the statement.
*/
public void setNumber(PreparedStatement stmnt, int idx, Number num,
Column col)
public void setNumber(PreparedStatement stmnt, int idx, Number num, Column col)
throws SQLException {
// check for known floating point types to give driver a chance to
// handle special numbers like NaN and infinity; bug #1053
@ -1363,8 +1361,7 @@ public class DBDictionary
* Set the given value as a parameter to the statement. The column
* type will come from {@link Types}.
*/
public void setObject(PreparedStatement stmnt, int idx, Object val,
int colType, Column col)
public void setObject(PreparedStatement stmnt, int idx, Object val, int colType, Column col)
throws SQLException {
if (colType == -1 || colType == Types.OTHER)
stmnt.setObject(idx, val);
@ -1383,8 +1380,7 @@ public class DBDictionary
/**
* Set the given value as a parameter to the statement.
*/
public void setShort(PreparedStatement stmnt, int idx, short val,
Column col)
public void setShort(PreparedStatement stmnt, int idx, short val, Column col)
throws SQLException {
stmnt.setShort(idx, val);
}
@ -1392,8 +1388,7 @@ public class DBDictionary
/**
* Set the given value as a parameter to the statement.
*/
public void setString(PreparedStatement stmnt, int idx, String val,
Column col)
public void setString(PreparedStatement stmnt, int idx, String val, Column col)
throws SQLException {
stmnt.setString(idx, val);
}
@ -1401,8 +1396,7 @@ public class DBDictionary
/**
* Set the given value as a parameter to the statement.
*/
public void setTime(PreparedStatement stmnt, int idx, Time val,
Calendar cal, Column col)
public void setTime(PreparedStatement stmnt, int idx, Time val, Calendar cal, Column col)
throws SQLException {
if (cal == null)
stmnt.setTime(idx, val);
@ -1413,8 +1407,7 @@ public class DBDictionary
/**
* Set the given value as a parameter to the statement.
*/
public void setTimestamp(PreparedStatement stmnt, int idx,
Timestamp val, Calendar cal, Column col)
public void setTimestamp(PreparedStatement stmnt, int idx, Timestamp val, Calendar cal, Column col)
throws SQLException {
val = StateManagerImpl.roundTimestamp(val, datePrecision);
@ -1435,8 +1428,7 @@ public class DBDictionary
* @param type the field mapping type code for the value
* @param store the store manager for the current context
*/
public void setTyped(PreparedStatement stmnt, int idx, Object val,
Column col, int type, JDBCStore store)
public void setTyped(PreparedStatement stmnt, int idx, Object val, Column col, int type, JDBCStore store)
throws SQLException {
if (val == null) {
setNull(stmnt, idx, (col == null) ? Types.OTHER : col.getType(),
@ -1857,9 +1849,9 @@ public class DBDictionary
case JavaTypes.LOCAL_DATETIME:
return getPreferredType(Types.TIMESTAMP);
case JavaTypes.OFFSET_TIME:
return getPreferredType(Types.TIME);
return getPreferredType(Types.TIME_WITH_TIMEZONE);
case JavaTypes.OFFSET_DATETIME:
return getPreferredType(Types.TIMESTAMP);
return getPreferredType(Types.TIMESTAMP_WITH_TIMEZONE);
case JavaSQLTypes.SQL_ARRAY:
return getPreferredType(Types.ARRAY);
case JavaSQLTypes.BINARY_STREAM:
@ -1962,6 +1954,10 @@ public class DBDictionary
return timeTypeName;
case Types.TIMESTAMP:
return timestampTypeName;
case Types.TIME_WITH_TIMEZONE:
return timeWithZoneTypeName;
case Types.TIMESTAMP_WITH_TIMEZONE:
return timestampWithZoneTypeName;
case Types.TINYINT:
return tinyintTypeName;
case Types.VARBINARY:

View File

@ -21,6 +21,7 @@ package org.apache.openjpa.jdbc.sql;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.sql.Types;
import java.util.Arrays;
import javax.sql.DataSource;
@ -69,6 +70,8 @@ public class DerbyDictionary
supportsComments = true;
// Derby does still not support 'WITH TIMEZONE' from the SQL92 standard
fixedSizeTypeNameSet.addAll(Arrays.asList(new String[]{
"BIGINT", "INTEGER", "TEXT"
}));
@ -223,4 +226,19 @@ public class DerbyDictionary
}
return count;
}
/**
* Derby doesn't support SQL-2003 'WITH TIMEZONE' nor the respective JDBC types.
*/
@Override
public int getPreferredType(int type) {
switch (type) {
case Types.TIME_WITH_TIMEZONE:
return Types.TIME;
case Types.TIMESTAMP_WITH_TIMEZONE:
return Types.TIMESTAMP;
default:
return type;
}
}
}

View File

@ -34,6 +34,10 @@ import java.sql.Statement;
import java.sql.Types;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.OffsetDateTime;
import java.util.Arrays;
import java.util.Date;
import java.util.HashSet;
@ -64,11 +68,9 @@ import org.postgresql.largeobject.LargeObjectManager;
/**
* Dictionary for PostgreSQL.
*/
public class PostgresDictionary
extends DBDictionary {
public class PostgresDictionary extends DBDictionary {
private static final Localizer _loc = Localizer.forPackage
(PostgresDictionary.class);
private static final Localizer _loc = Localizer.forPackage(PostgresDictionary.class);
private Method dbcpGetDelegate;
@ -122,6 +124,13 @@ public class PostgresDictionary
*/
public String isOwnedSequenceSQL = "SELECT pg_get_serial_sequence(?, ?)";
/**
* Since PostgreSQL
*/
private boolean supportsTimezone;
public PostgresDictionary() {
platform = "PostgreSQL";
validationSQL = "SELECT NOW()";
@ -411,14 +420,15 @@ public class PostgresDictionary
}
}
} catch (Throwable t) {
if (log.isWarnEnabled())
if (log.isWarnEnabled()) {
log.warn(_loc.get("psql-owned-seq-warning"), t);
}
return isOwnedSequence(strName);
}
} else {
if(log.isTraceEnabled()) {
log.trace(String.format("Unable to query ownership for sequence %s using the connection. " +
"Falling back to simpler detection based on the name",
"Falling back to simpler detection based on the name",
name.getName()));
}
@ -723,6 +733,47 @@ public class PostgresDictionary
}
}
@Override
public LocalDate getLocalDate(ResultSet rs, int column) throws SQLException {
return rs.getObject(column, LocalDate.class);
}
@Override
public LocalTime getLocalTime(ResultSet rs, int column) throws SQLException {
return rs.getObject(column, LocalTime.class);
}
@Override
public LocalDateTime getLocalDateTime(ResultSet rs, int column) throws SQLException {
return rs.getObject(column, LocalDateTime.class);
}
@Override
public OffsetDateTime getOffsetDateTime(ResultSet rs, int column) throws SQLException {
return rs.getObject(column, OffsetDateTime.class);
}
@Override
public void setLocalDate(PreparedStatement stmnt, int idx, LocalDate val, Column col) throws SQLException {
stmnt.setObject(idx, val);
}
@Override
public void setLocalTime(PreparedStatement stmnt, int idx, LocalTime val, Column col) throws SQLException {
stmnt.setObject(idx, val);
}
@Override
public void setLocalDateTime(PreparedStatement stmnt, int idx, LocalDateTime val, Column col) throws SQLException {
stmnt.setObject(idx, val);
}
@Override
public void setOffsetDateTime(PreparedStatement stmnt, int idx, OffsetDateTime val, Column col) throws SQLException {
stmnt.setObject(idx, val);
}
/**
* Determine XML column support and backslash handling.
*/
@ -967,8 +1018,7 @@ public class PostgresDictionary
/**
* Connection wrapper to work around the postgres empty result set bug.
*/
protected static class PostgresConnection
extends DelegatingConnection {
protected static class PostgresConnection extends DelegatingConnection {
private final PostgresDictionary _dict;
@ -996,8 +1046,7 @@ public class PostgresDictionary
/**
* Statement wrapper to work around the postgres empty result set bug.
*/
protected static class PostgresPreparedStatement
extends DelegatingPreparedStatement {
protected static class PostgresPreparedStatement extends DelegatingPreparedStatement {
private final PostgresDictionary _dict;

View File

@ -2464,7 +2464,7 @@ public class BrokerImpl implements Broker, FindCallbacks, Cloneable, Serializabl
Object failedObject = null;
if (t[0] instanceof OpenJPAException){
failedObject = ((OpenJPAException)t[0]).getFailedObject();
failedObject = ((OpenJPAException)t[0]).getFailedObject();
}
return new StoreException(_loc.get("rolled-back")).

View File

@ -1077,12 +1077,12 @@ public class DelegatingResultSet implements ResultSet, Closeable {
@Override
public <T>T getObject(String columnLabel, Class<T> type) throws SQLException{
throw new UnsupportedOperationException();
return _rs.getObject(columnLabel, type);
}
@Override
public <T>T getObject(int columnIndex, Class<T> type) throws SQLException{
throw new UnsupportedOperationException();
return _rs.getObject(columnIndex, type);
}
}

View File

@ -21,6 +21,8 @@ package org.apache.openjpa.persistence.simple;
import org.apache.openjpa.persistence.test.SingleEMFTestCase;
import javax.persistence.EntityManager;
import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
@ -35,12 +37,13 @@ public class TestJava8TimeTypes extends SingleEMFTestCase {
private static String VAL_LOCAL_TIME = "04:57:15";
private static String VAL_LOCAL_DATETIME = "2019-01-01T01:00:00";
@Override
public void setUp() {
setUp(CLEAR_TABLES, Java8TimeTypes.class);
}
public void testJava8Types() throws Exception {
public void testJava8Types() {
EntityManager em = emf.createEntityManager();
em.getTransaction().begin();
Java8TimeTypes e = new Java8TimeTypes();
@ -63,8 +66,17 @@ public class TestJava8TimeTypes extends SingleEMFTestCase {
assertEquals(LocalTime.parse(VAL_LOCAL_TIME), eRead.getLocalTimeField());
assertEquals(LocalDate.parse(VAL_LOCAL_DATE), eRead.getLocalDateField());
assertEquals(LocalDateTime.parse(VAL_LOCAL_DATETIME), eRead.getLocalDateTimeField());
assertEquals(e.getOffsetTimeField(), eRead.getOffsetTimeField());
assertEquals(e.getOffsetDateTimeField(), eRead.getOffsetDateTimeField());
// Many databases do not support WITH TIMEZONE syntax.
// Thus we can only portably ensure tha the same instant is used at least.
assertEquals(Instant.from(e.getOffsetDateTimeField()),
Instant.from(eRead.getOffsetDateTimeField()));
assertEquals(e.getOffsetTimeField().withOffsetSameInstant(eRead.getOffsetTimeField().getOffset()),
eRead.getOffsetTimeField());
}
}

View File

@ -0,0 +1,118 @@
= Supported Java8 Time types
This is a temporary document to describe the state of the java.time.* integration in Apache OpenJPA.
== JPA-2.2 required types
The following java.time types have to be supported mandatorily in JPA-2.2:
* java.time.LocalDate
* java.time.LocalTime
* java.time.LocalDateTime
* java.time.OffsetTime
* java.time.OffsetDateTime
Apache OpenJPA additionally supports the following types:
* TODO java.time.Instant etc
Not every database supports all those types natively though.
Some cannot store them at all, others have a mode which e.g. doesn't store the Offset part.
For our example we assume we live in Europe (+1 timezone).
If you store 04:12-9 then you might actually end up with 14:12+1 when retrieving the value from the database.
That means that OpenJPA tries to at least keep the instant correct if the database doesn't support zones natively.
== Database Support matrix:
[cols=6*,options=header]
|===
| DBName
| LocalDate
| LocalTime
| LocalDateTime
| OffsetTime
| OffsetDateTime
| Derby
| DATE
| TIME
| TIMESTAMP
| not natively supported
| not natively supported
| PostgreSQL
| DATE
| TIME
| TIMESTAMP
| TIME WITH TIME ZONE
| TIMESTAMP WITH TIME ZONE
| MySQL
| DATE
| TIME
| DATETIME
| not natively supported, fallback to TIME
| not natively supported, fallback to DATETIME
| MariaDB
| DATE
| TIME
| DATETIME
| not natively supported, fallback to TIME
| not natively supported, fallback to DATETIME
| Microsoft SQLServer
| DATE
| TIME
| DATETIME2
|
|
| Oracle
| DATE
| TIME
| DATE
| TIME WITH TIME ZONE
| TIMESTAMP WITH TIME ZONE
| H2
| DATE
| TIME
| DATE
|
|
...
|===
=== Notes
==== PostgreSQL
PostgreSQL supports some of the types natively in the JDBC driver:
* LocalDate
* LocalTime
* LocalDateTime
* OffsetDateTime
Note that `OffsetTime` is not supported in `setObject/getObject`.
Also note that PostgreSQL always stores DateTime values internally as UTC.
Thus when retrieving the date back from the Database again you will get the same Instant representation, but in a the local TimeZone!
==== MySQL & MariaDB
MySQL supports LocalDate, LocalTime and LocalDateTime in `setObject/getObject`.
It also supports OffsetTime and OffsetDateTime in `setObject/getObject`, but only via conversion.
So the MySQL JDBC driver will effectively convert them to the local timezone and keep the 'Instant'.
MariaDB does basically the same.
==== Oracle