最佳实践:如何通过 JDBC 检查 SQL.DATE 中的特定 java.util.Calendar/Date?

Posted

技术标签:

【中文标题】最佳实践:如何通过 JDBC 检查 SQL.DATE 中的特定 java.util.Calendar/Date?【英文标题】:Best Practice: How to check for a specific java.util.Calendar/Date in SQL.DATE by JDBC? 【发布时间】:2009-03-13 11:27:11 【问题描述】:

这是我从昨天开始就一直在努力解决的问题。

我有约会要保存在数据库中。它们由日期和时间组成,例如:

01.02.1970 14:00

(德语格式,在美国我想应该是 02/01/1970 2:00pm)。

第一个想法:将其保存为 SQL.DATE!

所以我创建了一个表:

CREATE TABLE appointments (id NUMBER(10) NOT NULL, datum DATE NOT NULL, PRIMARY KEY id)

到目前为止一切顺利。

现在我写了一个 DAO 来保存我通过网络表单输入的约会。后来我想写一个单元测试,检查约会是否正确保存。

相关测试部分如下:

JdbcDao myDao = new JdbcDao();
myDao.setDataSource(jdbcTemplate.getDataSource());      
myDao.saveAppointment(appointmentModel);

// Not needed but I saw, the appointment is saved in the database
setComplete();

// And now for the (sorry for the harsh words) pain in the *** part

String sql = "SELECT id, datum FROM appointments WHERE datum ... // <--

我试过了:

datum = ?

下面的调用

jdbcTemplate.query(sql, args, rowMapper);

在 args 数组中有一个 java.util.Date、一个 java.util.Calendar 或一个 java.lang.String ('dd.MM.yyyy'),它保存了替换 ?在准备好的语句中。

当然,这是个坏主意,因为数据库有类似的东西

DD.MM.YYYY HH:MI 

在表格行中(DD=天,MM=月,YY=年,HH=小时,MI=分钟)。

所以我找到了 BETWEEN sql 命令,重构(并尝试各种格式、输入、字符串、对象以在 args-array 中传递)SELECT 命令:

String sql = "SELECT id, datum FROM appointments WHERE datum BETWEEN to_date( ?, 'DD.MM.YYYY HH24:MI:SS') AND to_date( ?, 'DD.MM.YYYY HH24:MI:SS')

与许多其他尝试一样,如果我直接通过 sql-tool 输入它,例如

SELECT * FROM appointments WHERE datum BETWEEN to_date('01.02.1970 00:00:00', 'DD.MM.YYYY HH24:MI:SS') AND to_date('01.02.1970 23:59:59', 'DD.MM.YYYY HH24:MI:SS')

例如输出:

ID                      DATUM                   
----------------------  -------------------
70                      01.02.1970 11:11:11

但是我在 java 中的 jdbc-call 总是导致一个空的结果集。

长话短说:

如果由 java 对象表示的日期存在于数据库的 sql.DATE 列中,与给定时间无关,那么查询数据库的最佳做法是什么?

【问题讨论】:

【参考方案1】:

类似这样的:

PreparedStatement ps = conn.prepareCall("SELECT * FROM table WHERE someDate = ?");
ps.setDate(1, javaDate)

(根据记忆,语法可能不太正确)

您必须将 java.util.Date 对象转换为 java.sql.Date 对象。

这很简单:

java.sql.Date myDate = new java.sql.Date(oldDate.getTime());

oldDate 是一个 java.util.Date 对象。

【讨论】:

我非常喜欢“匹配 sql-string 和 java-object 的模式”,以至于我从来没有尝试过在不经过修饰的情况下传递一个普通的 Date-Object ......这很有效。【参考方案2】:

Answer by Sacre 正确但已过时。现代方式是使用 java.time 类。

tl;博士

StringBuild sql = new StringBuilder()
sql.append( "SELECT id_, datum_ " );
sql.append( "FROM appointments_ " );
sql.append( "WHERE datum_ >= ? " );  // Start is *inclusive*…
sql.append( "AND datum_ < ? ; " );   // …while end is *exclusive*. (Half-Open approach)

并传递一对Instant 对象。

pstmt.setObject( 1 , start ) ;  // Pass `java.time.Instant` object directly via `PreparedStatement::setObject` method.
pstmt.setObject( 2 , stop ) ;

使用对象而不是字符串

避免使用字符串在数据库中获取/存储日期时间值。将日期时间对象用于日期时间值。 Java 和 JDBC 对日期时间对象有丰富的支持。

使用 java.time

java.sql.Date 类旨在表示没有时间和时区的仅日期值。实际上它带有这两个额外的东西,但假装没有。现代替代品是LocalDateLocalDate 类表示没有时间和时区的仅日期值。

但是java.sql.Date(以及LocalDate)是错误的类,因为您的列似乎不是定义为date-only类型,而是定义为date-time 类型。

您的问题忽略了这里的关键问题:时区。 时区对于确定日期至关重要。对于任何给定的时刻,日期在全球范围内因区域而异。例如,Paris France 中午夜后几分钟是新的一天,而 Montréal Québec 中仍然是“昨天”。

continent/region 的格式指定proper time zone name,例如America/MontrealAfrica/CasablancaPacific/Auckland。切勿使用 3-4 个字母的缩写,例如 ESTIST,因为它们不是真正的时区,没有标准化,甚至不是唯一的 (!)。

ZoneId z = ZoneId.of( "America/Montreal" );
LocalDate today = LocalDate.now( z );

关于数据库中的时区……

您的数据库几乎可以肯定地将带有时区或与 UTC 的偏移量信息的日期时间值存储为 SQL 标准 TIMESTAMP WITH TIME ZONE 等类型的 UTC 值。 如果使用 TIMESTAMP WITHOUT TIME ZONE,则这些值没有时区或与 UTC 的偏移信息,并且不代表时间线上的实际时刻,但可能适用于未来足够远的约会,将它们存储为分区值存在与改变时区定义的政客发生冲突的风险,例如调整/采用/放弃夏令时。

如果存储为分区值,您需要确定日期在所需时区的开始和结束时刻,然后将该对点转换为 UTC 值,并在数据库中查询该对 UTC 值。

通常,在日期时间工作中定义时间跨度(例如此处需要的一天的持续时间)的最佳实践是使用半开放方法,其中跨度的开始包含 em> 而结尾是排他性的。因此,一天从一天的第一刻开始,一直到(但不包括)下一个天的第一刻。

不要假设一天的第一刻是 00:00:00。夏令时 (DST) 等异常意味着一天可能从 01:00:00 等时间开始。让 java.time 确定一天中的第一个时刻。

ZonedDateTime startZdt = localDate.atStartOfDay( z );
ZonedDateTime stopZdt = localDate.plusDays( 1 ).atStartOfDay( z );

还请记住,开始和停止之间的时间跨度不一定是 24 小时。 DST 等异常情况意味着一天可能持续 23 或 25 小时,或其他一些长度。

通过提取 Instant 对象将它们转换为 UTC。 Instant 类代表UTC 时间线上的时刻,分辨率为nanoseconds(最多九 (9) 位小数)。

Instant start = startZdt.toInstant();  // Convert to UTC value.
Instant stop = stopZdt.toInstant();

要执行 SQL 查询,请不要使用BETWEEN。该命令不是半开方法,而是使用封闭方法,其中开头和结尾都包含在内。

StringBuild sql = new StringBuilder()
sql.append( "SELECT id_, datum_ " );
sql.append( "FROM appointments_ " );
sql.append( "WHERE datum_ >= ? " );  // Start is *inclusive*…
sql.append( "AND datum_ < ? ; " );   // …while end is *exclusive*. (Half-Open approach)
PreparedStatement pstmt = conn.prepareStatement( sql.toString() );

将您的一对Instant 对象指定为SQL 语句要使用的参数。

pstmt.setObject( 1 , start ) ;  // Pass java.time.Instant object directly via `setObject` method.
pstmt.setObject( 2 , stop ) ;

侧边栏:根据 SQL 规范的承诺,在所有 SQL 标识符后面附加一个下划线,以避免与一千个保留字发生任何名称冲突。

请注意我们如何调用 setObject 来直接使用 JDBC 中的 java.time 类型。

如果您的 JDBC driver 尚不支持 JDBC 4.2 或更高版本,您必须回退到使用旧的 java.sql 类型。在这种情况下,我们需要java.sql.Timestamp。但这只是短暂的——尽量留在 java.time 中。

pstmt.setTimestamp( 1 , java.sql.Timestamp.from( start ) ) ;
pstmt.setTimestamp( 2 , java.sql.Timestamp.from( stop ) ) ;

关于java.time

java.time 框架内置于 Java 8 及更高版本中。这些类取代了麻烦的旧 legacy 日期时间类,例如 java.util.DateCalendarSimpleDateFormat

Joda-Time 项目现在位于maintenance mode,建议迁移到java.time 类。

要了解更多信息,请参阅Oracle Tutorial。并在 Stack Overflow 上搜索许多示例和解释。规格为JSR 310。

从哪里获得 java.time 类?

Java SE 8Java SE 9 及更高版本 内置。 标准 Java API 的一部分,带有捆绑实现。 Java 9 添加了一些小功能和修复。 Java SE 6Java SE 7 大部分 java.time 功能都在ThreeTen-Backport 中向后移植到 Java 6 和 7。 Android ThreeTenABP 项目专门为 android 适配 ThreeTen-Backport(如上所述)。 见How to use ThreeTenABP…

ThreeTen-Extra 项目通过附加类扩展了 java.time。该项目是未来可能添加到 java.time 的试验场。您可以在这里找到一些有用的类,例如IntervalYearWeekYearQuarter 和more。

【讨论】:

以上是关于最佳实践:如何通过 JDBC 检查 SQL.DATE 中的特定 java.util.Calendar/Date?的主要内容,如果未能解决你的问题,请参考以下文章

Spring对JDBC的最佳实践--上

Spring JDBC 连接池最佳实践

Flink JDBC Connector:Flink 与数据库集成最佳实践

Flink JDBC Connector:Flink 与数据库集成最佳实践

提供 MySQL JDBC 驱动程序密码的最佳实践

.什么是JDBC的最佳实践?