在 Forge - 数据库和日历
上个月,我们继续探讨了 iCalendar 标准,该标准使程序能够交换日历和约会信息。正如我们所见,iCalendar 文件包含一个或多个事件和任务。如果我们将该文件通过 HTTP 服务器(如 Apache)提供,我们可以将其分发给任何拥有 iCalendar 兼容程序(如 Mozilla 的 Sunbird)的人。正如我们上个月所见,我们可以更进一步,使用 CGI 程序动态生成 iCalendar 文件。
尽管我上个月介绍和讨论的程序在有限的上下文中可能有用,但任何 Web 开发人员都应该清楚,至少可以说,将日期和时间信息保存在程序内部是愚蠢的。
跟踪此类数据的最佳方法之一是使用关系数据库,例如 PostgreSQL。关系数据库允许您确保输入的数据有效,并为您提供对部分或全部数据的快速、灵活的访问。此外,通过将日历信息存储在数据库中,您可以使用相同的来源创建同一日历文件的多个版本。
本月,我们将研究一个基于 Web 的程序的简单示例,该程序从关系数据库中获取日历信息,并使用它来生成 iCalendar 数据文件,然后可以将其导入到 iCalendar 兼容的程序中,例如 Mozilla 的 Sunbird。
如果要将日历信息存储在关系数据库中,我们需要至少定义一个表。这是因为关系数据库中的所有内容(通常包括配置和状态信息)都存储在一个二维表中,其中列定义单个字段,每行包含一条记录。例如,以下是我们如何在 PostgreSQL 中定义一个简单的事件表
CREATE TABLE Events ( event_id SERIAL NOT NULL, event_summary TEXT NOT NULL CHECK (event_summary <> ''), event_location TEXT NOT NULL CHECK (event_location <> ''), event_start TIMESTAMP NOT NULL, event_end TIMESTAMP NOT NULL, event_timestamp TIMESTAMP NOT NULL DEFAULT NOW(), PRIMARY KEY(event_id) );
上面的表包含六列。第一列 event_id 被定义为 SERIAL 类型。如果在向表中添加行时没有显式为 event_id 提供值,PostgreSQL 会自动检索一个新的整数值,最大值可达 231。PostgreSQL 允许您设置更大的上限以允许序列环绕到 1,或者两者都允许;有关更多详细信息,请参阅文档。
event_id 列唯一标识表中的行,我们通过将其标记为 PRIMARY KEY 来告诉数据库这一点。这不仅告诉其他数据库程序员哪个列将用于检索记录,而且确保值是唯一的,并且该列也被索引。
另一个自动填充的列是 event_timestamp。从定义来看,似乎我们可以(并且将会)将 event_timestamp 设置为显式值,并在必要时使用当前时间作为默认值。但是,每当我以这种方式定义列时,都意味着我从不期望为此列显式设置值。相反,我感兴趣的是让 PostgreSQL 使用当前日期和时间设置列的值。
请注意 event_summary 和 event_location 列都被定义为 TEXT 类型(即无限长度的文本字段),而 event_start、event_end 和 event_timestamp 都是 TIMESTAMP 类型,这是 SQL 标准的日期和时间表示方式。
此表中的所有列都被定义为 NOT NULL,这意味着它们不能被赋值为 SQL 的未定义值 NULL。NULL 与 true 和 false 不同,这可能会使新手有点难以理解。但是,如果您将 NULL 视为表示未知或未定义的值,则可能会更清楚。尽管 NULL 在区分 false 值和未知值方面非常有用,但通常最好尽可能减少它们的使用。实际上,我长期以来听到的建议,并重复给其他人的是,您应该默认将列定义为 NOT NULL,并在情况需要时将其开放为允许 NULL 值。
最后,请注意我们的两个文本列(event_summary 和 event_location)都被定义为 NOT NULL,并且具有完整性检查,以确保我们输入的内容不是空字符串。这种约束组合是否合适取决于您的数据库需求。您可能需要考虑是否要完全允许 NULL 值,以及是否要允许摘要和位置列使用空字符串。
尽管这个简单的定义旨在作为一个示例,但考虑一下如果我们有一个单独的 Locations 表,包含 location_id 和 location_name,然后用 location_id 替换文本 event_location 列,效果会好得多。这将具有标准化位置名称的优势,这将减少不一致性。它还可以让我们搜索在特定位置发生的所有事件。
完成表定义后,我们向表中添加一些索引。每个索引都确保从表中检索数据的速度比平时更快,但代价是每次 INSERT 操作都会增加额外的时间。以下是定义
CREATE INDEX event_location_idx ON Events(event_location); CREATE INDEX event_start_idx ON Events(event_start); CREATE INDEX event_end_idx ON Events(event_end);
现在我们已经定义了表和索引,我们可以开始使用一些事件填充数据库表。与往常一样,我们可以使用以下语法将新事件 INSERT 到表中
INSERT INTO Events (event_summary, event_location, event_start, event_end) VALUES ('Ides of March', 'Everywhere', '2005-March-15 00:00', '2005-March-15 23:59:59')
如您所见,上面的 INSERT 语句仅命名了 Events 中定义的六列中的四列。当我们检查新行时,我们发现以下内容
atf=# select * from events; -[ RECORD 1 ]---+--------------------------- event_id | 1 event_summary | Ides of March event_location | Everywhere event_start | 2005-03-15 00:00:00 event_end | 2005-03-15 23:59:59 event_timestamp | 2005-04-04 01:20:15.575032
如您所见,event_id(我们定义为 SERIAL 类型)已自动接收到值 1。此外,event_timestamp 已设置为我们执行查询时的日期和时间。
很容易想象我们如何可以使用 CGI 或更高级的系统(例如 mod_perl 或 Zope)通过基于 Web 的程序调用此 INSERT 语句。实际上,我们真的不必过多考虑数据是如何到达数据库的,特别是如果我们已对数据设置了适当的约束。我们可以假设数据库中存在的所有内容都是可靠的,并且服务器已拒绝任何违反我们规则的条目。
现在我们的数据库表中有一些活动,我们可以将它们检索到 CGI 程序中。然后,该程序以 iCalendar 格式生成输出,允许 iCalendar 客户端检索其数据。清单 1 包含该程序,它是上个月的 dynamic-calendar.py 程序的修改版本。正如我上个月提到的,我用 Python 编写这个程序很大程度上是因为创建 iCalendar 格式文件的模块相对匮乏。幸运的是,Python 有这样一个模块,我在这程序中利用了这一事实。
清单 1. db-calendar.py
#!/usr/bin/python # Grab the CGI module import cgi import psycopg from iCalendar import Calendar, Event from datetime import datetime from iCalendar import UTC # timezone # Log any problems that we might have import cgitb cgitb.enable(display=0, logdir="/tmp") # Send a content-type header print "Content-type: text/calendar\n\n" # Create a calendar object cal = Calendar() # What product created the calendar? cal.add('prodid', '-//Python iCalendar 0.9.3//mxm.dk//') # Version 2.0 corresponds to RFC 2445 cal.add('version', '2.0') # Create the database connection db_connection = psycopg.connect('dbname=atf user=reuven') db_cursor = db_connection.cursor() db_cursor.execute ('''SELECT event_id, event_summary, event_location, event_start, event_end, event_timestamp FROM Events ORDER BY event_start''') result_rows = db_cursor.fetchall() for row in result_rows: # Create one event event = Event() # Set the event ID event['uid'] = str(row[0]) + 'id@ATF' # Set the description and location event.add('summary', row[1]) event.add('location', row[2]) # Transform the dates appropriately event.add('dtstart', datetime(tzinfo=UTC(), *row[3].tuple()[0:5])) event.add('dtend', datetime(tzinfo=UTC(), *row[4].tuple()[0:5])) event.add('dtstamp', datetime(tzinfo=UTC(), *row[5].tuple()[0:5])) # Give this very high priority! event.add('priority', 5) # Add the event to the calendar cal.add_component(event) # Ask the calendar to render itself as an iCalendar # file, and return that file in an HTTP response print cal.as_string()
如您在清单 1 中所见,该程序非常简单明了。在导入一些模块后,我们创建一个日历对象并插入 iCalendar 强制要求的字段,指示日历的来源。
然后,我们连接到 PostgreSQL 服务器,该服务器假定在本地计算机上。尽管 Python 中存在几个用于 PostgreSQL 访问的数据库适配器,但我长期以来一直使用 psycopg,它既快速又稳定。要使用 psycopg 连接到 PostgreSQL,我们使用以下语法
db_connection = psycopg.connect ('dbname=atf user=reuven')
上面指示数据库名称为 atf,用户名是 reuven。您可能还需要指定服务器和密码作为附加参数,尤其是在生产系统上工作时。
连接到数据库后,我们获取一个游标,它允许我们提交查询并获取其结果
db_cursor = db_connection.cursor()
手头有游标后,我们现在可以将 SQL 查询发送到数据库,使用 Python 的三引号功能使我们的 SQL 更具可读性。现在我们检索结果。如果我们期望检索数十或数百行,我们可能希望一次获取一行,或者分批获取。但我知道此日历仅包含少量事件,因此我使用 fetchall() 方法一次获取所有事件
result_rows = db_crsor.fetchall()
result_rows 的每个元素都是来自 PostgreSQL 数据库的行。因此,我们(在 for 循环中)遍历行,检索出现的不同元素。
在大多数情况下,这非常简单明了。但是,当我们处理日期和时间时,事情会变得有点棘手——日期和时间是任何事件日历的重要元素!问题是 psycopg 使用来自 eGenix.com 的开源 mxDateTime 模块,这使得处理日期非常容易。但是 mxm 的 iCalendar 模块使用 Python 的 datetime 模块,这是不同的。因此,我们需要检索每个日期(对于事件的开始时间、结束时间和时间戳),将它们从 mxDateTime 的实例转换为 datetime 兼容的元组,使用该元组创建 datetime 的实例,然后将其传递给 event.add,使用以
event.add('dtstart', datetime(tzinfo=UTC(), event.add('dtend', datetime(tzinfo=UTC(), *row[3].tuple()[0:5]))
上面三行代码中 datetime() 的第二个参数完全符合我们所说的。它从返回的行中检索一列,并将其转换为元组。然后,我们获取序列的切片(使用 Python 方便的 [0:5] 表示法)来抓取 tuple() 返回的项目的子集。
但是我们不能将序列传递给 datetime();相反,它期望的是一些单独的元素。换句话说,datetime() 需要几个数字,而不是对数字列表的引用或指针。我们使用 Python 的 * 运算符将元组转换为其各个元素。最后,眼尖的读者会注意到,我们在元组的各个元素之前传递了 tzinfo 参数;这是因为 Python 要求我们在 * 运算符之前传递命名参数。
当然,调用 db-calendar.py 的结果是一个完全 iCalendar 兼容的文件,适用于导入到 Sunbird 或任何其他日历程序中。此外,只需修改 Events 数据库表的内容,我们就可以确保订阅我们日历的每个人都获得最新版本。
我们可以更进一步,修改 db-calendar.py,使其仅在其结果中包含某些事件。例如,也许日历只需要包含未来的事件;没有必要用过去的事件来 clutter 某人的日历(和带宽)。通过向我们的 SQL 查询添加一个简单的 WHERE 子句,我们可以轻松删除所有过去的事件。
更吸引人的是支持不同组和访问级别的日历的可能性。HTTP 支持使用用户名和密码进行身份验证,尽管 Sunbird 目前不支持此类保护,但我希望它(和其他程序)在将来会这样做。鉴于 CGI 程序可以轻松确定发出经过身份验证的 HTTP 请求的人员的用户名,因此可以毫不夸张地说,db-calendar.py 可以根据一组分配的权限或角色为不同的用户生成不同的输出。
最后,尽管在过去的几个月中,我们一直专注于 iCalendar 格式的输出,但没有任何理由表明我们只能将数据库的内容转换为 iCalendar 文件。实际上,我们很可能希望以纯 HTML 以及 iCalendar 格式显示我们的事件数据库。再一次,很容易看出我们如何使用 HTML 表格来做到这一点——再次证明关系数据库可以轻松地以多种不同的方式显示一组数据。
本月,我们已经了解了如何使用数据库来存储事件信息,这些信息最终将转换为 iCalendar 兼容的文件。使用数据库不仅使我们更有信心存储的数据是有效的,而且使我们能够快速轻松地创建动态生成的文件,这些文件适用于使用 iCalendar 格式的程序。
本文资源: /article/8263。
Reuven M. Lerner,一位长期的 Web/数据库顾问和开发人员,现在是西北大学学习科学专业的 graduate student。他的博客在 altneuland.lerner.co.il,您可以通过 reuven@lerner.co.il 与他联系。