SQLAlchemy

作者: Reuven Lerner

尽管有时关系型数据库似乎已经过时,正在为非关系型数据库(NoSQL)让路,例如 MongoDB 和 Cassandra,但仍然有大量的系统依赖于关系型数据库。而且,虽然没有要求关系型数据库必须使用 SQL 作为其查询语言,但很少有数据库产品不这样做。

好消息是 SQL 相对容易使用,尤其是在查询简单明了的情况下。创建表、向表中插入数据、更新数据以及编写查询以检索部分或全部数据既快速又容易。SQL 还使组合(“连接”)来自多个表的信息变得相当容易,让您可以规范化数据,同时最大限度地保持速度和灵活性。

SQL 本身可能不难使用,但您很少在真空中使用它。通常,您的 SQL 语句驻留在您编写的程序中。SQL 作为文本字符串保存在应用程序中,然后通过网络套接字发送到服务器。

这存在几个问题。首先,这意味着您必须在同一个程序中混合两种不同的语言。在您努力编写并尽力确保可维护的 Web 应用程序中,您有一种完全独立的语言的代码,位于字符串内部,您无法直接测试或维护。

即使 SQL 查询不是写在字符串内部,您仍然会面临这样一个事实:您的 Web 应用程序的大部分是用一种语言编写的,但您的数据操作例程是用另一种语言编写的。无论您如何划分,Web 应用程序都包含 HTML、CSS 和 JavaScript 组件,以及您正在使用的任何服务器端语言。将 SQL 添加到其中只会使事情变得更加复杂。

即使 SQL 和典型的服务器端语言在 Web 应用程序的语法方面处于同等地位,它们处理数据的方式也存在根本的不匹配。SQL 使用表中的行和列进行操作;关系型数据库中的所有内容都必须符合这种以表为中心的视图。相比之下,现代编程语言具有丰富多样的数据结构,并且通常在某种程度上是面向对象的。

弥合过程式代码和 SQL 之间差距的库被称为对象关系映射器,或 ORM。ORM 通常将数据库记录表示为特定类的实例。为了表示 50 条记录,您需要 50 个类实例,每个实例的状态反映该记录中列的名称、类型和值。

Martin Fowler 描述了在面向对象的数据结构和数据库之间传递数据的两种基本范例。在第一种范例中,称为活动记录,每个实例都直接绑定到数据库中的一行,并且类本身(以及每个对象)负责确保数据保存到数据库中。换句话说,活动记录要求您创建一个类,并且它处理对象关系划分的两端。Ruby on Rails 中的活动记录类(毫不奇怪)是这种范例的实现,并提供了强大的功能和灵活性。

第二种范例称为数据映射器,它需要使用三个不同的对象类:一个类表示对象级别的数据本身,一个类表示数据库表,以及一个“映射器”对象作为中间人,确保系统的对象和关系部分适当同步。

在 SQLAlchemy 项目中可以找到数据映射器范例的一个优秀且流行的示例。SQLAlchemy 已经存在多年,并且可以从您的 Python 程序中灵活地使用关系型数据库,而无需编写任何 SQL。

在本文中,我将介绍 SQLAlchemy,探索其许多选项和功能,并考虑如何在 Web 和其他应用程序中使用它。

连接到数据库

对于任何以前安装过 Python 包的人来说,安装 SQLAlchemy 应该很简单。您可以从 PyPi(Python 包索引)获取 SQLAlchemy,可以从 http://pypi.python.org 下载,也可以使用 easy_install 或 pip 程序检索并安装它。我可以使用以下命令安装它


pip install sqlalchemy

您可能需要以 root 身份安装 SQLAlchemy。或者,您可以使用流行的 virtualenv 包 for Python 将其安装到虚拟机中,这使您可以对 Python 环境进行非 root 控制和权限。

您还需要为您打算使用的数据库安装驱动程序。我最喜欢的关系型数据库是 PostgreSQL,我使用 psycopg Python 驱动程序,它也可以在 PyPi 上获得,并且(通过扩展)可以通过 pip 获得。

我应该注意到,虽然我知道 SQLAlchemy 可以与 Python 3 一起使用,但我现在所做的大部分工作仍然是在 Python 2 中,主要是因为那是我的客户正在使用的。因此,我的示例也将在 Python 2 中,尽管我相信它们在 Python 3 中也能工作,几乎不需要更改。

假设您有一个数据库表,People


CREATE TABLE People (id SERIAL PRIMARY KEY,
                     first_name TEXT,
                     last_name TEXT,
                     email TEXT,
                     birthday DATE);

我们还添加一些初始记录


INSERT INTO People (first_name, last_name, email, birthday)
    VALUES ('Reuven', 'Lerner', 'reuven@lerner.co.il', '1970-jul-14'),
           ('Foo', 'Bar', 'foobar@example.com', '1970-jan-1');

为了使用 SQLAlchemy 的 ORM 访问此表,您首先需要创建一个数据库会话对象,该对象本身必须使用“引擎”创建。每个数据库驱动程序都有自己的 URL 样式。在使用 psycopg2 驱动程序访问的 PostgreSQL 的情况下,您将使用如下内容


dburl = 'postgresql+psycopg2://reuven:reuven@localhost/atf'

此 URL 不仅指示数据库和驱动程序类型,还指示我的用户名和密码(均为“reuven”)、主机名(localhost)以及我将要访问的数据库名称(“atf”)。如果数据库在默认的 PostgreSQL 端口 5432 上不可用,您也可以在 URL 中指定它。

然后,您告诉 Python 基于此 URL 创建一个新引擎


from sqlalchemy import create_engine
engine = create_engine(dburl)

现在您已经定义了引擎,您可以基于此引擎创建一个会话。这样做需要两个步骤:首先,您为此引擎创建一个新的自定义 Session 类,然后您创建一个 Session 类的实例,您将使用该实例访问数据库


from sqlalchemy.orm import sessionmaker # import sessionmaker class
Session = sessionmaker(bind=engine)     # make custom session type
session = Session()                     # make instance of session

您现在已连接到数据库!但是,这还不够。如果您想将数据库表映射到一个或多个 Python 对象,您需要定义一个类。您可以通过定义一个普通的 Python 类来做到这一点,但有一些细微的变化

  • 该类必须从 Base 继承,Base 是 SQLAlchemy 提供的 declarative_base 函数返回的类。

  • 数据库列必须定义为类属性,作为 SQLAlchemy 提供的 Column 类的实例。

  • 您可以通过定义 __tablename__ 类级别属性将类与数据库表连接起来。

例如,以下 Python 类提供了到 People 数据库表的映射


from sqlalchemy import Column, Integer, String, DateTime
from sqlalchemy.ext.declarative import declarative_base
Base = declarative_base()
class Person(Base):

    __tablename__ = 'people'
    id = Column(Integer, primary_key=True)
    first_name = Column(String)
    last_name = Column(String)
    email = Column(String)
    birthday = Column(DateTime)

    def __init__(self, firstname, lastname, email, birthday):
        self.first_name = firstname
        self.last_name = lastname
        self.email = email

乍一看可能不明显,但此类实现了数据映射器设计模式。您定义的类属性描述了数据库表中的列,并且可以包含大量详细信息,包括索引、唯一性要求,甚至自定义完整性约束,例如 PostgreSQL 提供的那些。类本身是一个标准的 Python 类。

但在幕后,由于此类继承自 Base,您会获得许多其他属性,包括 __mapper__,它指示您的 Python 类将如何映射到数据库表。您可以通过在交互式 Python shell 中请求查看 Person.__mapper__ 的打印表示来看到这一点


Person.__mapper__
<Mapper at 0x10af5ef90; Person>

您现在有一个连接到数据库的会话和一个已在 Python 中描述的数据库表。您现在可以针对您的表执行查询


for p in session.query(Person):
    print p.first_name

这给出了以下结果


Reuven
Foo

换句话说,session.query 正在对数据库执行查询,而无需您指定 SQL。您还可以通过将 filter_by 方法链接到您的查询来限制您将获得的记录


for p in session.query(Person).filter_by(id=1):
    print p.first_name

这给出了以下结果


Reuven

请注意,filter_by 方法不是作用于 session.query 的结果。相反,它正在更改最终发送到数据库的 SQL。您可以通过在不执行查询或不将其放入迭代上下文的情况下分配打印给查询对象来看到这一点


print session.query(Person).filter_by(id=1)

SELECT people.id AS people_id,
       people.first_name AS people_first_name,
       people.last_name AS people_last_name,
       people.email AS people_email,
       people.birthday AS people_birthday
FROM people
WHERE people.id = :id_1

您还可以从这个查询中看到,SQLAlchemy 将参数绑定到查询内部的变量,而不是直接放置您的值。这不仅允许您稍后使用不同的变量值重新运行查询,而且还降低了您遭受 SQL 注入攻击的可能性,这种攻击仍然非常普遍。

您还可以对结果进行排序


for person in session.query(Person).order_by('first_name'):
    print person.first_name

Foo
Reuven

而且,您还可以执行您期望的所有基本“CRUD”活动。例如,您可以创建 Person 类的新实例,然后将其保存到数据库


p = Person('newfirst', 'newlast', 'new@example.com', '1-jan-2012');
session.add(p)
session.commit()

请注意,我如何在单个事务中处理多个插入(或其他操作),方法是在添加多个对象后才发出 session.commit()。类似地,我可以更新对象和数据库中的相应行


p.first_name = '!!!'
session.add(p)
session.commit()

我也可以删除对象


session.delete(p)
session.commit()
关系

如果 SQLAlchemy 只能做到这些,它仍然是一个不错的库,可以简化您的查询。但是 SQLAlchemy 的真正威力在于您定义表之间的关系时。例如,假设我有一个 Appointments 表,指示我何时与不同的人会面


CREATE TABLE Appointments (
  id SERIAL PRIMARY KEY,
  person_id INTEGER NOT NULL REFERENCES People,
  meeting_at TIMESTAMP NOT NULL,
  notes TEXT
);

我们还添加一些预约


INSERT INTO Appointments (person_id, meeting_at, notes)
    VALUES (2, '1-jan-2013', 'New Year meeting'),
           (2, '1-feb-2013', 'Monthly update');

现在我需要创建一个 Python 类来表示预约


class Appointment(Base):
    __tablename__ = 'appointments'
    id = Column(Integer, primary_key=True)
    person_id = Column(Integer)
    meeting_at = Column(DateTime)
    notes = Column(String)

现在,此类可以正常工作。但是,根据 Python,Person 类和 Appointment 类之间没有关系。要使它工作,您需要更改每个表定义。在 Appointment 的情况下,您需要指示 person_id 列不仅包含整数,而且它是一个外键,指向 People 表上的“id”列


person_id = Column(Integer, ForeignKey('people.id'))

在 Person 表上,您需要在类属性中添加一行,在描述所有列之后


appointments = relationship("Appointment", backref="person")

由于这两行代码,您可以在您的 Person 模型上获得一个“appointments”属性。但由于“backref”参数,您还可以在预约中获得一个“person”引用。这意味着您可以这样做


for a in session.query(Appointment):
    print a.person

for p in session.query(Person):
    print p.appointments

请注意,假设您每个人有多个预约,表示一对多关系。

但是,假设您希望在人和预约之间建立多对多关系,这样您可以一次与多个人会面,并且您可以与特定的人进行多次预约。为了做到这一点,您需要稍微修改您的数据库表和代码,添加第三个(关联)表。SQLAlchemy 使之变得容易。虽然我没有空间在这里展示它,但基本思想是您创建第三个表,并使用 relationship() 函数来指示类和连接表之间存在二级关系。

结论

SQLAlchemy 功能丰富。除了我在这里展示的入门示例之外,它还可以处理从连接到连接池,到动态计算的列值,再到基于现有数据库表创建 Python 类的所有事情。毫无疑问,它是一个强大的系统,我期望在我在其上工作的一些 Python 项目中使用它。

话虽如此,我发现 SQLAlchemy 对于新手来说有点不知所措。也许是因为我长期以来一直在 Ruby 中使用活动记录模型,该模型具有最少的配置和语法,但我发现 SQLAlchemy 的语法有点过于冗长。话又说回来,Python 长期以来一直偏爱显式,毫无疑问,SQLAlchemy 提供了一个清晰而显式的 ORM,没有太多魔法,并且每个函数调用和参数都有明显的含义。

另一个可能会让 SQLAlchemy 新手感到困惑的事情是,文档很完整,但不是很友好。一旦您开始使用该系统,我希望您(像我一样)能够理解文档并充分利用它。但我发现即使是教程文档也有点形式化,在继续实际代码之前试图告诉您太多内容。希望本文可以帮助更多人对 SQLAlchemy 产生兴趣。

总之,SQLAlchemy 是一个很棒的 Python 模块,它理应享有盛誉和广泛的流行度。如果您有兴趣从 Python 程序中使用数据库,那么您绝对应该看看 SQLAlchemy。

资源

SQLAlchemy 的主页是 https://sqlalchemy.org.cn,文档位于 https://docs.sqlalchemy.org.cn。Python 语言位于 https://pythonlang.cn。我建议您通读介绍部分,然后阅读 ORM 文档,而不是按顺序查看文档。

有很多关于 SQLAlchemy 的在线教程。我喜欢的两个免费公开的教程是 https://www.youtube.com/watch?v=399c-ycBvo4https://www.youtube.com/watch?v=PKAdehPHOMo

最后,Rick Copeland 的书,《Essential SQLAlchemy》,由 O'Reilly 于 2008 年出版,是一本很好的入门书,特别是如果您查看 ORM 部分。其余部分有点枯燥和技术性,即使示例写得很好。本书并非完全是最新的,并且其中有几个项目反映了它是在几年前出版的事实。尽管如此,拥有额外的参考资料可能非常方便,并且可以为某些不明显的特性提供示例。

Reuven M. Lerner,一位长期的 Web 开发人员,提供 Python、Git、PostgreSQL 和数据科学方面的培训和咨询服务。他撰写了两本编程电子书(Practice Makes Python 和 Practice Makes Regexp)并发布了面向程序员的免费每周新闻通讯,网址为 http://lerner.co.il/newsletter。Reuven 在 Twitter 上的用户名是 @reuvenmlerner,与妻子和三个孩子住在以色列的莫迪因。

加载 Disqus 评论