摘要了解如何利用 Oracle XML DBPython 脚本和 PL/SQL 存储过程构建自己的受 Oracle 数据库支持的 Python 应用程序
尽管 Python 很快在开发人员之中普及但长久以来 Oracle 数据库一直是最出色的企业级数据库采用有效的方式将这两者结合在一起是比较令人感兴趣的主题但这实际上是真正的挑战因为二者都要付出很多
尽管受到警告但本文并不会对最杰出的 Python 和 Oracle 数据库特性进行概述而是提供一系列独立的示例本文借助一个示例让您了解如何采用互补的方法尝试将这两种技术结合使用尤其是本文将指导您利用 PL/SQL 存储过程(在 Python 脚本中编排其调用)创建 Oracle 支持的 Python 应用程序该应用程序在 Python 和数据库中实施业务逻辑
正如您将在本文中学习到的即使是轻型的 Oracle 数据库 g 快捷版 (XE) 也可以得到有效利用作为数据驱动的 Web 应用程序的数据库后端其前端层使用 Python 构建特别是Oracle 数据库 XE 支持 Oracle XML DB这是构建 Web 应用程序时通常需要的一组 Oracle 数据库 XML 技术
示例应用程序
在用户使用您的应用程序时收集有关用户执行操作的信息成为一种比较流行的接收用户反馈的机制通常相对于让用户明确表达偏好的任何调查来说并入在线应用程序中的点击跟蹤工具可以为您提供有关用户偏好的大量信息
举一个简单的例子假设您想从OTN — 新文章 RSS页面中选取三个最新的 Oracle 技术网 (OTN) 文章标题并将这些链接放到您的站点上然后您希望收集有关用户在您的站点上跟随这些链接中的每个链接的次数的信息这就是我们的示例将要做的现在让我们试着弄清如何实现所有这些功能首先必须决定如何在应用程序层之间分发业务逻辑实际上决定如何在应用程序层之间分发业务逻辑可能是规划数据库驱动的应用程序最具挑战性的部分尽管执行业务逻辑通常有多种方法但是您的工作是找到最有效的方法作为一般的经验当规划数据库驱动的应用程序时您应该认真考虑数据库中关键数据处理逻辑的实现这种方法可以帮助您削减与在 Web 服务器和数据库之间发送数据相关的网络开销并且可以减轻 Web 服务器的负担
将所有这些理论应用到我们的示例上例如将获得插入到数据库中的文章详细信息的负担放到在数据库中创建的存储过程上这样 Web 服务器不必再处理与维护数据完整性有关的任务这在实践中的意义是您不必编写特定 Python 代码这些代码负责跟蹤数据库中是否存在与其链接被点击的文章有关的记录如果不存在则插入该记录然后从OTN — 新文章 RSS页面中获取所需的所有详细信息通过让数据库自己跟蹤此类事情您可以获得具有更高可扩展性且更不易出错的解决方案在本例中Python 代码将只负责从 RSS 页面获取文章链接并在用户单击某个文章链接时向数据库发送一条消息
图 给出了示例组件如何彼此交互以及如何与外部源交互的图形描述
图 示例应用程序工作原理的高级视图
本文的其余部分介绍如何实现此示例应用程序有关如何设置和启动此示例的简要描述可以参考示例代码根目录下的 readmetxt 文件
准备工作环境
要构建此处讨论的示例您需要安装以下软件组件(参见 Downloads portlet)并使其在您的系统中正常工作
Apache HTTP Server x
Oracle 数据库 g 快捷版
Python 或更高版本
mod_python 模块
cx_Oracle 模块
有关如何安装上述组件的详细说明可以参考另一篇 OTN 文章为 Python Server Pages 和 Oracle 构建快速 Web 开发环境(作者Przemyslaw Piotrowski)
设计基础数据库
一般来说最好从设计基础数据库开始假设您创建了一个用户模式并授予其创建和操作模式对象所需的所有权限那么第一步就是创建基础表在这种特殊情况下您将需要一个唯一的名为 otn_articles_rss 的表创建该表的方式如下
CREATE TABLE otn_articles_rss (
guid VARCHAR() PRIMARY KEY
title VARCHAR()
pubDate VARCHAR()
link VARCHAR()
clicks INTEGER
);
下一步是设计一个将在 Python 代码中调用的名为 count_clicks 的存储过程它更新 otn_articles_rss 表中的数据继续 count_clicks 过程之前您必须先回答以下问题当 count_clicks 尝试更新尚未插入到 otn_articles_rss 表中的文章记录的 clicks 字段时会发生什么情况呢?假设一个新项目刚刚添加到 RSS 页面然后指向该项目的链接出现在您的站点上当有人单击该链接时系统将从负责处理指向 OTN 文章的链接上执行的单击次数的 Python 代码中调用 count_clicks PL/SQL 过程显然处理第一次单击时在 count_clicks 过程中发出的 UPDATE 语句将失败因为现在还没有要更新的行
要适应此类情况您可以在 count_clicks 过程中实现一个 IF 块如果由于 UPDATE 找不到指定的记录而将 SQL%NOTFOUND 属性设置为 TRUE 时该块会发挥作用在该 IF 块中只要指定了 guid 和单击次数您就可以先将一个新行插入到 otn_articles_rss 表中之后您应该提交这些更改以便这些更改立即可用于其他用户会话这些会话可能也需要更新新插入的文章记录的 clicks 字段最后您应该更新该记录设置其 titlepubDate 和 link 字段该逻辑可以作为一个单独的过程(比如 add_article_details)来实现该过程的创建方式如下
CREATE OR REPLACE PROCEDURE add_article_details (gid VARCHAR clks NUMBER) AS
item XMLType;
heading VARCHAR();
published VARCHAR();
url VARCHAR();
BEGIN
SELECT extract((
)getXML()
//item[contains(guid ||gid||)>])
INTO item FROM DUAL;
SELECT extractValue(item //title)
extractValue(item //pubDate)
extractValue(item //link)
INTO heading published url FROM DUAL;
UPDATE otn_articles_rss SET
title = heading
pubDate = published
link = url
clicks = clicks + clks
WHERE guid = gid;
END;
/
正如您所见该过程接受两个参数gid 是其链接受到单击的文章的 guidclks 是文章查看总次数的增量在该过程主体中您获得 RSS 文档的所需部分作为 XMLType 实例然后提取信息之后该信息将立即用于填充 otn_articles_rss 中与正在处理的 RSS 项目关联的记录
借助 add_article_details您可以继续下一环节按照如下方式创建 count_clicks 过程
CREATE OR REPLACE PROCEDURE count_clicks (gid VARCHAR clks NUMBER) AS
BEGIN
UPDATE otn_articles_rss SET
clicks = clicks + clks
WHERE guid = gid;
IF SQL%NOTFOUND THEN
INSERT INTO otn_articles_rss(guid clicks) VALUES(gid );
COMMIT;
add_article_details (gid clks);
END IF;
COMMIT;
END;
/
事务考虑事项
在上面清单中所示的 count_clicks 存储过程中注意 COMMIT 的使用要紧跟在 INSERT 语句之后最重要的是之后要调用 add_article_details其执行时间可能较长通过在这个阶段提交新插入的文章记录立即用于其他可能的更新否则要等待 add_article_details 完成
考虑以下示例假设 RSS 页面刚刚更新并且一个全新的文章链接变为可用接下来两个不同的用户加载您的页面并几乎同时单击这个新链接因此将进行两个对 count_clicks 的同时调用在本例中首先发生的调用将一条新记录插入到 otn_articles_rss 表中然后它将调用 add_article_details虽然正在执行 add_article_details但对 count_clicks 的另一个调用可以成功执行更新操作增加总单击次数但是如果此处忽略了 COMMIT那么第二个调用将找不到用于更新的行因此尝试执行另一个插入事实上这将导致不可预测的结果它将导致独特的违反约束的错误并且会丢失将第二次 count_clicks 调用进行的更新
此处最令人感兴趣的部分是在 count_clicks 过程主体结尾处执行另一个 COMMIT 操作正如您所猜测的需要在这个阶段提交以便从更新的记录中去除锁定从而使该记录立即可用于其他会话执行的更新有些人可能会说这个方法降低了灵活性使客户端无法根据自己的判断提交或回滚事务但是在这种特殊的情况下这并不是一个大问题因为无论如何从调用 count_clicks 开始的事务都应该立即提交这是因为当用户单击某个文章链接以离开您的页面时始终会调用 count_clicks
构建前端层
既然已经创建了存储过程并且准备好在应用程序中使用那么您必须弄清如何从前端层编排在数据库中实现的所有这些应用程序逻辑片段所执行的整个操作流这就是 Python 派上用场的地方了
我们先来看一个简单的实现为了开始您必须编写一些 Python 代码这些代码将负责从OTN — 新文章 RSS页面获取数据然后您将需要开发一些代码这些代码将处理在 Web 页面中的 OTN 文章链接上执行的单击最后您将需要构建该 Web 页面本身为此您可能会使用 Python 的一种服务器端技术比如 Python Server Pages (PSP)这使得将 Python 代码嵌入到 HTML 中成为可能
为了编写 Python 代码您可以使用您喜欢的文本编辑器如 vi 或记事本创建一个名为 oraclepersistpy 的文件然后在其中插入以下代码将该文件保存到 Python 解释器可以找到的位置
import cx_Oracle
import urllib
import xmldomminidom
def getRSS(addr):
xmldoc = xmldomminidomparseString(urlliburlopen(addr)read())
items = xmldocgetElementsByTagName(item)
return items
def getLatestItems(items num):
latest=[]
inxs = [titleguidpubDatelink]
myitems = [item for index item in enumerate(items) if index < num]
for item in myitems:
latestappend(dict(zip(inxs[itemgetElementsByTagName(inx)[]firstChilddata for inx in inxs])))
return latest
正如您所猜测的上面所示的 getRSS 函数将用来从 RSS 页面获取数据并将该数据作为一个 DOM 对象返回getLatestItems 专门用来处理该 DOM 文档将该文档转换为 Python dictionary 对象
在 getLatestItems 函数中注意列表内涵(一个新的 Python 语言特性)的使用它提供了一种出色的方法可显着简化数据处理任务的编码
下一步涉及一些代码的创建这些代码将处理在指向 OTN 文章的链接上执行的单击这些链接是从OTN — 新文章 RSS页面中获取并放置到 Web 页面上的为此您可以开发另一个自定义 Python 函数(比如说 processClick)每次用户单击您 Web 页面上的 OTN 文章链接时都会调用该函数要实现 processClick将以下代码添加到 oraclepersistpy
def processClick(guid clks = ):
db = nnect(usr pswd /XE)
c = dbcursor()
cexecute(call count_clicks(:guid :clks) {guid:guid clks:clks})
dbclose()
以上代码提供了实际运行的 cx_Oracle 的一个简单示例它首先连接到基础数据库然后它获得一个 Cursor 对象之后使用该对象的 execute 方法调用在之前的设计基础数据库部分讨论的 count_clicks 存储过程
现在您可以继续下一环节构建 Web 页面由于这是仅用于演示的应用程序因此该页面可能非常简单只包含从 RSS 页面获得的链接在 APACHE_HOME/htdocs 目录中创建一个名为 clicktrackpsp 的文件然后在其中插入以下代码
<html>
<head>
<meta httpequiv=ContentType content=text/html; charset=UTF>
<title>latest OTN articles</title>
</head>
<body>
<h>Three most latest OTN articles</h>
<%import oraclepersist
url =
doc = oraclepersistgetRSS(url)
articles = oraclepersistgetLatestItems(doc )
for article in articles:
%>
<% import urllib %>
<a href=<%= str(article[link]) %>
onclick = thishref = /dispatcherpsp?url=<%=urllibquote_plus(article[link])+str(&guid=)+urllibquote_plus(article[guid]) %>>
<%=str(article[title]) %>
</a><br/>
<%
%>
</body>
</html>
正如您所见以上文档包含几个嵌入的 Python 代码块在第一个块中您从之前按照该部分所述创建的 oraclepersist 模块调用函数获得列表的一个实例该列表的项目代表三篇最新的 OTN 文章然后在 for 循环中循环该列表为该列表中存在的每个文章项目生成一个链接令人感兴趣的是尽管这些链接中的每个链接都引用相应的 OTN 文章地址但是链接的 onclick 处理程序将动态修改链接到 dispatcherpsp 页面的目标该目标需要在 APACHE_HOME/htdocs 目录中创建将两个参数(即 guid 和 url)附加到每个动态生成的链接向 dispatcherpsp 提供有关正在加载的文章的信息
以下是 dispatcherpsp 的代码
<html>
<body>
<%
import oraclepersist
import urllib
from mod_python import util
params = utilFieldStorage(req)
oraclepersistprocessClick(urllibunquote_plus(params[guid]value) )
pspredirect(urllibunquote_plus(params[url]value))
%>
</body>
</html>
在以上代码中借助 FieldStorage 类的帮助访问了附加到 URL 的参数该类来自 mod_python 网页上提供的 Mod_python 手册中描述的 util 模块然后从我们的 oraclepersist 自定义模块中调用 processClick 函数将从 URL 中提取的 guid 作为第一个参数传递将 (意味着一次单击)作为第二个参数传递最后将您的浏览器重定向到要加载的文章的位置
现在可以测试这个应用程序了由于您处理的是实时数据因此您必须连接到互联网建立连接之后将浏览器指向 因此应该出现一个包含指向 OTN 最新文章的三个链接的简单 Web 页面如图 所示
图 这是加载时的应用程序页面
单击任一文章链接并查看所发生的情况从用户的角度您将只看到文章正加载到浏览器中如图 所示
图 当跟随应用程序页面上的文章链接时用户只能看到文章本身
负责收集有关单击信息的代码将在后台运行为了确保该代码已经这样操作您可以连接到基础数据库并发出以下查询
SELECT * FROM otn_articles_rss甚至在完全加载文章文档之前上述代码应该输出一个包含有关正在加载的文章信息的行在 clicks 字段中显示 随后对此链接进行的每个单击将使 clicks 字段的值增加
采用 Pythonic 方法
在前面部分中编写的代码结构与采用 Pythonic 方法实现的代码看起来不太相同尤其是您按照一定的顺序实现了一组将从在 HTML 中嵌入的代码调用的函数将一个函数返回的结果用作另一个函数的参数实际上这是采用任何其他脚本语言(比如说 PHP)结构化您的代码的方式
尽管 Python 的真正功能在于它能够隐藏令人厌烦的实现详细信息从而提供一个简单优美而有效的编码解决方案字典列表和列表内涵是常用的 Python 内置类型在处理结构化数据时可以显着简化您的代码返回在前面部分中讨论的 oraclepersistpy 脚本对其进行升级以便最大程度地利用这些杰出的 Python 语言工具为了避免混淆您可以将修订保存在一个单独的名为 oraclepersist_listpy 的文件中
import cx_Oracle
import urllib
import xmldomminidom
url =
inxs = [titleguidpubDatelink]
num =
def getRSS(addr):
xmldoc = xmldomminidomparseString(urlliburlopen(addr)read())
items = xmldocgetElementsByTagName(item)
return items
articles = [dict(zip(inxs[itemgetElementsByTagName(inx)[]firstChilddata for inx in inxs])) for index item in enumerate(getRSS(url)) if index < num]
def processClick(guid clks = ):
db = nnect(usr pswd /XE)
c = dbcursor()
cexecute(call count_clicks(:guid :clks) {guid:guid clks:clks})
dbclose()
从以上代码可以看出利用列表内涵(一种非常有效的结构化应用程序数据的机制)可以显着减少代码总量此外客户端也不必显式调用模块函数因此您现在可以重新编写按照前面部分所述嵌入在 clicktrackpsp 中的 Python 代码块如下所示
<%import oraclepersist_list
for article in oraclepersist_listarticles:
%>
尽管现在它更为简洁但用户不需要进行任何更改
但是有人可能会说将 PSP 页面中的代码与其后端连接实在不是一个灵活的方法例如将要显示的链接数量以及要使用的 RSS 地址硬编码到 oraclepersist_listpy 脚本中借助这个新的语法您无法根据需要动态更改这些参数要解决此问题可以将列表内涵封装在 oraclepersist_listpy 脚本中的某个函数中如下所示
def getLatestItems(num = url = ):
inxs = [titleguidpubDatelink]
return [dict(zip(inxs[itemgetElementsByTagName(inx)[]firstChilddata for inx in inxs])) for index item in enumerate(getRSS(url)) if index < num]
正如您所见以上代码仍然利用了基于使用列表内涵列表和字典的高级语法从而允许在 clicktrackpsp 页面中动态更改参数以下代码片段将阐释现在如何显式指定要显示的文章链接数量
<%import oraclepersist_list
for article in oraclepersist_listgetLatestItems():
%>
使用面向对象的方法
尽管 Python 中的面向对象编程 (OOP) 是完全可选的但利用该范例可以最大程度地减少冗余高效地自定义现有代码与其他现代语言一样Python 允许您使用类封装逻辑和数据简化了数据定义和数据操作
回到在前面部分中讨论的 oraclepersist_listpy 脚本将 processClick 函数替换为如下所示的 HandleClick 类
class HandleClick:
def __init__(self usrname=usr password =pswd orcldb=/XE):
selfdbconn = nnect(usrname password orcldb)
def __del__(self):
selfdbconnclose()
def processClick(selfguidclks):
selfdbconncursor()execute(call count_clicks(:guid :clks) {guid:guid clks:clks})
假设您将修订保存在 oraclepersist_classpy 文件中更新后的 dispatcherpsp 现在可能如下所示
<%
import oraclepersist_class
import urllib
from mod_python import util
params = utilFieldStorage(req)
h = oraclepersist_classHandleClick()
hprocessClick(urllibunquote_plus(params[guid]value) )
pspredirect(urllibunquote_plus(params[url]value))
%>
下面您创建 HandleClick 类的一个实例然后调用它的 processClick 方法正确传递参数就像您之前所做的那样
在此处所讨论的 HandleClick 类中特别令人感兴趣的是特殊类方法 methods __init__ 和 __del__ 的使用与其他特殊方法一样您从不直接调用它们相反Python 隐式调用它们以响应在实例生命周期期间发生的某些事件因此在创建实例时调用 __init__ 构造函数在销毁实例之前调用 __del__ 析构函数
在上面的示例中您在构造函数中连接到数据库并在析构函数中关闭该连接但在某些情况下采用这些方法实现更多操作可能是非常令人感兴趣的例如您可能希望在销毁实例之前从析构函数中发出 SQL 语句以下代码片段将阐释如何重新编写 HandleClick 类以便从析构函数中而不是从某个显式调用的类方法中调用 count_clicks 存储过程
class HandleClick:
def __init__(self usrname=usr password =pswd orcldb=/XE):
selfdbconn = nnect(usrname password orcldb)
selfparams ={}
def __del__(self):
selfdbconncursor()execute(call count_clicks(:guid :clks) selfparams)
selfdbconnclose()
def addArticleClick(selfguidclks):
selfparams[guid]=guid
selfparams[clks]=clks
正如您所见更新的 HandleClick 类中不再有 processClick相反客户端代码应调用 addArticleClick该函数用要传递给 count_clicks 存储过程的参数填充该类的属性 params dictionary将从析构函数中调用 count_clicks 存储过程因此现在您可以重新编写嵌入在 dispatcherpsp 页面中的 Python 代码块如下所示
<%
import oraclepersist_class
import urllib
from mod_python import util
params = utilFieldStorage(req)
h = oraclepersist_classHandleClick()
haddArticleClick(urllibunquote_plus(params[guid]value) )
del h
pspredirect(urllibunquote_plus(params[url]value))
%>
注意此处使用 del 语句取消包含绑定对 HandleClick 类的某个实例的引用的 h 变量由于这是对该实例的唯一引用因此之后 Python 将使用一种名为垃圾回收的机制隐式删除该实例删除后将自动触发 __del__ 析构函数执行 SQL 语句然后关闭连接
上面的示例极好地说明了采用 Python 开发面向对象的代码时使用特殊方法可以获取的优势在这个特殊示例中客户端代码只负责为要针对数据库发出的查询设置参数而 Python 隐式执行其余操作
结论
正如您在本文中所学到的开发一个可扩展的数据库驱动的 Web 应用程序需要进行较良好的规划继续构建应用程序组件和编写代码之前您必须首先决定可以在数据库中实现的应用程序逻辑的数量以及可以在前端层实现的操作
设计文章示例时将一些数据处理逻辑放到数据库中实现几个 PL/SQL 存储过程在这里您学习了如何使用 Oracle XML DB 特性从网页中获取 XML 数据然后从获取的 XML 文档中提取所需的信息然后构建一些 Python 代码用以编排存储过程所执行的完整操作流依次从构建的 PSP 页面中调用这些 Python 代码以实现应用程序的前端层因此您获得了相应的应用程序该应用程序从网页中获取某些实时数据并跟蹤用户在您站点上的活动将该信息存储在数据库中在 Python 端您看到了如何使用 Python 语言的内置工具获取保留以及操作结构化数据这些工具包括列表字典和列表内涵您还了解了在将应用程序逻辑和数据封装到类中时如何利用 Python 的面向对象的特性