使用 SQLAlchemy

下一代 Python 对象关系映射器

Comments

简介

对象关系映射器(Object Relational Mappers,ORM)在过去数年吸引了不少人的目光。主要原因是 ORM 经常会在 Web 应用程序框架中被提起,因为它是快速开发(Rapid Development)栈中的关键组件。Django 和 Ruby on Rails 等 Web 框架采用了设计一个独立栈的方法,将自主开发的 ORM 紧密集成到该框架中。而其他框架,如 Pylons、Turbogears 和 Grok,则采用更加基于组件的架构结合可交换的第三方组件。两种方法都有各自的优势:紧密集成允许非常连贯的体验(如果问题映射到框架),而基于组件的架构则允许最大的设计灵活性。但是,本文的主题并不是 Web 框架;而是 SQLAlchemy。

SQLAlchemy 在构建在 WSGI 规范上的下一代 Python Web 框架中得到了广泛应用,它是由 Mike Bayer 和他的核心开发人员团队开发的一个单独的项目。使用 ORM 等独立 SQLAlchemy 的一个优势就是它允许开发人员首先考虑数据模型,并能决定稍后可视化数据的方式(采用命令行工具、Web 框架还是 GUI 框架)。这与先决定使用 Web 框架或 GUI 框架,然后再决定如何在框架允许的范围内使用数据模型的开发方法极为不同。

SQLAlchemy 的一个目标是提供能兼容众多数据库(如 SQLite、MySQL、Postgres、Oracle、MS-SQL、SQLServer 和 Firebird)的企业级持久性模型。SQLAlchemy 正处于积极开发阶段,当前最新的 API 将围绕版本 0.5 设计。请参阅参考资料部分,获取官方 API 文档、教程和 SQLAlchemy 书籍的链接。

SQLAlchemy 取得成功的一个证明就是围绕它已建立了丰富的社区。针对 SQLAlchemy 的扩展和插件包括:declarative、Migrate、Elixir、SQLSoup、django-sqlalchemy、DBSprockets、FormAlchemy 和 z3c.sqlalchemy。在本文中,我们将学习一篇关于新 0.5 API 的教程,探究一些第三方库,以及如何在 Pylons 中使用它们。

安装

本文假定您使用 Python 2.5 或更高版本,并且安装了子版本。Python 2.5 包括 SQLite 数据库,因此也是测试 SQLALchemy 内存的好工具。如果您已经安装了 Python 2.5,则只需通过设置工具安装 sqlalchemy 0.5 beta 。要获取设置工具脚本,请在您的终端中下载并运行以下 4 条命令:

  wget http://peak.telecommunity.com/dist/ez_setup.py
  python ez_setup.py
  sudo easy_install http://svn.sqlalchemy.org/sqlalchemy/trunk
  sudo easy_install ipython

前三行代码检查最新版本的 sqlalchemy,并将它作为包添加到您本地系统的 Python 安装中。最后一个代码片段将安装 IPython,它是一个实用的声明式 Python 解释器,我将在本文中使用它。首先,我需要测试已安装的 SQLAlchemy 版本。您可以测试自己的版本是否为 0.5.x,方法是在 IPython 或普通 Python 解释器中发起以下命令。

 In [1]: import sqlalchemy
   
 In [2]: sqlalchemy.__version__
   
 Out[2]: '0.5.0beta1'

SQLAlchemy 0.5 快速入门指南

新的 0.5 发行版在 SQLAlchemy 中引入了一些显著的变更。此外列出了这些变更的概要信息:

  • 声明式扩展是多数情况下建议的开始方式。
  • session.query() 可以接受任意组合的 class/column 表达式。
  • session.query() 或多或少也是 select() 的支持 ORM 的替代方法。
  • 查询提供了一些试验性的 update()/delete() 方法,用于实现基于标准的更新/删除。
  • 会话将在 rollback() 和 commit() 方法后自动过期;因此使用默认的 sessionmaker() 意味着您通常不必调用 clear() 或 close();对象将自动与当前的事务同步。
  • 使用 session.add()、session.add_all()(save/update/save_or_update 已删除)在会话中添加内容。

虽然 declarative 扩展从 0.4 开始便一直出现在 SQLAlchemy 中,但它也经过了一些小修改,这使它在大多数 SQLAlchemy 项目中都成为了一种强有力的便捷方式。新的 declarative 语法允许在一步中创建表、类和数据库映射。下面我们来看看这种新语法的工作原理,以我编写的一个用于跟踪文件系统变化的工具为例。

清单 1. 新 SQLAlchemy 声明样式
#/usr/bin/env python2.5
#Noah Gift

from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy import Table, Column, Integer, String, MetaData, ForeignKey

Base = declarative_base()
class Filesystem(Base):
    __tablename__ = 'filesystem'

    path = Column(String, primary_key=True)
    name = Column(String)

    def __init__(self, path,name):
        self.path = path
        self.name = name

    def __repr__(self):
        return "<Metadata('%s','%s')>" % (self.path,self.name)

通过这种新的声明样式,SQLAlchemy 能够在一步中创建一个数据库表、创建一个类以及类与表之间的映射。如果您刚开始接触 SQLAlchemy,或许应该学习这种建立 ORM 的方法。此外,了解另一种更加显式地控制各步骤的方式也是有益的(如果您的项目要求这种级别的详细程度)。

在阅读这段代码时,需要指出一些可能会让初次接触 SQLAlchemy 或声明性扩展的用户犯难的地方。首先,

Base = declarative_base()

行创建了一个类,稍后的 Filesystem 类便继承自该类。如果您保存并在 declarative_style 中运行该代码,然后将它导入到 IPython 中,则会看到以下输出:

In [2]: declarative_style.Filesystem?
Type:		DeclarativeMeta
Base Class:	<class 'sqlalchemy.ext.declarative.DeclarativeMeta'>
String Form:	<class 'declarative_style.Filesystem'>

这个 DeclarativeMeta 类型的魔力就是允许所有操作发生在一个简单的类定义中。

另一个需要指出的地方是本示例并未实际执行任何操作。在运行创建表的代码之前,将不会创建实际的表,并且,您还需要定义 SQLAlchemy 将使用的数据库引擎。这两行代码如下所示:

       engine = create_engine('sqlite:///meta.db', echo=True)
       Base.metadata.create_all(engine)

SQlite 是试验 SQLAlchemy 的理想选择,并且您还可以选择使用内存数据库,在这种情况下,您的代码行应如下所示:

        engine = create_engine('sqlite:///:memory:', echo=True)

或者,只创建一个简单的文件,如第一个例子所示。如果您选择创建一个基于 SQLite 文件的数据库,则可以通过抛弃数据库中的所有表从零开始,而不需要删除文件。为此,您可以发起以下代码行:

          Base.metadata.drop_all(engine)

此时,我们已经了解了创建 SQLAlchemy 项目和通过 SQLAlchemy API 控制数据库所需的知识。在开始实际应用之前,惟一需要掌握一点是会话的概念。SQLAlchemy “官方” 文档将会话描述为数据库的句柄。在实际应用中,它允许不同的基于事务的连接发生在 SQLAlchemy 一直在等待的连接池中。在会话内部,这通常是添加数据到数据库中、执行查询或删除数据。

要创建会话,请执行下面这些后续步骤:

        #establish Session type, only need to be done once for all sessions
        Session = sessionmaker(bind=engine)
        #create record object
        create_record = Filesystem("/tmp/foo.txt", "foo.txt")
        #make a unique session
        session = Session()
        #do stuff in session.  We are adding a record here
        session.add(create_record)
        #commit the transaction
        session.commit()

这些就是使 SQLAlchemy 正常运行所需的所有工作。虽然 SQLAlchemy 提供了一个非常复杂的 API 来处理许多复杂的事情,但它实际上非常容易使用。在本节结束时,我还想指出,上例使用 echo=True 创建引擎。这是查看由 SQLAlchemy 创建的 SQL 的便捷方法。对于 SQLAlchemy 初学者,强烈建议使用该方法,因为它会让您觉得 SQLAlchemy 不再那么神秘。现在,运行自己创建的一些代码,并查看 SQL 创建表的过程。

清单 2. SQLAlchemy SQL 表创建输出
2008-06-22 05:33:46,403 INFO
 sqlalchemy.engine.base.Engine.0x..ec PRAGMA
 table_info("filesystem")
2008-06-22 05:33:46,404 INFO sqlalchemy.engine.base.Engine.0x..ec {}
2008-06-22 05:33:46,405 INFO sqlalchemy.engine.base.Engine.0x..ec 
CREATE TABLE filesystem (
	path VARCHAR NOT NULL, 
	name VARCHAR, 
	PRIMARY KEY (path)
)

Pylesystem:类似于 Spotlight 或 Beagle 的实时文件系统元数据索引程序

抽象地讨论如何使用某个工具会让许多人不好理解,因此,我将使用 SQLAlchemy 演示如何创建一个元数据工具。此工具的目标是监控文件系统、创建和删除事件,以及在一个 SQLAlchemy 数据库中保存这些变更的记录。如果您曾经在 OS X 上使用过 Spotlight,或在 Linux® 上使用过 Beagle,那就应该使用过一款实时的文件系统索引工具。要继续本文,您需要运行 Linux 内核 2.6.13 或更高版本。

下一个示例比较大,大约有 100 行代码。查看整个示例并运行它,然后,我将介绍代码各部分的作用。要运行此脚本,您必须在终端中执行以下步骤:

  1. wget http://peak.telecommunity.com/dist/ez_setup.py
  2. sudo python ez_setup.py
  3. sudo easy_install
    "http://git.dbzteam.org/?p=pyinotify.git;a=snapshot;h=HEAD;sf=tgz"
  4. sudo easy_install http://svn.sqlalchemy.org/sqlalchemy/trunk
清单 3. 文件系统事件监控数据库
#/usr/bin/env python2.5
#Noah Gift 06/21/08
#tweaks by Mike Bayer 06/22/08  
import os

from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy import Table, Column, Integer, String, MetaData, ForeignKey
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from sqlalchemy.orm import scoped_session

from pyinotify import *

path = "/tmp"

#SQLAlchemy
engine = create_engine('sqlite:///meta.db', echo=True)
Base = declarative_base()
Session = scoped_session(sessionmaker(bind=engine))

class Filesystem(Base):
    __tablename__ = 'filesystem'

    path = Column(String, primary_key=True)
    name = Column(String)

    def __init__(self, path,name):
        self.path = path
        self.name = name

    def __repr__(self):
        return "<Metadata('%s','%s')>" % (self.path,self.name)

def transactional(fn):
    """add transactional semantics to a method."""

    def transact(self, *args):
        session = Session()
        try:
            fn(self, session, *args)
            session.commit()
        except:
            session.rollback()
            raise
    transact.__name__ = fn.__name__
    return transact


class ProcessDir(ProcessEvent):
    """Performs Actions based on mask values"""

    @transactional
    def process_IN_CREATE(self, session, event):
        print "Creating File and File Record:", event.pathname
        create_record = Filesystem(event.pathname, event.path)
        session.add(create_record)

    @transactional
    def process_IN_DELETE(self, session, event):
        print "Removing:", event.pathname
        delete_record = session.query(Filesystem).\
            filter_by(path=event.pathname).one()
        session.delete(delete_record)

def init_repository():
    #Drop the table, then create again with each run
    Base.metadata.drop_all(engine)
    Base.metadata.create_all(engine)
    session = Session()

    #Initial Directory Walking Addition Brute Force
    for dirpath, dirnames, filenames in os.walk(path):
        for file in filenames:
            fullpath = os.path.join(dirpath, file)
            record = Filesystem(fullpath, file)
            session.add(record)
        session.flush()

    for record in session.query(Filesystem):
        print "Database Record Number: Path: %s , File: %s " \
        % (record.path, record.name)

    session.commit()

if __name__ ==  "__main__":

    init_repository()

    #Pyionotify
    wm = WatchManager()
    mask = IN_DELETE | IN_CREATE
    notifier = ThreadedNotifier(wm, ProcessDir())
    notifier.start()

    wdd = wm.add_watch(path, mask, rec=True)

要查看此脚本的实际运行结果,您需要打开两个终端窗口。在第一个窗口中,运行 pylesystem.py 脚本。您将看到一系列输出内容,如下所示(请注意,以下版本经过适当缩减):

2008-06-22 07:18:08,707 INFO
 sqlalchemy.engine.base.Engine.0x..ec ['/tmp/ba.txt', 'ba.txt']
2008-06-22 07:18:08,710 INFO
 sqlalchemy.engine.base.Engine.0x..ec COMMIT
2008-06-22 07:18:08,715 INFO
 sqlalchemy.engine.base.Engine.0x..ec BEGIN
2008-06-22 07:18:08,716 INFO
 sqlalchemy.engine.base.Engine.0x..ec SELECT filesystem.path
 AS filesystem_path, filesystem.name AS filesystem_name 
FROM filesystem
2008-06-22 07:18:08,716 INFO sqlalchemy.engine.base.Engine.0x..ec []
Database Record Number: Path: /tmp/ba.txt , File: ba.txt

第一个脚本运行一个多线程文件系统事件监控引擎,它将 /tmp 的所有创建和删除变更写入到 sqlalchemy 数据库中。注意:由于它是多线程的,当您 完成此教程时,需要键入 Control + \ 来停止线程应用程序。

成功运行之后,您可以在第二个终端窗口中创建事件,新创建或删除的文件将实时添加到数据库中或从数据库中删除。如果您只创建了 /tmp 目录中的某个文件,比如说 touch foobar.txt,则会在第一个窗口中看到以下输出:

Creating File and File Record: /tmp/foobar.txt
2008-06-22 08:02:19,468 INFO
 sqlalchemy.engine.base.Engine.0x..4c BEGIN
2008-06-22 08:02:19,471 INFO
 sqlalchemy.engine.base.Engine.0x..4c INSERT INTO filesystem (path, name) VALUES (?, ?)
2008-06-22 08:02:19,472 INFO
 sqlalchemy.engine.base.Engine.0x..4c ['/tmp/foobar.txt', '/tmp']
2008-06-22 08:02:19,473 INFO
 sqlalchemy.engine.base.Engine.0x..4c COMMIT

记得您之前启用了 SQL echo 吗?鉴于此,当代码将此新条目添加到文件系统中时,您可以看到 SQL 语句。如果您现在删除该文件,您也可以看到删除的过程。下面是您键入 rm 语句 rm foobar.txt 时的输出:

Removing: /tmp/foobar.txt
2008-06-22 08:06:01,727 INFO
 sqlalchemy.engine.base.Engine.0x..4c BEGIN
2008-06-22 08:06:01,733 INFO
 sqlalchemy.engine.base.Engine.0x..4c SELECT filesystem.path
 AS filesystem_path, filesystem.name AS filesystem_name 
FROM filesystem 
WHERE filesystem.path = ? 
 LIMIT 2 OFFSET 0
2008-06-22 08:06:01,733 INFO
 sqlalchemy.engine.base.Engine.0x..4c ['/tmp/foobar.txt']
2008-06-22 08:06:01,736 INFO
 sqlalchemy.engine.base.Engine.0x..4c DELETE FROM filesystem WHERE filesystem.path = ?
2008-06-22 08:06:01,736 INFO
 sqlalchemy.engine.base.Engine.0x..4c [u'/tmp/foobar.txt']
2008-06-22 08:06:01,737 INFO
 sqlalchemy.engine.base.Engine.0x..4c COMMIT

在 Filesystem 类中,您添加了一个 transactional 方法,您将使用一个修饰类来处理将文件系统事件提交给数据库的语义。Pyinotify 中的实际 Filesystem I/O 监控由 ProcessDir 类完成,该类继承自 ProcessEvents 并覆盖了其中的方法。如果您注意了 process_IN_CREATE 和 process_IN_DELETE 方法,会发现它们都附加了一个 transactional 修饰类。随后,它们将接受创建或删除事件并对数据库执行修改。

还有一个名称为 initial_repository 的方法,每次运行脚本时它都会填充数据库,实现方法是销毁数据库中的表并重新创建。脚本的最底部将通知 Pyinotify 代码以不确定的方式运行,而这最终表示作为守护进程运行。

结束语

本文介绍了 SQLAlchemy 的一些特性,并演示了它和 API 的使用是多么简单。借助 SQLAlchemy 和开源库 Pyinotify,您还使用不到 100 行 Python 代码构建了一个 功能异常强大的工具。这是简单但功能强大的 ORM 的特性之一。它消除了复杂的关系数据库处理操作,现在它为用户添加了快乐而不是负担。随后,这些省下来的精力可以用于解决感兴趣的问题,因为 SQLAlchemy 将是最简单的环节。

如果您有兴趣了解更多关于 SQLAlchemy 的信息,则应该阅读本文末尾列出的 参考资料。其中包括一本出色的书籍和大量优秀的在线文档,您可以考虑研究其他一些使用 SQLAlchemy 的项目并扩展它们。最近一个较有兴趣的 SQLAlchemy 相关项目就是 Website reddit.com。它使用纯 WSGI 框架 Pylons 构建,并整合了 SQLAlchemy 作为其默认 ORM。我附带了到 reddit 完整源代码的链接。借助新掌握的 SQLAlchemy 知识,您应该能够快速实现自己的 reddit,并且应该能执行一些数据库查询操作。祝您好运!


下载资源


相关主题


评论

添加或订阅评论,请先登录注册

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=AIX and UNIX, Linux
ArticleID=347361
ArticleTitle=使用 SQLAlchemy
publish-date=10232008