[ https://issues.apache.org/jira/browse/CAY-2701?page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel&focusedCommentId=17304064#comment-17304064 ]
Andrus Adamchik edited comment on CAY-2701 at 3/18/21, 11:35 AM: ----------------------------------------------------------------- Oh, and here is an ExtendedType that I am using. It has some MySQL-specific code (timestamp String format) {noformat} public class LocalDateTimeType implements ExtendedType<LocalDateTime> { static final DateTimeFormatter MYSQL_LOCAL_DATE_TIME = new DateTimeFormatterBuilder() .parseCaseInsensitive() .append(DateTimeFormatter.ISO_LOCAL_DATE) .appendLiteral(' ') .append(DateTimeFormatter.ISO_LOCAL_TIME) .toFormatter(); @Override public String getClassName() { return LocalDateTime.class.getName(); } @Override public void setJdbcObject(PreparedStatement statement, LocalDateTime value, int pos, int type, int scale) throws Exception { if (value == null) { statement.setNull(pos, type); } else { // using ISO-8601 formatted String.. This works with MySQL... Converting to Timestamp and using "setTimestamp" // doesn't work, as Timestamp.valueOf(LocalDateTime) will not properly handle the DST "skipped hour" if the // current Java TZ has a DST statement.setString(pos, value.toString()); } } @Override public LocalDateTime materializeObject(ResultSet rs, int index, int type) throws Exception { // On MySQL this requires "noDatetimeStringSync=true", otherwise the driver will first create a Timestamp, // that will mess up the DST settings, and then convert that Timestamp to a (possibly incorrect) String String ts = rs.getString(index); return ts != null ? LocalDateTime.parse(ts, MYSQL_LOCAL_DATE_TIME) : null; } @Override public LocalDateTime materializeObject(CallableStatement rs, int index, int type) throws Exception { String ts = rs.getString(index); return ts != null ? LocalDateTime.parse(ts, MYSQL_LOCAL_DATE_TIME) : null; } @Override public String toString(LocalDateTime value) { return value != null ? '\'' + value.toString() + '\'' : "NULL"; } } {noformat} was (Author: andrus): Oh, and here is an ExtendedType that I am using. It has some MySQL-specific code (timestamp String format), and doesn't handle fractional seconds. {noformat} public class LocalDateTimeType implements ExtendedType<LocalDateTime> { static final DateTimeFormatter MYSQL_LOCAL_DATE_TIME = new DateTimeFormatterBuilder() .parseCaseInsensitive() .append(DateTimeFormatter.ISO_LOCAL_DATE) .appendLiteral(' ') .append(DateTimeFormatter.ISO_LOCAL_TIME) .toFormatter(); @Override public String getClassName() { return LocalDateTime.class.getName(); } @Override public void setJdbcObject(PreparedStatement statement, LocalDateTime value, int pos, int type, int scale) throws Exception { if (value == null) { statement.setNull(pos, type); } else { // using ISO-8601 formatted String.. This works with MySQL... Converting to Timestamp and using "setTimestamp" // doesn't work, as Timestamp.valueOf(LocalDateTime) will not properly handle the DST "skipped hour" if the // current Java TZ has a DST statement.setString(pos, value.toString()); } } @Override public LocalDateTime materializeObject(ResultSet rs, int index, int type) throws Exception { // On MySQL this requires "noDatetimeStringSync=true", otherwise the driver will first create a Timestamp, // that will mess up the DST settings, and then convert that Timestamp to a (possibly incorrect) String String ts = rs.getString(index); return ts != null ? LocalDateTime.parse(ts, MYSQL_LOCAL_DATE_TIME) : null; } @Override public LocalDateTime materializeObject(CallableStatement rs, int index, int type) throws Exception { String ts = rs.getString(index); return ts != null ? LocalDateTime.parse(ts, MYSQL_LOCAL_DATE_TIME) : null; } @Override public String toString(LocalDateTime value) { return value != null ? '\'' + value.toString() + '\'' : "NULL"; } } {noformat} > DST-related conflicts with LocalDateTime > ---------------------------------------- > > Key: CAY-2701 > URL: https://issues.apache.org/jira/browse/CAY-2701 > Project: Cayenne > Issue Type: Bug > Affects Versions: 4.0.2, 4.1, 4.2.M2 > Environment: MySQL 5.7.x, table column with type "datetime" > JDBC Driver: mysql:mysql-connector-java:5.1.46 > JVM timezone: "America/New_York" > Reporter: Andrus Adamchik > Priority: Major > > Just ran into a whole collection of annoying problems related to > daylight-savings time. As mentioned in the "Environment" section, the test > environment is MySQL 5.7.x, mysql:mysql-connector-java:5.1.46, > "America/New_York" timezone. Some of the issues described here are universal, > others are DB and driver and JVM TZ sensitive. > h2. Problem 1: Lossy conversion from LocalDateTime to Timestamp > Say a LocalDateTime value corresponds to a UTC timezone (so no DST there), > and want to save a value of "2021-03-14T02:00:00". The JVM is located in > "America/New_York" timezone, where this specific hour ("2021-03-14T02:XX:XX") > is skipped due to EST to EDT switchover. This combination prevents Cayenne > from saving such as local date correctly because LocalDateTime to Timestamp > conversion (used by Cayenne to bind datetime value to JDBC) would actually > use the JVM TZ (!!) and the hour will be increased by 1 (so > "2021-03-14T03:35:00" will be saved). Here is a JDBC-agnostic test to > demonstreate that: > {noformat} > @Test > public void test() { > TimeZone defaultTz = TimeZone.getDefault(); > TimeZone.setDefault(TimeZone.getTimeZone("America/New_York")); > try { > LocalDateTime dt = LocalDateTime.parse("2021-03-14T02:35:00"); > assertEquals(dt, Timestamp.valueOf(dt).toLocalDateTime()); > } finally { > TimeZone.setDefault(defaultTz); > } > } > {noformat} > There seems to be on way around it (this is an expected java.sql.Timestamp > behavior!!), except to replace LocalDateTime-to-Timestamp conversion with > LocalDateTime-to-String conversion. This causes some downstream driver > issues. MySQL 5.1.x driver throws on "PreparedStatement.setObject(i, string, > Types.TIMESTAMP)", and "setString(..)" should be called instead. Not sure > about other DBs and 8.x MySQL driver. > With the above in mind, LocalDateTime ValueObjectType should be reimplemented > as an ExtendedType, and we need to test it across the DBs. > h2. Problem 2: MySQL 5.1.x driver will add an hour on read > The ExtendedType above allows to write LocalDateTime properly, DST or not. > But when reading it back, MySQL Driver interferes. When reading a column that > is a "datetime" as String, it first does a conversion to Timestamp, and then > converts it to String. So again - an hour is added unexpectedly. > There's no Cayenne-side fix for that. But the DB connection string must > contain "noDatetimeStringSync=true". And this seems to be fixed in the 8.x > driver. > h2. Workarounds / Best Practices > While we need to address this craiziness with a new ExtendedType, I suspect > if a user sets his Java server TZ to UTC, they should avoid all this > insanity. Though of course YMMV when e.g. running unit tests in a specific TZ > you may run into this problem. -- This message was sent by Atlassian Jira (v8.3.4#803005)