【问题标题】:Adding hours, minutes and seconds to java SQL Date将小时、分钟和秒添加到 java SQL 日期
【发布时间】:2016-05-28 02:01:05
【问题描述】:

我在使用 Java 计算数据库中的值时遇到了一些问题。

我试图从我的数据库中按日期计算一些值,但它们有小时、分钟和秒。如果我使用java.sql.Date 进行搜索,它只允许我输入年、月和日,因此不会计算带有日期的值:year"-"month"-31 (hour:minutes:seconds != 00:00:00)"。

有没有办法在java.sql.Date 中添加小时、分钟和秒?如果不是,那有什么更好的方法呢?

我的代码:

public static int getAmountPos(Connection con, Integer month, Integer year) {

        java.sql.Date date1 = java.sql.Date.valueOf(year+"-"+month+"-01");
        java.sql.Date date2 = java.sql.Date.valueOf(year+"-"+month+"-31");

        Integer amount = null;
        String sql = "SELECT COUNT(*) AS 'cantidad' "
                + " FROM table.point_of_service "
                + " WHERE registration_date between ? AND ?";

        PreparedStatement ps = con.getPreparedStatement(sql);
        ps.setDate(1, date1);
        ps.setDate(2, date2);

        ResultSet rs = ps.executeQuery();

        if(rs.next()) {
            amount = rs.getInt("cantidad");
        }

        return amount;
    }

【问题讨论】:

  • registration_date 列的确切数据类型是什么?

标签: java mysql


【解决方案1】:

java.sql.Date 类不用于保存有关时间的信息。它只包含日期(年、月、日)。您应该使用java.sql.Timestamp 来满足您的需要。

【讨论】:

  • @Gerard:找到java.sql.Timestamphere的文档链接。
【解决方案2】:

tl;博士

更改代码以使用 (a) java.time 类型和 (b) 时间跨度的半开方法。

String sql = "SELECT COUNT(*) AS 'count_' "
           + "FROM table_.point_of_service_ "
           + "WHERE registration_date_ >= ? "  // Greater-than-or-equal.
           + "AND WHERE registration_date_ < ? ; ";  // Less-than (not 'or-equal') is Half-Open approach.

PreparedStatement ps = con.getPreparedStatement(sql);
ps.setObject( 1, YearMonth.of( 2016 , Month.FEBRUARY ).atDay( 1 ) );  // Calling `setObject` to handle `LocalDate` in JDBC 4.2 and later.
ps.setObject( 2, YearMonth.of( 2016 , Month.FEBRUARY ).plusMonths( 1 ).atDay( 1 ) );

java.sql

java.sql 类型仅用于将数据移入和移出数据库。通常不应用于业务逻辑或操作日期时间值。

  • java.sql.Date 类表示没有时间和时区的仅日期值。
  • java.sql.Time 的反面是没有日期和时区的时间。
  • 对于日期时间,使用java.sql.Timestamp。此值始终位于 UTC 中。

有关日期时间,请在您的ResultSet 上致电getTimestamp。

java.sql.Timestamp tsWhenRegistered = myResultSet.getTimestamp( "when_registered_" );

当然,您必须调用数据库中具有匹配数据类型的列。

java.time

在 Java 8 及更高版本中,为您的业务逻辑使用 java.time 框架。

避免使用旧的 java.util.Date/.Calendar 类,因为它们已被证明是麻烦、混乱和有缺陷的。它们已被 java.time 类取代。

从数据库中获取 java.sql 类型后立即转换为 java.time 类型。希望 JDBC 驱动程序将被更新以直接处理 java.time 类型,但在此之前自己进行转换。

Instant

Instant 是 UTC 时间线上的时刻。

Instant instant = tsWhenRegistered.toInstant(); // From java.sql to java.time.

时区

根据您的上下文应用时区 (ZoneId)。假设您的公司在魁北克。

ZoneId zoneId = ZoneId.of( "America/Montreal" );
ZonedDateTime zdt = instant.atZone( zoneId );

YearMonth

现在,从您的问题中看到的从 java.time 到 java.sql 的另一种方式。使用YearMonth 类,而不是将Integer 对象传递给年份和月份。请记住,这些 java.time 类现在已内置到 Java 中。因此,您可以在整个代码中使用它们。这为您提供类型安全和编译器检查,以及有效值的保证。

YearMonth yearMonth = YearMonth.of( 2016 , Month.FEBRUARY ); // Pass Month enum for type-safety and range-checking.
YearMonth yearMonth = YearMonth.of( 2016 , 2 );  // Or, pass month number 1-12.

重新执行问题中显示的方法,假设“when_registered_”是日期时间值,而不是您在问题中建议的仅日期(不太清楚)。

public static int getAmountPos( Connection con, YearMonth yearMonth ) {
…

顺便说一句,提示:在数据库名称中添加下划线可以防止与 SQL 中的关键字发生冲突。 SQL 标准明确承诺从不使用尾随下划线。

半开

不要使用 SQL BETWEEN。在日期时间工作中,最佳实践是“半开放”方法,其中开头是包含,而结尾是独占。搜索逻辑以greater-than-or-equal 开头,less-than 结尾(not 小于或等于结尾)。这避免了在瞬间确定一天结束的问题。

YearMonth → LocalDate

从YearMonth,我们可以获得该月的第一天和下个月的第一天作为仅日期 (LocalDate) 值。

LocalDate localDateStart = yearMonthArg.atDay( 1 ); // Get first-of-month.
LocalDate localDateStop = yearMonthArg.plusMonths( 1 ).atDay( 1 ); // Get first of *next* month.

如果您的驱动程序符合 JDBC 4.2 或更高版本,您应该能够通过 setObject 方法将这些 LocalDate 对象传递给 PreparedStatement,并通过 getObject 检索数据。

如果您的数据库列被定义为标准 SQL DATE 类型或类似的仅日期类型(无时间、无时区),以下是示例代码。

…
String sql = "SELECT COUNT(*) AS 'count_' "
           + "FROM table_.point_of_service_ "
           + "WHERE registration_date_ >= ? "  // Greater-than-or-equal.
           + "AND WHERE registration_date_ < ?";  // Less-than (not 'or-equal') is Half-Open approach.

PreparedStatement ps = con.getPreparedStatement(sql);
ps.setObject( 1, localDateStart );  // Calling `setObject` to handle `LocalDate` in JDBC 4.2 and later.
ps.setObject( 2, localDateStop );
…

如果您的驱动程序无法以这种方式执行,请用 java.sql.Date 对象替换。旧的日期时间类具有促进此类转换的新方法。这里我们需要java.sql.Date.valueOf 方法。从数据库中检索数据时的另一个方向,调用toLocalDate。

…
ps.setDate( 1, java.sql.Date.valueOf( localDateStart ) );  // Converting `LocalDate` to `java.sql.Date`.
ps.setDate( 2, java.sql.Date.valueOf( localDateStop ) );
…

LocalDate + ZoneId → ZonedDateTime → java.sql.Timestamp

如果您的数据库列没有定义为仅日期类型,而是日期时间类型,那么我们需要使用日期时间对象而不是上面看到的LocalDate 对象进行查询.这意味着 OffsetDateTime 对象,如果您的日期适用于 UTC,或者 ZonedDateTime 如果您的日期在某个其他时区有意义。

请记住,LocalDate 对象没有真正的意义,因为世界各地的日期在任何特定时刻都会变化。我们必须应用时区才能将每天的第一时刻作为ZonedDateTime 对象。请注意,由于夏令时和其他异常情况,一天的第一时刻并不总是00:00:00.0。要将java.time转换为java.sql,我们从ZonedDateTime对象中提取Instant对象,最后转换为java.sql.Timestamp。

public Integer countForYearMonth ( Connection connArg , YearMonth yearMonthArg ) {
    // CAUTION: Pseudo-code. Ignoring real-world issues such as closing resources and handling exceptions. Never run, so never tested.

    LocalDate localDateStart = yearMonthArg.atDay( 1 ); // Get first-of-month.
    LocalDate localDateStop = yearMonthArg.plusMonths( 1 ).atDay( 1 ); // Get first of *next* month.

    // Give those LocalDate objects real meaning by applying a time zone.
    ZoneId zoneId = ZoneId.of( "America/Montreal" );  // Perhaps pass as argument rather than hard-code a particular time zone.
    ZonedDateTime zdtStart = localDateStart.atStartOfDay( zoneId );  // Inclusive of first moment of February… 2016-02-01T00:00:00.0-05:00[America/Montreal]
    ZonedDateTime zdtStop = localDateStop.atStartOfDay( zoneId ); // Inclusive of first moment of March… 2016-03-01T00:00:00.0-05:00[America/Montreal]

    // Business logic is complete. So convert to java.sql for database access.
    java.sql.Timestamp tsStart = java.sql.Timestamp.from ( zdtStart.toInstant() );  // February 1st 2016 at 5 AM UTC.
    java.sql.Timestamp tsStop = java.sql.Timestamp.from ( zdtStop.toInstant() );  // March 1st 2016 at 5 AM UTC.

    Integer amount = null;
    String sql = "SELECT COUNT(*) AS 'count_' "
               + "FROM some_table_ "
               + "WHERE when_registered_ >= ? "  // Greater-than-or-equal.
               + "AND when_registered_ < ? "  // Less-than (not 'or-equal') is Half-Open approach.
               + "; ";

    PreparedStatement ps = connArg.getPreparedStatement(sql);
    ps.setDate( 1, tsStart );
    ps.setDate( 2, tsStop );

    ResultSet rs = ps.executeQuery();

    if( rs.next() ) {
        amount = rs.getInt( "count_" );
    }
    return amount;
}

【讨论】:

    【解决方案3】:

    我不同意那些说java.sql.date 不持有时间价值的人。当您在构造函数中传递一个较长的时间值时,它将保存该时间信息。由于这些方法已被弃用,因此您无法操纵该时间信息,但时间信息将持续存在并且不会丢失或截断。

    根据 java documentation:

    使用给定的毫秒时间值构造一个 Date 对象。如果给定的毫秒值包含时间信息,驱动程序会将时间组件设置为对应于 0 GMT 的默认时区(运行应用程序的 Java 虚拟机的时区)中的时间。

    现在回答这个问题,您转换为 Timestamp 或 Instant 或 LocalDate 以创建将在 sql 字符串中使用的完整日期。请参阅此StackOverflow for conversion details。 David Keen 有一个不错的 cheat sheet that is useful 和 Michael Sharhag has java 8 date and time conversion details。

    这样的事情应该可以工作:

    String dateString = year.toString() + "-" + month.toString() + "-01T00:00:00.0Z";
    Instant dtgInstant = Instant.parse(dateString);
    java.sql.Date timestamp = new java.sql.Date(dtgInstant.toEpochMilli());
    

    【讨论】:

    • 不确定您的答案的重点。 java.sql.Date 类与其所有相关的日期时间类一样,一团糟。在这种情况下,尤其是 bad hack。这个类从java.util.Date(日期+时间)扩展而来,但它的类文档清楚地告诉你忽略这个继承的事实。它和你应该假装它没有时间,而实际上它确实将它的时间调整为 UTC 中一天中的第一刻。这一切都巧妙地封装在accepted answer by Lewandowski的第一句话中。
    • 尽管 java.sql.Date 类是一团糟,但公认的答案使人们相信类的时间部分没有存储-“它所拥有的只是日期(年、月、日)。”我只是指出这个陈述是不正确的,文档也表明了这一点。我使用大量使用这种做法的遗留代码。我的回答显示了一种使用 java.sql.Date 作为问题的另一种解决方案的方法。
    • 我发现您的回答和评论自相矛盾。 java.sql.Date 类 假装 表示仅日期值,但正如您自己引用的文档所说,它实际上是在 UTC 时区调整为 00:00:00 的日期+时间。从技术上讲,它是自 1970 年初以来的毫秒数,继承自 java.util.Date 类。你说传递给构造函数的时间值被保留,“没有丢失或截断”,然后引用文档说传递的值确实被修改了。我建议你看一下 OpenJDK 源代码。
    • 此外,您将 java.time 类与旧的 java.sql(或 java.util)类混合看起来很愚蠢。如果您可以使用 java.time 类,请使用它们,因为它们旨在取代旧的类。旧的类 java.util.Date、java.util.Calendar、java.sql.Date、java.text.SimpleDateFormat 等等现在都是遗留的。我并不是要击败您的答案,但这似乎是误导性的建议。
    猜你喜欢
    • 1970-01-01
    • 2016-01-07
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-07-10
    相关资源
    最近更新 更多