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 类型仅用于将数据移入和移出数据库。通常不应用于业务逻辑或操作日期时间值。
有关日期时间,请在您的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;
}