如果您是初次接触 PHP 或 Oracle,那么设计一个高效处理日期的策略可能会比较困难。 您可能曾经在其他平台上成功应用过一些策略,但这些策略是否适合 Oracle 与 PHP 组合?
这篇 Oracle+PHP 文章将帮助您了解 PHP 和 Oracle 提供的用于处理日期和时间的功能,以及它们相互之间的关系。 这样,你将学习到使用何种技术来解决什么问题,并回答类似“是在 PHP 还是在 Oracle 中计算这两个日期之间的差”这样的问题。
Oracle 中的日期和时间
Oracle 提供了三个用于存储日期/时间值的数据类型:
表示日期和时间的
DATE
类型。
DATE
字段中存储的值包含有与世纪、年、月、日、小时、分钟和秒相对应的“组成部分”。 日期可以是公元前 4712 年 1 月 1 日到公元 9999 年 12 月 31 日这一范围中的任何一天。
从 Oracle9
i
开始提供的
TIMESTAMP
类型是
DATE
类型的有效扩展格式,并且符合 ANSI SQL 标准。 它提供了更大的时间精度,支持多达九位的小数,同时还能够存储时区信息。
从 Oracle9
i
开始提供的
INTERVAL
类型,它支持存储时间差(如“两年零五个月”或“三天零十八个小时零四十五分钟”),并可以与
DATE
或
TIMESTAMP
进行加法运算以生成一个新的
DATE
/
TIMESTAMP
值。
本文主要介绍
DATE
类型,但适用于
DATE
的大部分功能也适用于
TIMESTAMP
。 (有关
TIMESTAMP
和
INTERVAL
类型的更多背景知识,请阅读 Jonathan Gennick 在
Oracle 杂志
上发表的文章 和“
Finding the Time in Between
”(这两篇文章均在 2002 年 11 月 - 12 月刊中发表)。
Oracle 如何存储 DATE。
对于 Oracle 中的
DATE
类型,首先要了解的是,它的内部表示形式使它可以通过不同的方法进行显示和操作。 它有效地独立于任何特定的字符串格式。 如果使用
SELECT
选择一个
DATE
类型,Oracle 将自动把它转换为可读字符串,但这并不是值的实际存储方法。
使用
SYSDATE
选择当前的系统时间,
SYSDATE
返回
DATE
类型的值,是数据库所在的操作系统的当前日期和时间:
SELECT SYSDATE FROM dual /* e.g. 25-JUL-05 */
该格式受 Oracle 参数
NLS_DATE_FORMAT
的控制,可以根据每个会话进行更改(如下所示)。 了解一下内部表示形式:
SELECT DUMP(SYSDATE) FROM dual /* e.g. Typ=13 Len=8: 213,7,7,25,23,7,15,0 */
结果中由逗号分隔的值对应于 Oracle 存储日期和时间的每个部分(从世纪一直到秒)所使用的字节。此处有一个重要问题值得注意:在比较
DATE
类型时,将比较
DATE
的所有组成部分(一直比较到秒)。 在某些情况下,您可能要根据不同的时间单位(如年、月或日)比较两个日期。 这种情况下,可以使用
TRUNC
这样的函数对所比较的两个
DATE
的小时、分钟和秒部分进行向下取整。 有关更多详细信息,请参阅下面的“
日期运算
”。
如果您熟悉面向对象的编程,则也可将
DATE
类型看作是对象。 他们全都拥有属性(年、月、小时等)和行为,如:
SELECT SYSDATE - 7 FROM dual /* e.g. 18-JUL-05 */
该示例返回七天前的日期。 其它的“行为”包括
DATE
比较,这意味着您可以对日期执行
SORT BY
、
GROUP BY
以及查找界于两个日期之间的日期 (
BETWEEN
) 等,还可以进行减法操作: 从一个
DATE
中减去另一个
DATE
以获得整数的日期差(在使用
TIMESTAMP
时,将获得
INTERVAL
类型)。
在 DATE 类型和字符串之间进行转换。
TO_DATE()
和
TO_CHAR()
函数用于在 Oracle
DATE
“对象”和使用者可理解的日期字符串之间进行转换。 这两个函数均使用三个参数,一个要转换的值、一个可选的格式掩码和一个用于指定语言的可选字符串(例如,
FRENCH
)。 从概念上而言,格式掩码类似于
正则表达式
;您指定日期模式,该模式指示 Oracle 如何将匹配的字符串与
DATE
类型关联。
Oracle 数据库 SQL 参考
的“
格式模型
”中介绍了格式掩码。
使用
TO_CHAR
。 以下是一个简单示例,它再次使用了
SYSDATE
函数:
SELECT TO_CHAR( SYSDATE, 'YYYY-MM-DD HH24:MI:SS' ) FROM dual
/* e.g. 2005-07-26 17:34:04 */
详细了解一下该格式掩码,“
YYYY
”表示一个四位年份,“
MM
”表示一个两位月份,“
DD
”表示一个两位的月份中的日,“
HH24
”表示 24 小时制的小时,“
MI
”表示 0 和 59 之间的分钟,“
SS
”表示 0 和 59 之间的秒。注意,以下字符从格式掩码按原样传递到输出:
/ - , . ; :
其他字符串可以通过括在引号中进行“传递”:
SELECT TO_CHAR(SYSDATE, '"The time is now " HH24:MI:SS "precisely"') FROM dual
/* e.g. The time is now 17:38:22 precisely
您在该文档中将发现,还有很多格式掩码模式可用于满足各种使用情况。
注意:
TO_CHAR
也可以与
TIMESTAMP
类型结合使用。
使用 TO_DATE。
Oracle 可以使用与
TO_CHAR
相同的格式掩码将字符串分析为
DATE
类型。 假设有一个字符串 20050726173102:
SELECT TO_DATE( '20050726173102', 'YYYYMMDDHH24MISS' ) FROM dual
或者,要转换“Jul 26, 2005 17:13:05”,我可以使用:
SELECT TO_DATE('Jul 26, 2005 17:13:05', 'Mon DD, YYYY HH24:MI:SS') FROM dual
注意: 对于
TIMESTAMP
类型,等价函数为
TO_TIMESTAMP
。
更改默认的日期格式。
默认情况下,Oracle 根据
NLS_DATE_FORMAT
参数中定义的格式掩码显示
DATE
类型。 可以在如下所示的会话中更改此默认日期格式:
ALTER SESSION SET NLS_DATE_FORMAT = 'YYYY-MM-DD HH24:MI:SS'
日期运算。
为确定 2005 年 7 月 26 日 6 天后的日期,只要将值 6 与
DATE
对象相加:
SELECT TO_DATE( '20050726173102', 'YYYYMMDDHH24MISS' ) + 6 FROM dual
/* e.g. 2005-08-01 17:13:05 */
此类操作的最小整数单位是一天。 为减去 18 个小时,需要日期的相应小数:
SELECT TO_DATE( '20050726173102', 'YYYYMMDDHH24MISS' ) - (1/24 * 18) FROM dual
/* e.g. 2005-07-25 23:31:02 */
同样,要加上 59 秒:
SELECT TO_DATE( '20050726173102', 'YYYYMMDDHH24MISS' ) + (1/(24*60*60) * 59) FROM dual
在处理月份或年份时,由于两者均不能用常数天数表示(注意具有不同天数的闰年和闰月),因此需要
ADD_MONTHS
函数。 将 12 个月与某个日期相加:
SELECT ADD_MONTHS( TO_DATE( '20050726173102', 'YYYYMMDDHH24MISS' ), 12) FROM dual
/* e.g. 2006-07-26 17:31:02 */
注意: 要减去月数,请使用减号。
LAST_DAY
函数返回
DATE
类型的月份的最后一天:
SELECT LAST_DAY( TO_DATE( '20050701', 'YYYYMMDD' ) ) FROM dual
/* e.g. 2005-07-31 00:00:00 */
TRUNC
函数根据提供的日期掩码(即第二个参数),对
DATE
进行向下取整。 如果在进行
DATE
比较时需要从比较中消除秒和分钟这样的单位,可以使用该函数:
SELECT TRUNC( TO_DATE( '20050726173102', 'YYYYMMDDHH24MISS' ), 'DD' ) FROM dual
/* e.g. 2005-07-26 00:00:00 */
如果未提供日期掩码,
TRUNC
将对
DATE
向下取整到它所表示的日的起始。
其他与日期相关的函数包括
MONTHS_BETWEEN
(用于计算两个
DATE
类型之间的月数的整数差)、
NEXT_DAY
(用于获取与某个字符串文字(例如,“
MONDAY
”)匹配的下一个星期几的
DATE
类型)以及
ROUND
(类似于
TRUNC
,但按就近取整原则返回最近
DATE
)。
使用 Date 构建 SQL 语句
下面是一些在 SQL 语句中使用
DATE
类型,使用“emp”(员工)表(Oracle 中附带的示例数据的一部分)的示例。 “em”表的“hiredate”列使用
DATE
类型存储值。
找到在两个日期之间被辞退的所有员工:
SELECT
ename, TO_CHAR(hiredate, 'ddth Mon, YYYY')
WHERE
hiredate
BETWEEN
TO_DATE('1980-01-01','YYYY-MM-DD')
TO_DATE('1985-01-01','YYYY-MM-DD')
ORDER BY
hiredate
添加一个新员工:
INSERT INTO emp
empno,
ename,
hiredate,
deptno
VALUES
8002,
'JOHNSON',
'ANALYST',
7566,
TO_DATE('2005-07-22','YYYY-MM-DD'),
3000,
使用从
TO_YMINTERVAL
函数返回的
INTERVAL
类型找到所有在公司的工作时间超过 15 年的员工:
SELECT
WHERE
SYSDATE - TO_YMINTERVAL('15-00') > hiredate
PHP 中的日期和时间
可以在
PHP 手册
的
日期和时间函数
部分中找到所有基于 UNIX 时间戳的可用函数列表。 此处将主要介绍
date()
和
mktime()
函数,它们通常是您使用最频繁的函数。 注意,PHP 5.1.x 版引入了其他与日期相关的功能,而此处的示例只局限于可以在较早的 4.3.x 和 5.0.x 版中执行的操作,但它们也向前兼容 PHP 5.1.x。
PHP 中的日期和时间基于 UNIX 时间戳,即
UNIX epoch
(全球统一时间(格林尼治标准时间)1970 年 1 月 1 日 00:00:00)之前或之后的秒数。要查看 UNIX 时间戳,只需打印 PHP 的
time()
函数的结果,该函数相当于 Oracle 的
SYSDATE
:
print time(); // e.g. 1122391862
要加上或减去天、小时、分钟等单位,请将该单位转换为秒,然后将它直接应用于时间戳。 使用月和年这样的单位执行计算需要 mktime() 函数(如下所示)。
格式化 UNIX 时间戳。
date()
函数用于格式化 UNIX 时间戳:
print date('Y-m-d H:i:s',time()); // e.g. 2005-07-26 17:31:02
PHP 手册对
date()
函数的掩码进行了
详细介绍
。
date()
无法识别的任何字符(如标点字符)将自动“传递”到输出。 可以用反斜线对可能被误认为是格式一部分的字符进行转义,例如:
print date('l \t\he jS of F',1122391862); // e.g. Tuesday the 26th of July
生成“Tuesday the 26th of July”。
注意: 如果将格式掩码括在双引号中,则可能需要一个双反斜线来对某些字符进行转义;有关详细信息,请参阅
PHP 手册
的
字符串类型和双引号
部分。
date()
函数还可用于某些计算:
$years = range(2005, 2020);
foreach ( $years as $year ) {
if ( date('L', mktime(0,0,0,1,1,$year) ) ) {
print "$year is a leap year\n";
Which tells me:
2008 is a leap year
2012 is a leap year
2016 is a leap year
2020 is a leap year
还要注意
gmdate()
函数,它与
date()
函数几乎完全相同,唯一的区别就是前者将结果转换为格林尼治标准时间 (UTC)。
创建和操作 UNIX 时间戳。
mktime()
函数用于根据表示日期各个部分的给定整数值生成某个日期的 UNIX 时间戳。 可以将
mktime()
看作是与 Oracle 的
TO_DATE
函数等价的函数。例如:
$year = 2005;
$month = 7;
$day = 27;
$hour = 12;
$minute = 34;
$second = 43;
print mktime($hour, $minute, $second, $month, $day, $year);
mktime()
函数负责在跨越界限(如两个月之间的界限)时进行所需的调整:
$year = 2005;
$month = 7;
$day = 27 + 10; // Add 10 days
$hour = 12;
$minute = 34;
$second = 43;
print date('Y-M-d H:i:s',mktime($hour, $minute, $second, $month, $day, $year));
// e.g. 2005-Aug-06 12:34:43
此处,尝试为
mktime()
提供“July 37”(7 月 37 日),结果将调整为 August(8 月)。
要找到给定月份的最后一天,可以将后一个月以及一个零值用作该月的最后一天。例如:
$year = 2005;
$month = 8; // August
$day = 0; // Last day of July
$hour = 12;
$minute = 34;
$second = 43;
print date('Y-M-d',mktime($hour, $minute, $second, $month, $day, $year));
与
date()
函数一样,还有一个也调整为格林尼治标准时间的
gmmktime()
函数。
PHP 中的日期和时间存在的问题。
UNIX 时间戳的一个限制是,它们受当前 32 位主流 CPU 和操作系统的束缚。 这意味着您只能在 UNIX 平台上表示一组最大值为
2038 年
、最小值为 1902 年的日期。 如果该限制会影响你的应用程序,可以使用 PEAR
Date
库这一替换方案,该库尽管较慢但却很有用。
另一个问题领域是本地化,
date()
函数只支持英语星期日期和月份名。 如果只需支持少数几种语言,则会发现最简单的实用解决方案是使用相关数组转换名称。
注意,现在各方正采取措施来增强 PHP 的本地日期和时间函数。 身为核心 PHP 开发人员之一的 Derick Rethans 在他的
PHP Time Handling talk
(在德国召开的 ApacheCON 2005 大会中发表)中对此进行了概述。
问题是,您在何处划清界限? 您就在什么范围内处理日期相关的操作时? 在 PHP 中还是 Oracle 中? 本部分介绍了几种方法,可帮助您做出明智的决策。
通常,Oracle 的日期工具比 PHP 的日期工具更强大,前者因提供了范围更广的日期格式掩码,因此在分析日期字符串或格式化日期输出方面具有更大的灵活性。 除此以外,由于 UNIX 时间戳存在 32 位限制,
TIMESTAMP
可以存储时区以及本地化问题,因此您可以考虑将所有与日期相关的工作留给 Oracle 完成。 但应该指出的是,许多 Web 应用程序(尤其是企业内部网应用程序)只面向单个本地化信息区域,因此它们在单个语言和时区中运行。 此外,UNIX 时间戳的范围通常超出了应用程序将处理的数据类型的范围。 这种情况下,PHP 的日期时间函数不会引起问题。
日期存储。
下面介绍一些有关日期和时间存储方面的问题。对于某些数据库(如 PHP5 的内置
SQLite
)而言,可能不必使用 UNIX 时间戳。 对于 Oracle 而言,最好将
DATE
或
TIMESTAMP
用作列类型来存储日期和时间值。 支持的功能简化了日期操作,尤其是当
SELECT
基于
DATE
和
TIMESTAMP
时。您还将发现,以可视化方式检查日期将更为简单,因为 SQL*Plus 这样的工具能够以为用户可理解的形式显示日期。
日期格式化。
一个反对 Oracle 的日期格式化功能的意见(从分层体系结构的角度考虑)是,在应用程序的数据存储层中对输出进行格式化是一个“糟糕的实践”,这应该是表示层完成的工作。
如果您遇到一种不常见的情况 - 编写一个基于多个数据库实现运行的应用程序,那么这样的反面论调是合情合理的。 这种情况下,您应考虑采用 John Lim 所提供的
ADOdb
来帮助解决这个问题 - 请查看他提供的“
编写可移植 SQL 的技巧
”的“数据类型”部分。
更为常见的情况是,你将基于单个数据库进行开发,因此供应商抽象不会成为问题。 您应该考虑让 Oracle 通过各种方式完日期的格式化,而不是尝试在 PHP 中处理日期,如以下查询所示:
SELECT
ename,
TO_CHAR(hiredate, 'YYYY') AS hired_year,
TO_CHAR(hiredate, 'YYYYMM') AS hired_month,
TO_CHAR(hiredate, 'DD Mon, YYYY') AS hired_pretty,
TO_CHAR(hiredate, 'DD Mon, YYYY', 'NLS_DATE_LANGUAGE=GERMAN') AS hired_german
ORDER BY
hiredate
或者(或除此以外),在结果中返回一个适合 PHP 的
date()
函数(如下所示)的 UNIX 时间戳。 Oracle 的某些格式掩码(如“Month”)用空白字符填充输出,因此在这种情况下使用
date()
意味着编写较少的代码行。
转换 Oracle
DATE
类型和 Unix 时间戳。
以下两个 Oracle 函数针对
DATE
类型实现了这一转换。
将 UNIX 时间戳转换为 Oracle
DATE
类型:
CREATE OR REPLACE
FUNCTION unixts_to_date(unixts IN PLS_INTEGER) RETURN DATE IS
* Converts a UNIX timestamp into an Oracle DATE
unix_epoch DATE := TO_DATE('19700101000000','YYYYMMDDHH24MISS');
max_ts PLS_INTEGER := 2145916799; -- 2938-12-31 23:59:59
min_ts PLS_INTEGER := -2114380800; -- 1903-01-01 00:00:00
oracle_date DATE;
BEGIN
IF unixts > max_ts THEN
RAISE_APPLICATION_ERROR(
-20901,
'UNIX timestamp too large for 32 bit limit'
ELSIF unixts < min_ts THEN
RAISE_APPLICATION_ERROR(
-20901,
'UNIX timestamp too small for 32 bit limit' );
oracle_date := unix_epoch + NUMTODSINTERVAL(unixts, 'SECOND');
END IF;
RETURN (oracle_date);
以下 PHP 脚本显示了如何使用该方法。 注意,该脚本需要 PHP 5.x+,因为它使用新的 OCI 扩展函数名:
$conn = oci_connect('scott', 'tiger');
$sql = "
SELECT
FROM EMP
WHERE
hiredate
BETWEEN
unixts_to_date(:startdate)
unixts_to_date(:enddate)
ORDER BY
hiredate
$stmt = oci_parse($conn, $sql);
// Bind a UNIX timestamps to :startdate and :enddate
oci_bind_by_name($stmt, ":startdate", mktime(0,0,0,1,1,1981));
oci_bind_by_name($stmt, ":enddate", mktime(0,0,0,1,1,1990));
oci_execute($stmt);
print "NAME : HIREDATE\n";
while ( $row = oci_fetch_assoc($stmt) ) {
print "{$row['ENAME']} : {$row['HIREDATE']}\n";
oci_free_statement($stmt);
oci_close($conn);
相反,以下函数根据给定的
DATE
类型返回一个 UNIX 时间戳:
CREATE OR REPLACE
FUNCTION date_to_unixts(oracle_date IN DATE) RETURN PLS_INTEGER IS
* Converts an Oracle DATE to a UNIX timestamp
unix_epoch DATE := TO_DATE('19700101000000','YYYYMMDDHH24MISS');
max_date DATE := TO_DATE('20380101000000','YYYYMMDDHH24MISS');
min_date DATE := TO_DATE('19030101000000','YYYYMMDDHH24MISS');
unix_ts PLS_INTEGER;
BEGIN
IF oracle_date > max_date THEN
RAISE_APPLICATION_ERROR( -20902,'Date too large for 32bit UNIX timestamp' );
ELSIF oracle_date < min_date THEN
RAISE_APPLICATION_ERROR( -20902,'Date too small for 32bit UNIX timestamp' );
unix_ts := (oracle_date - unix_epoch) / (1/86400);
END IF;
RETURN (unix_ts);
The following query shows how it might be used:
SELECT
ename,
TO_CHAR(hiredate, 'YYYY') AS hired_year,
TO_CHAR(hiredate, 'YYYYMM') AS hired_month,
TO_CHAR(hiredate, 'ddth Mon, YYYY') AS hired_pretty
date_to_unixts(hiredate) AS hired_unixts
ORDER BY
hiredate
现在,当您遍历结果集时使用
date()
函数可轻松地将时间戳转换为格式化日期。
您现在已经了解了(或温习了)Oracle 和 PHP 中提供的日期时间功能。 现在,您已经了解了在 Oracle / PHP 应用程序中使用日期和时间的基础知识。 此外,您还对在处理日期时面临的设计和实现决策有了一定的认识。
Harry Fuecks
[
http://www.phppatterns.com
] 于 1999 年接触 PHP,此后作为 PHP 的开发人员和撰稿人而名声鹊起。他通过
Sitepoint
开发人员网络发布了大量初级和中级 PHP 文章,并编写有
The PHP Anthology
一书。
将您的意见发送给我们
Integrated Cloud Applications & Platform Services