最佳实践:如何通过 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
类旨在表示没有时间和时区的仅日期值。实际上它带有这两个额外的东西,但假装没有。现代替代品是LocalDate
。 LocalDate
类表示没有时间和时区的仅日期值。
但是java.sql.Date
(以及LocalDate
)是错误的类,因为您的列似乎不是定义为date-only类型,而是定义为date-time 类型。
您的问题忽略了这里的关键问题:时区。 时区对于确定日期至关重要。对于任何给定的时刻,日期在全球范围内因区域而异。例如,Paris France 中午夜后几分钟是新的一天,而 Montréal Québec 中仍然是“昨天”。
以continent/region
的格式指定proper time zone name,例如America/Montreal
、Africa/Casablanca
或Pacific/Auckland
。切勿使用 3-4 个字母的缩写,例如 EST
或 IST
,因为它们不是真正的时区,没有标准化,甚至不是唯一的 (!)。
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.Date
、Calendar
和 SimpleDateFormat
。
Joda-Time 项目现在位于maintenance mode,建议迁移到java.time 类。
要了解更多信息,请参阅Oracle Tutorial。并在 Stack Overflow 上搜索许多示例和解释。规格为JSR 310。
从哪里获得 java.time 类?
Java SE 8、Java SE 9 及更高版本 内置。 标准 Java API 的一部分,带有捆绑实现。 Java 9 添加了一些小功能和修复。 Java SE 6 和 Java SE 7 大部分 java.time 功能都在ThreeTen-Backport 中向后移植到 Java 6 和 7。 Android ThreeTenABP 项目专门为 android 适配 ThreeTen-Backport(如上所述)。 见How to use ThreeTenABP…。ThreeTen-Extra 项目通过附加类扩展了 java.time。该项目是未来可能添加到 java.time 的试验场。您可以在这里找到一些有用的类,例如Interval
、YearWeek
、YearQuarter
和more。
【讨论】:
以上是关于最佳实践:如何通过 JDBC 检查 SQL.DATE 中的特定 java.util.Calendar/Date?的主要内容,如果未能解决你的问题,请参考以下文章
Flink JDBC Connector:Flink 与数据库集成最佳实践