添加链接
link之家
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接

如果您是初次接触 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 一书。

    将您的意见发送给我们