LocalDate binding doesn't work when MySQL server has a different timezone to the client
See original GitHub issueI have a MySql server that is configured with a timezone of UTC. My Java app has a timezone of “Australia/Perth”. When I save down LocalDate
fields, they are stored in the DB as the previous day. Eg if I bind 2018-01-10
when I query the DB I see 2018-01-09
, and when I retrieve it using JDBI I get 2018-01-09
.
Here is a simplified example:
public interface ReportDao {
@SqlUpdate("insert into report (report_date, title) values (:date, :title")
void insert(@Bind("date") LocalDate date, @Bind("title") String title);
@SqlQuery("select report_date from report")
List<LocalDate> getAllDates();
}
And this is used as follows:
// ...
final ReportDao verdictDao = jdbi.onDemand(ReportDao.class);
LocalDate d = LocalDate.of(2018, Month.JANUARY, 10);
reportDao.insert(d, "bar");
reportDao.getAllDates().forEach(System.out::println);
2018-01-09
is printed, when I expect it to print out 2018-01-10
.
Client set-up:
- Timezone Australia/Perth
- jdbi v3.6.0
- mysql-connector-java v8.0.11 (using driver: com.mysql.cj.jdbc.Driver)
Server set-up:
- Timezone UTC
- MySQL 5.7.25-0ubuntu0.18.04.2
- Column type either DATE, or DATETIME
The problem occurs because the MySQL driver will convert the java.sql.Date
from the client timezone to the server timezone. This seems like the correct thing to do, generally. Here is where JDBI interacts with the MySQL driver, in JavaTimeArgumentFactory
line 29:
register(LocalDate.class, Types.DATE, (p, i, v) -> p.setDate(i, java.sql.Date.valueOf(v)));
And in the MySQL driver, in ClientPreparedQueryBindings
(called from ClientPreparedStatement
), we see this:
// JDBI calls the 2-arg setDate()
@Override
public void setDate(int parameterIndex, Date x) {
setDate(parameterIndex, x, this.session.getServerSession().getDefaultTimeZone());
}
@Override
public void setDate(int parameterIndex, Date x, TimeZone tz) {
if (x == null) {
setNull(parameterIndex);
} else {
if (this.ddf == null) {
this.ddf = new SimpleDateFormat("''yyyy-MM-dd''", Locale.US);
}
this.ddf.setTimeZone(tz);
setValue(parameterIndex, this.ddf.format(x)); // TODO set MysqlType?
}
}
I’ve tested the above scenario with PostgreSQL and it doesn’t suffer from the same issue.
I can think of 2 possible fixes for this in the JDBI source:
1. Pin the time-zone by supplying a calendar to the setDate() call, in JavaTimeArgumentFactory
// Calendar.getInstance() pins the client-side timezone thereby avoiding conversion
register(LocalDate.class, Types.DATE, (p, i, v) -> p.setDate(i, java.sql.Date.valueOf(v), Calendar.getInstance()));
2. Supply an ISO date string instead of the date, in JavaTimeArgumentFactory
// Send the String to the DB, and allow the DB to coerce it into a date
register(LocalDate.class, Types.DATE, (p, i, v) -> p.setDate(i, v.toString()));
I’ve tested 1. on both MySQL and Postgresql and it works. It is a bit horrible to create a calendar though.
I haven’t tested 2. but it should work. Especially, given that both MySQL and Postgresql drivers format the date as a string before sending to the servers. 2. is flaky though, as I’m pretty sure Oracle allows a date format to be set per session. So there’s a good chance that this will not work in systems not using ISO formats.
Please let me know if you’d like more info!
Issue Analytics
- State:
- Created 5 years ago
- Comments:20 (5 by maintainers)
I can confirm this solved the issue for me.
Praise be to the Flying Spaghetti Monster! Would a MySQL / Jdbi user suffering LocalDate binding issues please confirm that time binding works well with the new driver? Thank you!