MySQL Connector / J не преобразовывает SQL DATE в часовой пояс JVM - PullRequest
0 голосов
/ 28 августа 2018

Предположим, что выполнены следующие условия:

  1. На обоих хостах (на одном из которых JVM , а также на MySQL ) аппаратные часы работают в формате UTC, и эти часы синхронизируются (например, с использованием NTP).
  2. Часовые пояса MySQL и JVM отличаются (в моем примере MySQL работает в Europe/Moscow ( +03:00) часовой пояс и JVM использует GMT+14:00).

В этом случае будут периоды в течение каждого дня, когда текущее представление даты (в формате yyyy-MM-dd) будет отличаться от перспектив Java и базы данных (дата базы данных будет отставать).

Я использую MySQL Connector / J 8.0, который является часовым поясом по умолчанию (в отличие от 5.1.46), поэтому достаточно просто установить для свойства соединения serverTimezone значение Europe/Moscow, если драйвер не может проанализировать @@time_zone и / или @@system_time_zone.

Теперь рассмотрим следующий сценарий:

  1. Клиент сохраняет текущую метку времени (как экземпляр java.sql.Timestamp) в базе данных.
  2. Затем тот же клиент считывает только дробь даты вышеуказанной отметки времени (как java.sql.Date) обратно в JVM.

Ожидается, что считанная часть даты будет преобразована обратно во временную зону JVM (это то, что я наблюдаю для Oracle , PostgreSQL и MS SQL Server ), т.е. е. следующий тест должен пройти успешно:

import static java.lang.String.format;
import static java.lang.System.currentTimeMillis;
import static org.assertj.core.api.Assertions.assertThat;

import java.sql.Connection;
import java.sql.Date;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.sql.Timestamp;
import java.text.SimpleDateFormat;
import java.util.Properties;
import java.util.TimeZone;

import org.testng.annotations.BeforeClass;
import org.testng.annotations.Test;

public final class TimeZoneTestPartial {
    private static final TimeZone DEFAULT_TIME_ZONE = TimeZone.getTimeZone("GMT+14:00");

    private static final String URL = "jdbc:mysql://localhost:3306/sandbox";

    private static final Properties CONNECTION_INFO = new Properties();

    static {
        CONNECTION_INFO.setProperty("user", "...");
        CONNECTION_INFO.setProperty("password", "...");
        CONNECTION_INFO.setProperty("useSSL", "false");
        CONNECTION_INFO.setProperty("serverTimezone", "Europe/Moscow");
    }

    @BeforeClass
    public static void setUpOnce() {
        TimeZone.setDefault(DEFAULT_TIME_ZONE);
    }

    @Test
    @SuppressWarnings("static-method")
    public void testDate() throws SQLException {
        try (final Connection conn = DriverManager.getConnection(URL, CONNECTION_INFO)) {
            try (final Statement stmt = conn.createStatement()) {
                final String tableName = "date_with_time_zone_test";
                try {
                    stmt.executeUpdate(format("drop table %s",
                            tableName));
                } catch (@SuppressWarnings("unused") final SQLException ignored) {
                    // ignore
                }

                stmt.executeUpdate(format("create table %s (value %s not null)",
                        tableName,
                        getTimestampType()));

                final long clientTimeMillis = currentTimeMillis();

                try (final PreparedStatement pstmt = conn.prepareStatement(format("insert into %s (value) values (?)",
                        tableName))) {
                    pstmt.setTimestamp(1, new Timestamp(clientTimeMillis));
                    pstmt.executeUpdate();
                }

                final String selectSql = format("select * from %s", tableName);
                try (final ResultSet rset = stmt.executeQuery(selectSql)) {
                    assertThat(rset.next()).isTrue();
                    final Date date = rset.getDate(1);
                    assertThat(date).isNotNull();
                    assertThat(rset.next()).isFalse();

                    final SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd");
                    format.setTimeZone(DEFAULT_TIME_ZONE);
                    assertThat(format.format(date))
                            .as("date fraction from the database")
                            .isEqualTo(format.format(new java.util.Date(clientTimeMillis)));
                }

                stmt.executeUpdate(format("drop table %s",
                        tableName));
            }
        }
    }

    private static String getTimestampType() {
        return "datetime"; //"timestamp";
    }
}

По сути, тест не проходит для MySQL - i. то есть, в отличие от других основных баз данных, драйвер MySQL может возвращать вчерашнюю дату: если я сохраню SQL TIMESTAMP и прочту SQL DATE, фракция даты будет иметь часовой пояс базы данных, не тот из JVM.

Я что-то здесь упускаю?

Как настроить MySQL Connector / J 8.0, чтобы он работал согласованно с другими драйверами JDBC?

1 Ответ

0 голосов
/ 28 августа 2018

Типы SQL TIMESTAMP и DATE не содержат информацию о часовом поясе.

Аналогично, типы java.util.Date, java.sql.Date и java.sql.Timestamp не содержат информацию о часовом поясе. java.util.Date и java.sql.Timestamp содержат количество миллисекунд с 1 января 1970 года 00:00:00 UTC.

Их toString методы используют системный часовой пояс по умолчанию, но это не влияет на их значения.

Поскольку информация о часовом поясе не имеет смысла в данных даты или метки времени, вам не следует использовать ее в своих сравнениях.

Не сравнивайте значения, используя их строковые формы. Не используйте SimpleDateFormat вообще. Вместо этого сравните фактические значимые данные, которые каждый из них представляет, используя сравнение, на которое не влияют часовые пояса.

Самый простой способ сделать это - преобразовать данные в менее неоднозначные LocalDate и LocalDateTime типы:

LocalDateTime localClientTime = new Timestamp(clientTimeMillis).toLocalDateTime();
assertThat(date.toLocalDate())
        .as("date fraction from the database")
        .isEqualTo(localClientTime.toLocalDate());
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...