添加链接
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 一书。

    将您的意见发送给我们