SQLObject

鸣谢

SQLObject 由 Ian Bicking (ianb@colorstudy.com) 和 贡献者 编写。网站是 sqlobject.org

许可证

此代码在 宽通用公共许可证 (LGPL) 下获得许可。

此程序的发布基于它将是有用的希望,但没有任何担保;甚至没有对适销性或特定用途适用性的默示担保。有关更多详细信息,请参阅 GNU 宽通用公共许可证。

简介

SQLObject 是 Python 编程语言的对象关系映射器。它允许你将 RDBMS 表行转换为 Python 对象,并操作这些对象以透明地操作数据库。

在使用 SQLObject 时,您将创建一个类定义,该定义描述对象如何转换为数据库表。SQLObject 将生成用于访问数据库的代码,并使用您的更改更新数据库。生成的界面看起来与任何其他界面类似,并且调用者不必了解数据库后端。

SQLObject 还包括一项新颖的功能,以避免以文本方式生成您的 SQL 查询。这也允许使用相同的查询语法将非 SQL 数据库与之一起使用。

要求

目前,SQLObject 通过 MySQLdb(又名 MySQL-python,对于 Python 3 称为 mysqlclient)、MySQL ConnectoroursqlPyMySQLmariadb connectorPyODBCPyPyODBC 支持 MySQLMariaDB。对于 PostgreSQL,建议使用 psycopg2;支持 PyGreSQLpy-postgresqlpg8000SQLite 有一个内置驱动程序或 PySQLite。通过 fdbkinterbasdb 支持 Firebird;支持 pyfirebirdsql,但存在问题。MAX DB(也称为 SAP DB)通过 sapdb 获得支持。通过 Sybase 支持 Sybase。MSSQL Server 通过 pymssql(+ FreeTDS)或 adodbapi(Win32)获得支持。PyODBCPyPyODBC 支持 MySQL、PostgreSQL 和 MSSQL,但存在问题(并非所有测试都通过)。

需要 Python 2.7 或 3.4+。

与其他数据库包装器的比较

Python 有几个对象关系映射器 (ORM)。我们老实说无法深入评论这些软件包的质量,但我们会尝试从透视的角度介绍 SQLObject。

对象具有内置的魔法 - 设置属性会产生副作用(它会更改数据库),并且定义类会产生副作用(通过使用元类)。属性通常是公开的,而不是标记为私有的,因为知道它们可以稍后变为动态的或只写的。

SQLObject 创建的对象与普通的 Python 对象感觉类似。附加到列的属性看起来与附加到文件或计算得出的属性没有区别。一个具体的目标是,你可以在不更改接口的情况下更改数据库,包括更改数据库的范围,使其作为存储机制或多或少更突出。

这与一些向数据库提供字典式接口的 ORM 形成对比(例如,PyDO)。字典接口将行与普通的 Python 对象区分开来。我们也不关心在属性看起来更自然的地方使用字符串 - 列的数量有限且预定义,就像属性一样。(注意:PyDO 的较新版本显然也允许属性访问)

据我所知,SQLObject 在使用元类来促进这种无缝集成方面是独一无二的。一些其他 ORM 使用代码生成来创建接口,在 CSV 或 XML 文件中表示模式(例如,MiddleKitWebware 的一部分)。通过使用元类,你可以在 Python 源代码中轻松地定义你的模式。无需代码生成,无需奇怪的工具,无需编译步骤。

SQLObject 提供了强大的数据库抽象,允许跨数据库兼容性(只要你不回避 SQLObject)。

SQLObject 具有联接、一对多和多对多,这是许多 ORM 所没有的。联接系统也旨在具有可扩展性。

你可以在数据库名称和 Python 属性和类名称之间进行映射;通常这两者不匹配,或者数据库样式不适合 Python 属性。通过这种方式,你的数据库模式不必考虑 SQLObject 而进行设计,并且生成的类不必继承数据库的命名方案。

使用 SQLObject:简介

让我们快速开始。我们通常只需从 sqlobject 类导入所有内容

>>> from sqlobject import *

声明连接

连接 URI 必须遵循标准 URI 语法

scheme://[user[:password]@]host[:port]/database[?parameters]

方案之一是 sqlitemysqlpostgresfirebirdinterbasemaxdbsapdbmssqlsybase

示例

mysql://user:password@host/database
mysql://host/database?debug=1
postgres://user@host/database?debug=&cache=
postgres:///full/path/to/socket/database
postgres://host:5432/database
sqlite:///full/path/to/database
sqlite:/C:/full/path/to/database
sqlite:/:memory:

参数为:debug(默认值:False)、debugOutput(默认值:False)、cache(默认值:True)、autoCommit(默认值:True)、debugThreading(默认值:False)、logger(默认值:None)、loglevel(默认值:None)、schema(默认值:None)。

如果你想在连接 URI 中传递 True 值 - 传递几乎任何非空字符串,尤其是 yestrueon1;空字符串或 nofalseoff0 表示 False。

还有特定于连接的参数,它们列在相应的章节中。

让我们首先建立一个连接

>>> import os
>>> db_filename = os.path.abspath('data.db')
>>> connection_string = 'sqlite:' + db_filename
>>> connection = connectionForURI(connection_string)
>>> sqlhub.processConnection = connection

sqlhub.processConnection 分配意味着所有类在默认情况下都将使用我们刚刚建立的这个连接。

声明类

我们将开发一个简单的类似于地址簿的数据库。我们可以自己创建表,并让 SQLObject 访问这些表,但我们让 SQLObject 来做这项工作。首先,类

>>> class Person(SQLObject):
...
...     firstName = StringCol()
...     middleInitial = StringCol(length=1, default=None)
...     lastName = StringCol()

许多基本表模式不会比这更复杂。firstNamemiddleInitiallastName 都是数据库中的列。此类定义隐含的通用模式是

CREATE TABLE person (
    id INT PRIMARY KEY AUTO_INCREMENT,
    first_name TEXT,
    middle_initial CHAR(1),
    last_name TEXT
);

这是针对 SQLite 或 MySQL 的。其他数据库的模式看起来略有不同(尤其是 id 列)。您会注意到名称已从 mixedCase 更改为 underscore_separated - 这是通过 样式对象 完成的。有许多方法可以处理不符合约定的名称(请参阅 不规则命名)。

现在,我们将在数据库中创建表

>>> Person.createTable()
[]

我们可以通过使用 StringCol 以外的内容或使用不同的参数来更改不同列的类型。有关此内容的更多信息,请参阅 列类型

您会注意到 id 列未在类定义中给出,它隐含在其中。对于 MySQL 数据库,它应定义为 INT PRIMARY KEY AUTO_INCREMENT,在 Postgres 中为 SERIAL PRIMARY KEY,在 SQLite 中为 INTEGER PRIMARY KEY AUTOINCREMENT,对于其他后端则相应地定义。您不能使用没有单个主键的 SQLObject 表,并且您必须将该键视为不可变的(否则您会极大地混淆 SQLObject)。

您可以在数据库中 覆盖 id 名称,但它在 Python 中始终被称为 .id

使用类

现在您有了类,您将如何使用它?我们将考虑上面定义的类。

要创建新对象(和行),请使用类实例化,如下所示

>>> Person(firstName="John", lastName="Doe")
<Person 1 firstName='John' middleInitial=None lastName='Doe'>

注意

在 SQLObject 中,NULL/None 表示默认值。NULL 很有趣;在不同的上下文中和对于不同的人来说,它的含义大不相同。有时它表示“默认值”,有时表示“不适用”,有时表示“未知”。如果您想要默认值(无论是否为 NULL),您必须始终在类定义中明确指定。

还要注意,SQLObject 默认值与数据库默认值不同(SQLObject 永远不会使用数据库默认值)。

如果您省略了 firstNamelastName,您将收到一个错误,因为未为这些列提供默认值(middleInitial 有一个默认值,因此它将设置为 NULL,即 None 的数据库等效值)。

您可以使用类方法 .get() 来获取已存在的实例

>>> Person.get(1)
<Person 1 firstName='John' middleInitial=None lastName='Doe'>

当您创建一个对象时,它会立即插入到数据库中。SQLObject 使用数据库作为即时存储,这与您必须将对象明确保存到数据库中的其他一些系统不同。

以下是使用该类的较长示例

>>> p = Person.get(1)
>>> p
<Person 1 firstName='John' middleInitial=None lastName='Doe'>
>>> p.firstName
'John'
>>> p.middleInitial = 'Q'
>>> p.middleInitial
'Q'
>>> p2 = Person.get(1)
>>> p2
<Person 1 firstName='John' middleInitial='Q' lastName='Doe'>
>>> p is p2
True

列的访问方式与属性类似(这使用了 Python 的 property 特性,以便检索和设置这些属性会执行代码)。还要注意,对象是唯一的 - 在任何时候,内存中通常只有一个特定 id 的 Person 实例。如果您多次按特定 ID 请求一个人,您将获得相同的实例。这样,如果您有多个线程访问相同的数据,您可以确保一定程度的一致性(当然,跨进程无法共享实例)。如果您使用的是 事务,则情况并非如此,事务必然是隔离的。

为了了解表面之下发生的情况,我们将提供与发送的 SQL 相同的操作,以及一些注释

>>> # This will make SQLObject print out the SQL it executes:
>>> Person._connection.debug = True
>>> p = Person(firstName='Bob', lastName='Hope')
 1/QueryIns:  INSERT INTO person (first_name, middle_initial, last_name) VALUES ('Bob', NULL, 'Hope')
 1/QueryR  :  INSERT INTO person (first_name, middle_initial, last_name) VALUES ('Bob', NULL, 'Hope')
 1/COMMIT  :  auto
 1/QueryOne:  SELECT first_name, middle_initial, last_name FROM person WHERE ((person.id) = (2))
 1/QueryR  :  SELECT first_name, middle_initial, last_name FROM person WHERE ((person.id) = (2))
 1/COMMIT  :  auto
>>> p
<Person 2 firstName='Bob' middleInitial=None lastName='Hope'>
>>> p.middleInitial = 'Q'
 1/Query   :  UPDATE person SET middle_initial = ('Q') WHERE id = (2)
 1/QueryR  :  UPDATE person SET middle_initial = ('Q') WHERE id = (2)
 1/COMMIT  :  auto
>>> p2 = Person.get(1)
>>> # Note: no database access, since we're just grabbing the same
>>> # instance we already had.

希望您看到发送的 SQL 非常清晰且可预测。要查看正在发送的 SQL,请将 ?debug=true 添加到您的连接 URI,或在连接上设置 debug 属性,所有 SQL 都将打印到控制台。这可以令人放心,我们鼓励您尝试一下。

作为一个小优化,您可以使用 set 方法分配多个属性,而不是单独分配每个属性,如下所示

>>> p.set(firstName='Robert', lastName='Hope Jr.')

这将仅发送一个 UPDATE 语句。您还可以对非数据库属性使用 set(没有好处,但它有助于隐藏数据库属性和非数据库属性之间的差异)。

选择多个对象

虽然 SQLObject 中并未揭示您可以使用关系数据库执行的所有类型的联接的全部功能,但可以使用简单的 SELECT

select 是一个类方法,您可以像这样调用它(使用生成的 SQL)

>>> Person._connection.debug = True
>>> peeps = Person.select(Person.q.firstName=="John")
>>> list(peeps)
 1/Select  :  SELECT person.id, person.first_name, person.middle_initial, person.last_name FROM person WHERE ((person.first_name) = ('John'))
 1/QueryR  :  SELECT person.id, person.first_name, person.middle_initial, person.last_name FROM person WHERE ((person.first_name) = ('John'))
 1/COMMIT  :  auto
[<Person 1 firstName='John' middleInitial='Q' lastName='Doe'>]

此示例返回所有名字为 John 的人。

查询可以更复杂

>>> peeps = Person.select(
...         OR(Person.q.firstName == "John",
...            LIKE(Person.q.lastName, "%Hope%")))
>>> list(peeps)
 1/Select  :  SELECT person.id, person.first_name, person.middle_initial, person.last_name FROM person WHERE (((person.first_name) = ('John')) OR (person.last_name LIKE ('%Hope%')))
 1/QueryR  :  SELECT person.id, person.first_name, person.middle_initial, person.last_name FROM person WHERE (((person.first_name) = ('John')) OR (person.last_name LIKE ('%Hope%')))
 1/COMMIT  :  auto
[<Person 1 firstName='John' middleInitial='Q' lastName='Doe'>, <Person 2 firstName='Robert' middleInitial='Q' lastName='Hope Jr.'>]

您会注意到类有一个属性 q,它可以访问用于构造查询子句的特殊对象。 q 下的所有属性都引用列名,如果您使用这些属性构造逻辑语句,它将为您提供该语句的 SQL。您还可以更手动地创建 SQL

>>> Person._connection.debug = False  # Need for doctests
>>> peeps = Person.select("""person.first_name = 'John' AND
...                          person.last_name LIKE 'D%'""")

如果您手动创建 SQL(如果您使用 q,则会自动引用),则应使用 MyClass.sqlrepr 引用您使用的任何值。

您可以在 select 语句中使用关键字参数 orderBy 来创建 ORDER BYorderBy 采用一个字符串,该字符串应该是列的数据库名称,或采用 Person.q.firstName 形式的列。您可以使用 "-colname"DESC(Person.q.firstName) 来指定降序(这会转换为 DESC,因此它也适用于非数字类型),或调用 MyClass.select().reversed()。orderBy 还可以采用相同格式的列列表:["-weight", "name"]

您可以使用 sqlmeta 类变量 defaultOrder 为所有 select 提供默认排序。要在使用 defaultOrder 时获取无序结果,请使用 orderBy=None

Select 结果是惰性求值的生成器。因此,仅当您迭代 select 结果或使用 list() 强制执行结果时,才会执行 SQL。当您迭代 select 结果时,一次获取一行。通过这种方式,您可以在不将整个结果集保存在内存中的情况下迭代大型结果。您还可以执行诸如 .reversed() 之类操作,而无需获取和反转整个结果 - 相反,SQLObject 可以更改发送的 SQL,以便您获得等效的结果。

您还可以对 select 结果进行切片。这会修改 SQL 查询,因此 peeps[:10] 将导致在 SQL 查询的末尾添加 LIMIT 10。如果无法在 SQL 中执行切片(例如 peeps[:-10]),则会执行 select,并在结果列表上执行切片。通常,只有在您使用负索引时才会发生这种情况。

在某些情况下,您可能会在 select 结果中多次获得一个对象,例如在某些联接中。如果您不希望这样,则可以添加关键字参数 MyClass.select(..., distinct=True),这会导致 SELECT DISTINCT 调用。

您可以通过对结果对象调用 count 来获取结果的长度,而不获取所有结果,例如 MyClass.select().count()。使用 COUNT(*) 查询 - 不会从数据库中获取实际对象。结合切片,这使得批处理查询易于编写

start = 20 size = 10 query = Table.select() results = query[start:start+size] total = query.count() print “Showing page %i of %i” % (start/size + 1, total/size + 1)

注意

考虑这种批处理的效率时,有几个因素,并且这在很大程度上取决于批处理的使用方式。考虑一个 Web 应用程序,您平均显示 100 个结果,一次显示 10 个,并且结果按添加到数据库的日期排序。虽然切片将阻止数据库返回所有结果(从而节省一些通信时间),但数据库仍必须扫描整个结果集以对项目进行排序(因此它知道前十个项目是什么),并且根据您的查询可能需要扫描整个表(取决于您对索引的使用)。在这种情况下,索引可能是提高性能的最重要方式,您可能会发现缓存比切片更有效。

在这种情况下,缓存意味着检索完整结果。您可以使用 list(MyClass.select(...)) 来执行此操作。当用户逐页查看结果页面时,您可以将这些结果保存一段时间。这意味着搜索结果中的第一页将稍微昂贵一些,但所有后续页面都将非常便宜。

有关查询中 where 子句的更多信息,请参阅 SQLBuilder 文档

q-magic

请注意在上述示例中对 q 属性的使用。 q 是返回特殊对象以构建 SQL 表达式的对象。对 q-magic 返回的对象执行的操作不会立即求值,而是以类似于符号代数的方式存储;整个表达式通过构建一个字符串来求值,然后将该字符串发送到后端。

例如,对于代码

>>> peeps = Person.select(Person.q.firstName=="John")

SQLObject 不求值 firstName,而是存储表达式

Person.q.firstName==”John”

稍后,SQLObject 将其转换为字符串 first_name = 'John' 并将该字符串传递给后端。

selectBy 方法

.select 的替代方法是 .selectBy。它的工作原理如下

>>> peeps = Person.selectBy(firstName="John", lastName="Doe")

每个关键字参数都是一列,并且所有关键字参数都通过 AND 连接在一起。返回值是 SelectResults,因此您可以对其进行切片、计数、排序等。

延迟更新

默认情况下,SQLObject 会为设置的每个属性或每次调用 .set() 时向数据库发送 UPDATE。如果您想避免进行如此多的更新,请将 lazyUpdate = True 添加到您的类 sqlmeta 定义 中。

然后,仅当您调用 inst.syncUpdate()inst.sync() 时,更新才会写入数据库: .sync() 还会从数据库中重新获取数据,而 .syncUpdate() 不会。

启用后,实例将具有属性 .sqlmeta.dirty,该属性指示是否存在待处理的更新。插入仍然会立即完成;目前无法进行延迟插入。

一对多关系

没有地址的地址簿毫无用处。

首先,让我们定义新的地址表。当然,人们可以有多个地址

>>> class Address(SQLObject):
...
...     street = StringCol()
...     city = StringCol()
...     state = StringCol(length=2)
...     zip = StringCol(length=9)
...     person = ForeignKey('Person')
>>> Address.createTable()
[]

请注意列 person = ForeignKey("Person")。这是对 Person 对象的引用。我们通过名称(使用字符串)引用其他类。在地址表中,将有一个 person_id 列,类型为 INT,指向 person 表。

注意

SQLObject 使用字符串引用其他类的原因是,其他类通常还不存在。Python 中的类是创建的,而不是声明的;因此,导入模块时会执行命令。 class 只是另一个命令;它创建一个类并将其分配给您给定的名称。

如果类 A 引用类 B,但类 B 在模块中定义在 A 的下面,那么当创建类 A(包括创建其所有列属性)时,类 B 根本不存在。通过按名称引用类,我们可以等到所有必需的类都存在后,再创建类之间的链接。

我们希望有一个属性,用于提供一个人的地址。在类定义中,我们会执行以下操作

class Person(SQLObject):
    ...
    addresses = MultipleJoin('Address')

但我们已经有了类。我们可以直接将此内容添加到类中

>>> Person.sqlmeta.addJoin(MultipleJoin('Address',
...                        joinMethodName='addresses'))

注意

在几乎所有情况下,您都可以在创建 SQLObject 类后对其进行修改。在类定义中具有包含 *Col 对象的属性等效于调用某些类方法(如 addColumn())。

现在,我们可以使用 Person.addresses 获取反向引用,它会返回一个列表。示例

>>> p.addresses
[]
>>> Address(street='123 W Main St', city='Smallsville',
...         state='MN', zip='55407', person=p)
<Address 1 ...>
>>> p.addresses
[<Address 1 ...>]

注意

MultipleJoin 和 RelatedJoin 都返回结果列表。通常最好获取 SelectResults 对象,在这种情况下,您应使用 SQLMultipleJoin 和 SQLRelatedJoin。这些联接的声明与上面相同,但返回的迭代器具有许多其他有用的方法。

多对多关系

对于此示例,我们将拥有用户和角色对象。这两个对象具有多对多关系,它用 RelatedJoin 表示。

>>> class User(SQLObject):
...
...     class sqlmeta:
...         # user is a reserved word in some databases, so we won't
...         # use that for the table name:
...         table = "user_table"
...
...     username = StringCol(alternateID=True, length=20)
...     # We'd probably define more attributes, but we'll leave
...     # that exercise to the reader...
...
...     roles = RelatedJoin('Role')
>>> class Role(SQLObject):
...
...     name = StringCol(alternateID=True, length=20)
...
...     users = RelatedJoin('User')
>>> User.createTable()
[]
>>> Role.createTable()
[]

注意

sqlmeta 类用于存储不同类型的元数据(并覆盖该元数据,如表)。这是 SQLObject 0.7 中的新增内容。有关其工作方式和具有特殊含义的属性的详细信息,请参阅 sqlmeta 类 部分。

以及用法

>>> bob = User(username='bob')
>>> tim = User(username='tim')
>>> jay = User(username='jay')
>>> admin = Role(name='admin')
>>> editor = Role(name='editor')
>>> bob.addRole(admin)
>>> bob.addRole(editor)
>>> tim.addRole(editor)
>>> bob.roles
[<Role 1 name='admin'>, <Role 2 name='editor'>]
>>> tim.roles
[<Role 2 name='editor'>]
>>> jay.roles
[]
>>> admin.users
[<User 1 username='bob'>]
>>> editor.users
[<User 1 username='bob'>, <User 2 username='tim'>]

在此过程中,会创建一个中间表 role_user,它引用其他两个类。此表永远不会作为类公开,其行没有等效的 Python 对象 - 这隐藏了多对多关系的一些麻烦。

顺便说一句,如果您想创建自己的中间表(可能带有其他列),请注意标准 SQLObject 方法 add/removesomething 可能无法按预期工作。假设您使用正确的 joinColumn 和 otherColumn 参数提供联接,请注意,无法通过此类方法插入额外数据,它们也不会设置任何默认值。

我们举个例子:在前面的用户/角色系统中,您正在创建一个 UserRole 中间表,其中两列包含 MTM 关系的外键,以及一个默认值为 datetime.datetime.now 的附加 DateTimeCol:当使用 addRole 方法添加角色时,该列将保持为空。如果您想直接从中间表获取行列表,请向用户或角色类添加 MultipleJoin。

您可能会注意到,这些列具有额外的关键字参数 alternateID。如果您使用 alternateID=True,这意味着该列唯一标识行 - 如用户名唯一标识用户。此标识符是主键(id)之外的标识符,主键始终存在。

注意

SQLObject 有一个严格的要求,即主键必须唯一且不可变。您无法通过 SQLObject 更改主键,如果您通过其他机制更改它,则可能导致任何正在运行的 SQLObject 程序(以及您的数据)出现不一致。因此,建议使用无意义的整数 ID - 如将来可能更改的用户名可以唯一标识行,但将来可能会更改它。只要它不用于引用行,将来更改它也是安全的

alternateID 列创建一个类方法,如名为 username 的列的 byUsername(或者您可以使用 alternateMethodName 关键字参数来覆盖它)。它的用法

>>> User.byUsername('bob')
<User 1 username='bob'>
>>> Role.byName('admin')
<Role 1 name='admin'>

使用关系选择对象

选择表达式可以引用多个类,如

>>> Person._connection.debug = False # Needed for doctests
>>> peeps = Person.select(
...         AND(Address.q.personID == Person.q.id,
...             Address.q.zip.startswith('504')))
>>> list(peeps)
[]
>>> peeps = Person.select(
...         AND(Address.q.personID == Person.q.id,
...             Address.q.zip.startswith('554')))
>>> list(peeps)
[<Person 2 firstName='Robert' middleInitial='Q' lastName='Hope Jr.'>]

在构造复杂查询时,也可以使用 q 属性,如

>>> Person._connection.debug = False  # Needed for doctests
>>> peeps = Person.select("""address.person_id = person.id AND
...                          address.zip LIKE '504%'""",
...                       clauseTables=['address'])

请注意,如果你使用除了你正在从中选择的表之外的表,你必须使用 clauseTables。如果你使用 q 属性,SQLObject 将自动找出你可能已经使用了哪些额外的类。

类 sqlmeta

这个新类从 SQLObject 0.7 开始可用,它允许以更清晰的方式指定元数据,而不会使用更多属性污染类命名空间。

此类中可以使用一些特殊属性,这些属性将改变包含它的类的行为。这些值是

table:
数据库中的表名。如果未给出显式名称,则此名称派生自 style 和类名。如果你没有给出名称并且没有定义替代的 style,那么将执行标准的 MixedCasemixed_case 转换。
idName:
数据库中主键列的名称。如果未给出显式名称,则此名称派生自 style。默认名称为 id
idType:
设置 ID 时强制转换/规范化 ID 的类型。必须是 intstr。默认情况下,此值为 int(所有 ID 都规范化为整数)。
idSize:
这将设置 MySQL 和 PostgreSQL 中整型列 id 的大小。允许的值有 'TINY''SMALL''MEDIUM''BIG'None;默认值为 None。对于映射到 smallserial/serial/bigserial 的 Postgres。对于其他后端,目前忽略它。
style:
样式对象 - 此对象允许你使用其他算法在 Python 属性和类名以及数据库的列名和表名之间进行转换。有关更多信息,请参见 更改命名样式。它是 IStyle 接口的一个实例。
lazyUpdate:
布尔值(默认为 false)。如果为 true,则在实例上设置属性(或使用 inst.set(.) 将不会立即发送 UPDATE 查询(你必须先调用 inst.syncUpdates()inst.sync())。
defaultOrder:
在选择对象且未给出明确顺序时,此属性指示默认排序。它就像此值传递给 .select() 和相关方法;有关详细信息,请参阅这些方法的文档。
cacheValues:

布尔值(默认值为 true)。如果为 true,则只要实例保持(且未调用 inst.expire()),行中的值就会被缓存。

如果设置为 False,则来自数据库的属性值将不会被缓存。因此,每次访问对象中的属性时,都会对数据库进行值查询,即,将发出 SELECT。如果您想处理来自多个进程的并发数据库访问,那么这可能是实现此目的的方法。

registry:
由于 SQLObject 使用字符串来关联类,且这些字符串不区分模块名称,因此如果您将不同的系统放在一起,就会发生名称冲突。此字符串值用作类的命名空间。
fromDatabase:
布尔值(默认值为 false)。如果为 true,则在创建类时,将对数据库进行表列查询,并将自动添加任何缺失的列(可能为所有列)。请注意,并非所有连接都完全实现了数据库自省。
dbEncoding:
UnicodeColcolumn.dbEncodingNone 时查找 sqlmeta.dbEncoding(如果 sqlmeta.dbEncodingNoneUnicodeCol 查找 connection.dbEncoding,如果在任何地方未定义 dbEncoding,则默认为 "utf-8")。对于 Python 3,连接必须有一个编码 - 请勿使用不同的编码定义不同的列,这是未实现的。

以下属性提供自省,但不得直接设置 - 请参阅 运行时列和连接更改 以动态修改这些类元素。

columns:
{columnName: anSOColInstance} 的字典。您可以通过此只读属性获取有关列的信息。
columnList:
columns 中值的列表。有时需要列的稳定有序版本;为此使用此版本。
columnDefinitions:
类似于 columns 的字典,但包含原始列定义(这些定义不是特定于类的,且没有逻辑)。
joins:
此类的所有 Join 对象的列表。
indexes:
此类的所有索引的列表。
createSQL:
在创建表后运行的 SQL 查询。createSQL 可以是包含单个 SQL 命令的字符串、SQL 命令列表或字典,其中键是 dbNames,值是单个 SQL 命令字符串或 SQL 命令列表。这通常用于 ALTER TABLE 命令。

还有一个实例属性

expired:
布尔值。如果为 true,则下次访问此对象的列属性时,将运行查询。

在 SQLObject 的早期版本中,这些属性直接在将数据库数据映射到 Python 的类中定义,并且它们都以下划线为前缀,现在建议您将代码更改为这种新样式。旧方法已在 SQLObject 0.8 中删除。

请注意:使用 InheritedSQLObject 时,sqlmeta 属性不会被继承,例如,你无法通过 sqlmeta.columns 字典访问父类的列对象。

使用 sqlmeta

要使用 sqlmeta,你应该编写类似于此示例的代码

class MyClass(SQLObject):

    class sqlmeta:
        lazyUpdate = True
        cacheValues = False

    columnA = StringCol()
    columnB = IntCol()

    def _set_attr1(self, value):
        # do something with value

    def _get_attr1(self):
        # do something to retrieve value

上述定义正在创建一个表 my_class(如果你更改所使用的 style,名称可能不同),其中包含两个名为 columnA 和 columnB 的列。还有一个第三个字段,可以使用 MyClass.attr1 访问。sqlmeta 类正在更改 MyClass 的行为,以便它执行延迟更新(你必须调用 sync() 方法将更新写入数据库),并且它还指示 MyClass 不会有任何缓存,因此每次你请求一些信息时,它都会从数据库中检索。

j-magic

有一个类似于 q 的魔术属性 j,它具有 ForeignKey 和 SQLMultipleJoin/SQLRelatedJoin 的属性,为 SQLBuilder 连接表达式提供了遍历给定关系的简写。例如,对于 ForeignKey AClass.j.someB 等效于 (AClass.q.someBID==BClass.q.id),而 BClass.j.someAs 对于匹配的 SQLMultipleJoin 也是如此。

SQLObject 类

有一个特殊属性 - _connection。它是为表定义的连接。

_connection:

要使用的连接对象,来自 DBConnection。你还可以设置封闭模块中的变量 __connection__,它将被选取(确保在你的类之前定义 __connection__)。你还可以按实例创建时传递连接对象,如 transactions 中所述。

如果你已定义 sqlhub.processConnection,则可以从你的类中省略此属性,而将使用 sqlhub。如果你有几个类使用相同的连接,除了节省大量输入之外,这可能是一个优势。

自定义对象

虽然我们在示例中没有这样做,但你可以在类定义中包含你自己的方法。编写你自己的方法应该足够明显(就像在任何其他类中一样),但还有一些其他细节需要注意。

初始化对象

SQLObject 实例有两种产生方式:它们可以从数据库中获取,也可以插入到数据库中。在这两种情况下,都会创建一个新的 Python 对象。这使得 __init__ 的作用有点混乱。

通常情况下,您不应该触碰 __init__。相反,请使用 _init 方法,该方法在获取或插入对象后调用。此方法具有签名 _init(self, id, connection=None, selectResults=None),但您可能只想使用 _init(self, *args, **kw)注意:如果您覆盖该方法,请不要忘记调用 SQLObject._init(self, *args, **kw)

添加魔术属性(属性)

您可以在此类中使用所有常规技术来定义方法,包括 classmethodstaticmethodproperty,但您还可以使用快捷方式。如果您有一个名称以 _set__get__del__doc_ 开头的,它将用于创建属性。因此,例如,假设您将图像存储在 /var/people/images 目录中的人员 ID 下

class Person(SQLObject):
    # ...

    def imageFilename(self):
        return 'images/person-%s.jpg' % self.id

    def _get_image(self):
        if not os.path.exists(self.imageFilename()):
            return None
        f = open(self.imageFilename())
        v = f.read()
        f.close()
        return v

    def _set_image(self, value):
        # assume we get a string for the image
        f = open(self.imageFilename(), 'w')
        f.write(value)
        f.close()

    def _del_image(self, value):
        # We usually wouldn't include a method like this, but for
        # instructional purposes...
        os.unlink(self.imageFilename())

稍后,您可以像属性一样使用 .image 属性,并且通过调用这些方法,这些更改将反映在文件系统中。对于最好保存在文件中而不是数据库中的信息(例如图像等大型不透明数据),这是一个很好的技术。

您还可以将 image 关键字参数传递给构造函数或 set 方法,例如 Person(..., image=imageText)

所有方法(_get__set_ 等)都是可选的 - 您可以使用其中任何一个而无需使用其他方法。因此,您只需定义一个 _get_attr 方法,以便 attr 为只读。

覆盖列属性

如果您想覆盖数据库列属性的行为,则会稍微复杂一些。例如,想象一下,每当某人的姓名发生更改时,您都希望运行一些特殊代码。在许多系统中,您会执行一些自定义代码,然后调用超类的代码。但超类(SQLObject)不知道您子类中的列。对于属性来说,情况更糟。

SQLObject 为你的每一列创建诸如 _set_lastName 的方法,但同样,你无法使用它,因为没有可引用的超类(并且你无法编写 SQLObject._set_lastName(...),因为 SQLObject 类不知道你的类的列)。你想自己覆盖 _set_lastName 方法。

为了解决此问题,SQLObject 为每个 getter 和 setter 创建两个方法,例如:_set_lastName_SO_set_lastName。因此,要拦截对 lastName 的所有更改

class Person(SQLObject):
    lastName = StringCol()
    firstName = StringCol()

    def _set_lastName(self, value):
        self.notifyLastNameChange(value)
        self._SO_set_lastName(value)

或者,你可能希望将电话号码限制为实际数字,并具有适当的长度,并使格式美观

import re

class PhoneNumber(SQLObject):
    phoneNumber = StringCol(length=30)

    _garbageCharactersRE = re.compile(r'[\-\.\(\) ]')
    _phoneNumberRE = re.compile(r'^[0-9]+$')
    def _set_phoneNumber(self, value):
        value = self._garbageCharactersRE.sub('', value)
        if not len(value) >= 10:
            raise ValueError(
                'Phone numbers must be at least 10 digits long')
        if not self._phoneNumberRE.match(value):
            raise ValueError, 'Phone numbers can contain only digits'
        self._SO_set_phoneNumber(value)

    def _get_phoneNumber(self):
        value = self._SO_get_phoneNumber()
        number = '(%s) %s-%s' % (value[0:3], value[3:6], value[6:10])
        if len(value) > 10:
            number += ' ext.%s' % value[10:]
        return number

注意

修改在属性中设置的数据时,你应该小心一点。通常,使用你的类的人会期望他们将属性设置为的值与他们取回的值相同。在此示例中,我们在将其放入数据库之前删除了一些字符,并在输出时重新格式化了它。与属性访问相比,方法的一个优点是程序员更有可能期望这种断开连接。

还要注意,虽然这些转换将在获取和设置列时发生,但在查询中不会发生转换。因此,如果你将值从“Pythonic”表示转换为“SQLish”表示,则你的查询(使用 .select().selectBy() 时)必须以 SQL/数据库表示的形式(因为这些命令生成在数据库上运行的 SQL)。

未定义的属性

还有一件事值得一提,因为你在输入错误时可能会得到一些奇怪的结果。设置先前未定义的属性时,SQLObject 永远不会抱怨或引发任何错误;它只会设置它,而不会对数据库进行任何更改,即:它将像你在任何 Python 类上设置的任何其他属性一样工作,它会“忘记”它是一个 SQLObject 类。

这有时可能是一个问题:如果你有一个“name”属性,并且你在设置时写了一次 a.namme="Victor",则在设置时,你不会收到任何错误、警告或任何内容,并且你可能会发疯,因为你不知道为什么没有在你的数据库中设置该值。

参考

上面的说明应该足以让你入门,并且对许多情况有用。现在,我们将展示如何更完整地指定类。

Col 类:指定列

列的列表是 Col 对象的列表。这些对象本身没有功能,但可以用来指定列。

dbName:
这是数据库中列的名称。如果您不提供名称,您的 Pythonic 名称将从混合大小写转换为下划线分隔。
default:
此列的默认值。在创建新行时使用。如果您提供一个可调用对象或函数,将调用该函数,并使用返回值。因此,您可以提供 DateTimeCol.now 以使默认值变为当前时间。或者,您可以使用 sqlbuilder.func.NOW() 使数据库在内部使用 NOW() 函数。如果您不提供默认值,则在调用 new 时未指定此列,将出现异常。
defaultSQL:
DEFAULT SQL 属性。
alternateID:

此布尔值(默认为 False)表示该列是否可用作该字段的 ID(例如,用户名),尽管它不是主键。如果是,将添加一个类方法,例如 byUsername,它将返回该对象。如果您不喜欢 by* 名称,请使用 alternateMethodName(例如,alternateMethodName="username")。

该列应在表模式中声明为 UNIQUE

unique:
如果为 true,当 SQLObject 创建表时,它将声明此列为 UNIQUE
notNone:
如果为 true,则不允许此列为 None/NULL。如果您使用 SQLObject 创建表,这将很有用。
sqlType:
此列的 SQL 类型(如 INTBOOLEAN 等)。您可以为此使用类(在下面定义),但如果这些类不起作用,有时最简单的方法就是使用 sqlType。仅当 SQLObject 创建表时才需要。
validator:
formencode 类似的 验证器。长话短说,这是一个提供 to_python()from_python() 的对象,用于验证转换从数据库读写时获得的值。有关更多详细信息,请参阅 formencode 验证器 文档。此验证器将附加到列验证器列表的末尾。如果列具有验证器列表,则其 from_python() 方法将从列表的开头运行到末尾; to_python() 则以相反的顺序运行。也就是说,此验证器的 from_python() 方法将在列表中的所有验证器之后最后调用; to_python() 则首先调用。
validator2:
另一个验证器。它插入到验证器列表的开头,即其 from_python() 方法首先调用; to_python() 则最后调用。

列类型

当列是引用另一个表/类的时,应使用 ForeignKey 类,而不是 Col。通常像 ForeignKey('Role') 这样使用,在此实例中,创建对表 Role 的引用。这在很大程度上等同于 Col(foreignKey='Role', sqlType='INT')。通常会创建两个属性, role,它返回 Role 实例,以及 roleID,它返回相关角色的整数 ID。

还有 Col 的一些其他子类。当 SQLObject 创建表时,这些子类用于指示不同类型的列。

BLOBCol:
用于二进制数据的列。目前仅适用于 MySQL、PostgreSQL 和 SQLite 后端。
BoolCol:
将在 Postgres 中创建一个 BOOLEAN 列,或在其他数据库中创建一个 INT 列。它还将根据数据库后端将值转换为 "t"/"f"0/1
CurrencyCol:
等同于 DecimalCol(size=10, precision=2)。警告:由于 DecimalCol 可能不会返回精确的数字,因此此列可能具有相同行为。请阅读 DecimalCol 警告。
DateTimeCol:
日期和时间(通常作为 datetime 或 mxDateTime 对象返回)。
DateCol:
日期(通常返回为 datetime 或 mxDateTime 对象)。
TimeCol:
时间(通常返回为 datetime 或 mxDateTime 对象)。
TimestampCol:
支持 MySQL TIMESTAMP 类型。
DecimalCol:
十进制精确数字。使用关键字参数 size 表示存储的数字数量,使用 precision 表示小数点后的数字数量。警告:可能发生 DecimalCol 值虽然正确存储在数据库中,但返回为浮点数而不是小数的情况。例如,由于 类型关联,SQLite 将小数存储为整数或浮点数(NUMERIC 存储类)。您应该使用数据库适配器进行测试,并且在导入 SQLObject 之前尝试导入 Decimal 类型和数据库适配器。
DecimalStringCol:
类似于 DecimalCol,但将数据存储为字符串,以解决某些驱动程序中的问题以及 SQLite 中的类型关联问题。由于它将数据存储为字符串,因此该列无法在 SQL 表达式(column1 + column2)中使用,并且可能在 ORDER BY 中出现问题。
EnumCol:

几个字符串值之一 - 使用 enumValues 关键字参数将可能的字符串作为列表提供。MySQL 具有本机 ENUM 类型,但也可以与其他数据库配合使用(存储效率不会那么高)。

对于 PostgreSQL,EnumCol 使用检查约束实现。由于 PostgreSQL 处理涉及 NULL 的检查约束的方式,将 None 指定为 EnumCol 的成员实际上意味着在 SQL 级别,将忽略检查约束(有关详细信息,请参阅 http://archives.postgresql.org/pgsql-sql/2004-12/msg00065.php)。

SetCol:
支持 MySQL SET 类型。
FloatCol:
浮点数。
ForeignKey:
指向另一个表/类的键。使用类似 user = ForeignKey('User') 的方式。它可以使用关键字参数 cascade 检查引用完整性,有关详细信息,请参阅 ForeignKey
IntCol:
整数。
JsonbCol:
用于 jsonb 对象的列。仅在 Postgres 上受支持。可以使用 json.dumps 序列化的任何 Python 对象都可以存储。
JSONCol:
一个通用的 json 列,它使用 json.dumps/loads 将简单的 Python 对象(None、bool、int、float、long、dict、list、str/unicode)转换为 JSON/从 JSON 转换。StringCol 的子类。需要后端的 VARCHAR/TEXT 列,不适用于 JSON 列。
PickleCol:
BLOBCol 的扩展;此列可以存储/检索任何 Python 对象;它实际上将对象从/转换为字符串并存储/检索字符串。可以获取和设置列的值,但不能搜索(在 WHERE 中使用)。
StringCol:

字符串(字符)列。其他关键字

length:
如果给定,则类型将类似于 VARCHAR(length)。如果没有给出,则假定为 TEXT(即无长度)。
varchar:
一个布尔值;如果您有长度,则区分 CHARVARCHAR,默认为 True,即使用 VARCHAR
UnicodeCol:

StringCol 的子类。还接受 dbEncoding 关键字参数,其默认为 None,这意味着在 sqlmeta 和连接中查找 dbEncoding,如果在任何地方都没有定义 dbEncoding,则默认为 "utf-8"。从数据库中进出的值将被编码和解码。注意:在查询中使用 UnicodeCol 有一些限制

  • 仅支持简单的 q-magic 字段;不支持表达式;
  • 仅支持 == 和 != 运算符;

以下代码有效

MyTable.select(u'value' == MyTable.q.name)
MyTable.select(MyTable.q.name != u'value')
MyTable.select(OR(MyTable.q.col1 == u'value1', MyTable.q.col2 != u'value2'))
MyTable.selectBy(name = u'value')
MyTable.selectBy(col1=u'value1', col2=u'value2')
MyTable.byCol1(u'value1') # if col1 is an alternateID

以下代码无效

MyTable.select((MyTable.q.name + MyTable.q.surname) == u'value')

在这种情况下,您必须自己应用编码

MyTable.select((MyTable.q.name + MyTable.q.surname) == u'value'.encode(dbEncoding))
UuidCol:
用于 UUID 的列。在 Postgres 上使用“UUID”数据类型,在所有其他后端上使用 VARCHAR(36)。

类/表之间的关系

ForeignKey

您可以使用 ForeignKey 来处理表中的外键引用,但对于反向引用和多对多关系,您将使用联接。

ForeignKey 允许您使用关键字 cascade 指定引用完整性,该关键字可以具有以下值

None:
不对相关的已删除列执行任何操作(这是默认值)。按照 Person/Address 示例,如果您删除 id 为 1(John Doe)的 Person 对象,则 id 为 1(123 W Main St)的 Address 将保持不变(personID=1)。
False:
删除使用 ForeignKey 与其他对象相关联的对象将失败(设置 ON DELETE RESTRICT)。按照 Person/Address 示例,如果您删除 id 为 1(John Doe)的 Person 对象,则会引发 SQLObjectIntegrityError 异常,因为 id 为 1(123 W Main St)的 Address 对它有引用(personID=1)。
True:
删除使用 ForeignKey 与其他对象相关联的对象也将删除所有相关对象(设置 ON DELETE CASCADE)。按照 Person/Address 示例,如果您删除 id 为 1(John Doe)的 Person 对象,则 id 为 1(123 W Main St)的 Address 也将被删除。
‘null’:
使用 ForeignKey 删除与其他对象相关的对象将把 ForeignKey 列设置为 NULL/None(设置 ON DELETE SET NULL)。按照 Person/Address 示例,如果你删除 ID 为 1(John Doe)的 Person 对象,则 ID 为 1(123 W Main St)的 Address 将被保留,但对人的引用将被设置为 NULL/None (personID=None)。

MultipleJoin 和 SQLMultipleJoin:一对多

请参阅 一对多关系,了解一对多关系的示例。

MultipleJoin 返回结果列表,而 SQLMultipleJoin 返回 SelectResults 对象。

允许向 MultipleJoin 构造函数传递多个关键字参数

joinColumn:
指向此表的键的列名。因此,如果你有一个表 Product,另一个表有一个指向此表的列 ProductNo,那么你将使用 joinColumn="ProductNo"。警告:你传递的参数必须符合数据库中的列名,而不是类中的属性。因此,如果你有一个包含 ProductNo 列的 SQLObject,则它可能在数据库中被转换为 product_no_id (product_no 是正常的将大写转换为小写 + 下划线的 SQLO 转换,添加的 _id 只是因为引用表的列可能是一个 ForeignKey,而 SQLO 以这种方式转换外键)。你应该传递该参数。
orderBy:
select()orderBy 参数类似,你可以指定连接对象返回的顺序。如果未指定,将使用 defaultOrderNone 强制无序结果。
joinMethodName:
动态添加连接(使用类方法 addJoin)时,你可以提供连接的访问器名称。它也可以自动创建,并且通常是隐含的(即,addresses = MultipleJoin(...) 隐含 joinMethodName="addresses")。

RelatedJoin 和 SQLRelatedJoin:多对多

请参阅 多对多关系,了解多对多连接的使用示例。

RelatedJoin 返回结果列表,而 SQLRelatedJoin 返回 SelectResults 对象。

RelatedJoin 具有 MultipleJoin 的所有关键字参数,外加

otherColumn:
类似于 joinColumn,但引用的是联接的类。关于列名的警告相同。
intermediateTable:
引用两个类的中间表的名称。警告:您应该传递数据库表名,而不是表示的 SQLO 类。
addRemoveName:
user/role 示例 中,创建了方法 addRole(role)removeRole(role)。可以通过在此处提供字符串值来更改这些方法名称的 Role 部分。
createRelatedTable:
默认值:True。如果为 False,则不会自动创建相关表;相反,您必须手动创建它(例如,使用显式 SQLObject 类进行联接)。0.7.1 中的新增功能。

注意

假设您有从 SQLObject 继承的类 Alpha 和 Beta,以及用于多对多关系的 AlphasAndBetas。AlphasAndBetas 包含引用 Alpha 的 alphaIndex 外键列,以及引用 Beta 的 betaIndex FK 列。如果您希望在 Alpha 中有一个“betas”RelatedJoin,则应将其添加到 Alpha,并传递“Beta”(类名!)作为第一个参数,然后传递“alpha_index_id”作为 joinColumn,“beta_index_id”作为 otherColumn,以及“alphas_and_betas”作为 intermediateTable。

需要使用 joinColumnotherColumnintermediateTable 的示例架构

CREATE TABLE person (
    id SERIAL,
    username VARCHAR(100) NOT NULL UNIQUE
);

CREATE TABLE role (
    id SERIAL,
    name VARCHAR(50) NOT NULL UNIQUE
);

CREATE TABLE assigned_roles (
    person INT NOT NULL,
    role INT NOT NULL
);

然后在类中使用

class Person(SQLObject):
    username = StringCol(length=100, alternateID=True)
    roles = RelatedJoin('Role', joinColumn='person', otherColumn='role',
                        intermediateTable='assigned_roles')
class Role(SQLObject):
    name = StringCol(length=50, alternateID=True)
    roles = RelatedJoin('Person', joinColumn='role', otherColumn='person',
                        intermediateTable='assigned_roles')

SingleJoin:一对一

类似于 MultipleJoin,但仅返回一个对象,而不是一个列表。

连接池

连接对象从池中获取一个新的底层 DB API 连接并将其存储起来;底层连接从池中移除;“释放”表示“将其返回到池中”。对于单线程程序,池中有一个连接。

如果池为空,则打开一个新的底层连接;如果已禁用池(通过设置 conn._pool = None),则连接将关闭,而不是返回到池中。

事务

SQLObject 中的事务支持留给数据库。事务可以使用以下方式

conn = DBConnection.PostgresConnection('yada')
trans = conn.transaction()
p = Person.get(1, trans)
p.firstName = 'Bob'
trans.commit()
p.firstName = 'Billy'
trans.rollback()

此处的 trans 对象本质上是对单个数据库连接的包装,commitrollback 只是将该消息传递给底层连接。

可以调用尽可能多的 .commit(),但在 .rollback() 之后必须调用 .begin()。最后一个 .commit() 应调用为 .commit(close=True) 以将底层连接释放回连接池。

您可以在支持它的那些数据库中使用 SELECT FOR UPDATE

Person.select(Person.q.name=="value", forUpdate=True, connection=trans)

方法 sqlhub.doInTransaction 可用于在事务中运行一段代码。该方法接受一个可调用对象以及位置和关键字参数。它使用其 processConnectionthreadConnection 开始一个事务,调用可调用对象,提交事务并关闭底层连接;它返回可调用对象返回的任何内容。如果在调用可调用对象期间发生错误,它将回滚事务并重新引发异常。

自动架构生成

所有连接都支持基于类定义创建和删除表。首先,您必须准备类定义,这意味着在列中包含类型信息。

索引

您还可以为表定义索引,这仅在通过 SQLObject 创建表时有意义(SQLObject 依赖于数据库来实现索引)。您再次使用属性分配来执行此操作,例如

firstLastIndex = DatabaseIndex('firstName', 'lastName')

这将在两列上创建一个索引,如果您正在选择特定名称,则此索引很有用。当然,您可以给出单列,并且您可以给出列对象 (firstName) 而不是字符串名称。请注意,如果您使用 uniquealternateID(暗示 unique),数据库可能会为您创建一个索引,并且主键始终编入索引。

如果您向 DatabaseIndex 提供关键字参数 unique,您将创建一个唯一索引——列的组合必须是唯一的。

您还可以在列名称中使用字典来添加额外的选项。例如

lastNameIndex = DatabaseIndex({'expression': 'lower(last_name)'})

在这种情况下,索引将位于该列的小写版本上。似乎只有 PostgreSQL 支持此功能。您还可以执行

lastNameIndex = DatabaseIndex({'column': lastName, 'length': 10})

它要求数据库仅关注前十个字符。只有 MySQL 支持此功能,但在其他数据库中会被忽略。

创建和删除表

要创建表,请调用 createTable。它采用两个参数

ifNotExists:
如果表已存在,则不要尝试创建它。默认值为 False。
createJoinTables:
如果您使用了 多对多关系,则将创建中间表(但仅适用于两个涉及的类之一)。默认值为 True。

dropTable 采用参数 ifExistsdropJoinTables,不言自明。

动态类

SQLObject 类可以动态操作。这为从 XML 文件、数据库内省或图形界面构建 SQLObject 类提供了可能性。

自动类生成

SQLObject 可以从数据库中读取表描述,并填写类列(通常在 _columns 属性中描述)。像这样操作

class Person(SQLObject):
    class sqlmeta:
        fromDatabase = True

您仍然可以指定列(在 _columns 中),并且只会添加缺失的列。

运行时列和连接更改

您可以在运行时向您的类添加和删除列。此类更改将影响所有实例,因为更改是在类中就地进行的。有两种 类 sqlmeta 对象 的方法,addColumndelColumn,它们都将 Col 对象(或子类)作为参数。还有一个选项参数 changeSchema,如果为 True,将从数据库中添加或删除列(通常使用 ALTER 命令)。

添加列时,您必须将名称作为列构造函数的一部分传递,例如 StringCol("username", length=20)。删除列时,您可以使用 Col 对象(如在 sqlmeta.columns 中找到的,或在 addColumn 中使用的),也可以使用列名称(如 MyClass.delColumn("username"))。

您还可以添加 连接,例如 MyClass.addJoin(MultipleJoin("MyOtherClass")),并使用 delJoin 删除连接。 delJoin 不使用字符串,您必须从 sqlmeta.joins 属性中获取连接对象。

旧版数据库架构

通常您将拥有一个已经存在的数据库,并且它不使用 SQLObject 预期的命名约定,或者根本不使用任何命名约定。

SQLObject 要求

虽然 SQLObject 尝试不对您的架构提出太多要求,但会做出一些假设。其中一些将来可能会放松。

您要转换为类的所有表都需要具有整数主键。该键应定义如下

MySQL
INT PRIMARY KEY AUTO_INCREMENT
Postgres
SERIAL PRIMARY KEY
SQLite
INTEGER PRIMARY KEY AUTOINCREMENT

SQLObject 不支持由多列组成的主键(这可能不会改变)。它通常不支持主键具有业务含义的表,即,主键被假定为不可变的(这不会改变)。

目前,外键列名称必须以 "ID" 结尾(不区分大小写)。此限制可能会在下一个版本中删除。

由多列组成的主键的解决方法

如果数据库表/视图具有一个数字主键,则应使用 sqlmeta - idName 将表列名称映射到 SQLObject id 列。

如果主键仅由数字列组成,则可以这样创建虚拟列 id

Postgresql 示例

select ‘1’||lpad(PK1,max_length_of_PK1,’0’)||lpad(PK2,max_length_of_PK2,’0’)||…||lpad(PKn,max_length_of_PKn,’0’) as “id”, column_PK1, column_PK2, .., column_PKn, column… from table;

注意

  • 字符串开头的任意 ‘1’ 允许第一个 PK 的前导零。
  • 应用程序设计人员必须确定每个主键的最大长度。

此语句可以保存为视图,或者可以将列添加到数据库表中,在其中可以使用数据库触发器对其进行更新。

显然,“视图”方法通常不允许插入、更新或删除。对于 Postgresql,您可能希望查阅“规则”章节来操作底层表。

对于字母数字主键列,可以使用类似的方法

lpaded PK 的每个字符都必须使用 ascii(character) 传输,该字符返回一个 3 位数字,可以按上述方式连接。

注意事项

  • 这样,id 可能会变成一个非常大的整数,这可能会在其他地方造成问题。
  • 如果 where 子句指定 PK 列,则不会出现性能损失。

示例:CD 专辑 * 专辑:PK=ean * 曲目:PK=ean、disc_nr、track_nr

显示曲目的数据库视图开始

SELECT ean||lpad(“disc_nr”,2,’0’)||lpad(“track_nr”,2,’0’) as id, … 注意:对于 ean 号码,不需要前导“1”和填充

Tracks.select(Tracks.q.ean==id) … 其中 id 是专辑的 ean。

更改命名样式

默认情况下,SQLObject 中的名称在 Python 中应为混合大小写(如 mixedCase),在 SQL 中以下划线分隔(如 mixed_case)。这适用于表和列名称。主键假定为 id

还有其他样式。一种典型样式是混合大小写列名称,以及包含表名称的主键,如 ProductID。您可以使用不同的 Style 对象来指示不同的命名约定。例如

class Person(SQLObject):
    class sqlmeta:
        style = MixedCaseStyle(longID=True)
    firstName = StringCol()
    lastName = StringCol()

如果您使用 Person.createTable(),您将获得

CREATE TABLE Person (
    PersonID INT PRIMARY KEY,
    FirstName Text,
    LastName Text
)

MixedCaseStyle 对象处理单词的初始大写,但除此之外不予处理。通过使用 longID=True,我们指示主键应看起来像一个普通引用(MixedCaseStylePersonID,或默认样式的 person_id)。

如果您希望全局更改样式,请将样式分配给连接,如

__connection__.style = MixedCaseStyle(longID=True)

不规则命名

现在已涵盖在 类 sqlmeta 部分中。

非整数键

虽然严格来说这不是遗留数据库问题,但它属于“不规则性”类别。如果您使用非整数键,则所有主键管理都由您负责。您必须自己创建表(SQLObject 可以创建具有 int 或 str ID 的表),并且在创建实例时,您必须将 id 关键字参数传递到构造函数中(如 Person(id='555-55-5555', ...))。

DBConnection:数据库连接

目前,DBConnection 模块有六个外部类,MySQLConnectionPostgresConnectionSQLiteConnectionSybaseConnectionMaxdbConnectionMSSQLConnection

可以向任何连接器传递关键字参数 debug。如果设置为 true,则发送到数据库的任何 SQL 也会打印到控制台。

还可以传递 logger 关键字参数,它应该是要使用的记录器的名称。如果指定了该参数,且 debugTrue,SQLObject 将通过该记录器写入调试打印语句,而不是直接打印到控制台。参数 loglevel 允许选择记录级别 - 它可以是 debuginfowarningerrorcriticalexception。如果 logger 不存在或为空,SQLObject 会使用 print 而不是记录;在这种情况下,loglevel 可以是 stdoutstderr;默认值为 stdout

要配置记录,可以执行类似以下操作

import logging
logging.basicConfig(
    filename='test.log',
    format='[%(asctime)s] %(name)s %(levelname)s: %(message)s',
    level=logging.DEBUG,
)
log = logging.getLogger("TEST")
log.info("Log started")

__connection__ = "sqlite:/:memory:?debug=1&logger=TEST&loglevel=debug"

该代码将 SQLObject 调试消息重定向到 test.log 文件。

MySQL

MySQLConnection 采用关键字参数 hostportdbuserpassword,就像 MySQLdb.connect 所做的那样。

MySQLConnection 支持所有功能,尽管 MySQL 仅在使用 InnoDB 后端时支持 事务;SQLObject 可以使用 sqlmeta.createSQL 显式定义后端。

支持的驱动程序是 mysqldbconnectoroursqlpymysqlmariadbpyodbcpypyodbcodbc(尝试 pyodbcpypyodbc);默认值为 mysqldb

关键字参数 conv 允许传递自定义转换器列表。示例

import time
import sqlobject
import MySQLdb.converters

def _mysql_timestamp_converter(raw):
         """Convert a MySQL TIMESTAMP to a floating point number representing
         the seconds since the Un*x Epoch. It uses custom code the input seems
         to be the new (MySQL 4.1+) timestamp format, otherwise code from the
         MySQLdb module is used."""
         if raw[4] == '-':
             return time.mktime(time.strptime(raw, '%Y-%m-%d %H:%M:%S'))
         else:
             return MySQLdb.converters.mysql_timestamp_converter(raw)

conversions = MySQLdb.converters.conversions.copy()
conversions[MySQLdb.constants.FIELD_TYPE.TIMESTAMP] = _mysql_timestamp_converter

MySQLConnection = sqlobject.mysql.builder()
connection = MySQLConnection(user='foo', db='somedb', conv=conversions)

特定于连接的参数包括:unix_socketinit_commandread_default_fileread_default_groupconvconnect_timeoutcompressnamed_pipeuse_unicodeclient_flaglocal_infilessl_keyssl_certssl_cassl_capathcharset

Postgres

PostgresConnection 采用单个连接字符串,如 "dbname=something user=some_user",就像 psycopg.connect 一样。您还可以使用与 MySQLConnection 相同的关键字参数,并将构建一个 dsn 字符串。

PostgresConnection 支持事务及所有其他功能。

用户可以通过在 DB URI 或 PostgresConnection 中使用 driver 参数来选择 PostgreSQL 的 DB API 驱动程序,该参数可以是驱动程序名称的逗号分隔列表。可能的驱动程序包括:psycopg2psycopgpsycopg2 的别名)、pygresqlpypostgresqlpg8000pyodbcpypyodbcodbc(尝试 pyodbcpypyodbc)。默认值为 psycopg

特定于连接的参数包括:sslmodeunicodeColsschemacharset

SQLite

SQLiteConnection 采用单个字符串,即指向数据库文件的路径。

SQLite 将所有数据放入一个文件中,并在操作过程中在同一目录中打开一个日志文件(该文件在程序退出时删除)。SQLite 不限制您可以在一列中放入的类型——字符串可以放入整数列中,日期可以放入整数中,依此类推。

SQLite 可能存在并发问题,具体取决于您在多线程环境中的使用情况。

用户可以通过在 DB URI 或 SQLiteConnection 中使用 driver 参数来选择 SQLite 的 DB API 驱动程序,该参数可以是驱动程序名称的逗号分隔列表。可能的驱动程序有:pysqlite2(别名 sqlite2)、sqlite3sqlite(别名 sqlite1)。默认情况下,按此顺序测试 pysqlite2、sqlite3 和 sqlite。

特定于连接的参数有:encodingmodetimeoutcheck_same_threaduse_table_info

Firebird

FirebirdConnection 接受参数 hostdbuser(默认 "sysdba")、password(默认 "masterkey")。

Firebird 支持所有功能。支持尚处于早期阶段,因此可能存在一些问题,尤其是在并发访问时,尤其是在使用延迟选择时。如果遇到问题,请尝试 list(MyClass.select()) 以避免并发游标(使用 list() 将预取选择的所有结果)。

Firebird 支持 fdbkinterbasdbfirebirdsql 驱动程序。默认值是 fdbkinterbasdb

如果尝试从在 Unix 上运行的程序连接到在 w32 上运行的服务器,则可能存在问题;问题在于如何指定数据库,以便 SQLObject 正确解析它。只有在 w32 上,竖线才会被分号替换。在 Unix 上,竖线是一个很普通的字符,不能被处理。

解决此问题的最正确方法是使用数据库名称而不是文件名连接到 DB。在 Firebird 中,DBA 可以设置别名,而不是在 aliases.conf 文件中设置数据库名称

Firebird 2.0 管理员手册 中的示例

# fbdb1 is on a Windows server:
fbdb1 = c:\Firebird\sample\Employee.fdb

现在,程序可以连接到 firebird://host:port/fbdb1。

服务器正在运行时,可以编辑 aliases.conf。无需停止并重新启动服务器,即可识别新的 aliases.conf 条目。

如果你正在使用索引并收到类似索引键大小超出实现限制的错误,请参阅 此页面 以了解对索引的限制。

特定于连接的参数有:dialectrolecharset

Sybase

SybaseConnection 接受参数 hostdbuserpassword。它还接受额外的布尔参数 locking(默认值为 True),该参数在执行连接时传递。如果你不使用多个线程,则可以将 locking 的值设为 False,以获得轻微的性能提升。

它使用 Sybase 模块。

特定于连接的参数是:lockingautoCommit

MAX DB

MAX DB,也称为 SAP DB,可从 SAP 和 MySQL 合作关系中获得。它采用典型参数:hostdatabaseuserpassword。它还采用参数 sqlmode(默认 "internal")、isolationtimeout,在创建与数据库的连接时会传递这些参数。

它使用 sapdb 模块。

特定于连接的参数是:autoCommitsqlmodeisolationtimeout

MS SQL Server

MSSQLConnection 对象希望使用格式为

mssql://user:pass@host:port/db

的新样式连接字符串。然后,这将映射到正确的驱动程序格式。如果在“已命名”端口上运行 SQL Server,请务必在 URI 中指定端口号。

当前支持的两个驱动程序是 adodbapipymssql

用户可以通过在 DB URI 或 MSSQLConnection 中使用 driver 参数来选择用于 MSSQL 的 DB API 驱动程序,该参数可以是驱动程序名称的逗号分隔列表。可能的驱动程序是:adodb(别名 adodbapi)和 pymssql。默认情况下,按此顺序测试 adodbapipymssql

特定于连接的参数是:autoCommittimeout

事件(信号)

信号是一种机制,用于在数据或架构通过 SQLObject 发生更改时收到通知。这可能对执行自定义数据验证、记录更改、设置默认属性等很有用。信号可以执行的部分操作也可以通过覆盖方法来实现,但信号可能提供更简洁的方法,尤其是在未通过继承关联的类中。

示例

from sqlobject.events import listen, RowUpdateSignal, RowCreatedSignal
from model import Users

def update_listener(instance, kwargs):
    """keep "last_updated" field current"""
    import datetime
    # BAD method 1, causes infinite recursion?
    # instance should be read-only
    instance.last_updated = datetime.datetime.now()
    # GOOD method 2
    kwargs['last_updated'] = datetime.datetime.now()

def created_listener(instance, kwargs, post_funcs):
    """"email me when new users added"""
    # email() implementation left as an exercise for the reader
    msg = "%s just was just added to the database!" % kwargs['name']
    email(msg)

listen(update_listener, Users, RowUpdateSignal)
listen(created_listener, Users, RowCreatedSignal)

导出的符号

您可以使用 from sqlobject import *,但不必这样做。它导出一小部分符号。导出的符号

来自 sqlobject.main

  • NoDefault
  • SQLObject
  • getID
  • getObject

来自 sqlobject.col: * Col * StringCol * IntCol * FloatCol * KeyCol * ForeignKey * EnumCol * SetCol * DateTimeCol * DateCol * TimeCol * TimestampCol * DecimalCol * CurrencyCol

来自 sqlobject.joins: * MultipleJoin * RelatedJoin

来自 sqlobject.styles: * Style * MixedCaseUnderscoreStyle * DefaultStyle * MixedCaseStyle

来自 sqlobject.sqlbuilder

  • AND
  • OR
  • NOT
  • IN
  • LIKE
  • DESC
  • CONTAINSSTRING
  • const
  • func

LEFT JOIN 和其他 JOIN

首先在 常见问题解答 中查看问题“如何执行 LEFT JOIN?”

还在吗?好吧。要执行 JOIN,请使用 SQLBuilder 中的 JOIN 帮助器之一。将帮助器的实例传递给 .select() 方法。例如

from sqlobject.sqlbuilder import LEFTJOINOn
MyTable.select(
    join=LEFTJOINOn(Table1, Table2,
                    Table1.q.name == Table2.q.value))

将生成查询

SELECT my_table.* FROM my_table, table1
LEFT JOIN table2 ON table1.name = table2.value;

如果要与主表进行联接,请将第一个表保留为 None

MyTable.select(
    join=LEFTJOINOn(None, Table1,
                    MyTable.q.name == Table1.q.value))

将生成查询

SELECT my_table.* FROM my_table
LEFT JOIN table2 ON my_table.name = table1.value;

.select() 的联接参数可以是 JOIN() 或 JOIN() 的序列(列表/元组)。

可用的联接是 JOIN、INNERJOIN、CROSSJOIN、STRAIGHTJOIN、LEFTJOIN、LEFTOUTERJOIN、NATURALJOIN、NATURALLEFTJOIN、NATURALLEFTOUTERJOIN、RIGHTJOIN、RIGHTOUTERJOIN、NATURALRIGHTJOIN、NATURALRIGHTOUTERJOIN、FULLJOIN、FULLOUTERJOIN、NATURALFULLJOIN、NATURALFULLOUTERJOIN、INNERJOINOn、LEFTJOINOn、LEFTOUTERJOINOn、RIGHTJOINOn、RIGHTOUTERJOINOn、FULLJOINOn、FULLOUTERJOINOn、INNERJOINUsing、LEFTJOINUsing、LEFTOUTERJOINUsing、RIGHTJOINUsing、RIGHTOUTERJOINUsing、FULLJOINUsing、FULLOUTERJOINUsing。

如何将表与自身联接?

使用 SQLBuilder 中的 Alias。示例

from sqlobject.sqlbuilder import Alias
alias = Alias(MyTable, "my_table_alias")
MyTable.select(MyTable.q.name == alias.q.value)

将生成查询

SELECT my_table.* FROM my_table, my_table AS my_table_alias
WHERE my_table.name = my_table_alias.value;

可以使用带有别名的 JOIN() 吗?

当然!这是 JOIN 和别名主要开发的情况。代码

from sqlobject.sqlbuilder import LEFTJOINOn, Alias
alias = Alias(OtherTable, "other_table_alias")
MyTable.select(MyTable.q.name == OtherTable.q.value,
    join=LEFTJOINOn(MyTable, alias, MyTable.col1 == alias.q.col2))

将生成查询

SELECT my_table.* FROM other_table,
    my_table LEFT JOIN other_table AS other_table_alias
WHERE my_table.name == other_table.value AND
    my_table.col1 = other_table_alias.col2.

子查询(子选择)

可以在那些可以执行子查询的 DBMS 上运行带有子查询(子选择)的查询(MySQL 从版本 4.1 开始支持子查询)。

使用 SQLBuilder 中的相应类和函数

from sqlobject.sqlbuilder import EXISTS, Select
select = Test1.select(EXISTS(Select(Test2.q.col2, where=(Outer(Test1).q.col1 == Test2.q.col2))))

生成查询

SELECT test1.id, test1.col1 FROM test1 WHERE
EXISTS (SELECT test2.col2 FROM test2 WHERE (test1.col1 = test2.col2))

注意 Outer 的用法 - 它是一个帮助器,允许引用外部查询中的表。

使用 Select() 而不是 .select(),因为需要控制内部查询返回哪些列。

可用的查询是 IN()NOTIN()EXISTS()NOTEXISTS()SOME()ANY()ALL()。最后 3 个与比较运算符一起使用,如下所示:somevalue = ANY(Select(...))

实用程序

SQLObject 中包含一些有用的实用程序函数。有关更多信息,请参阅其模块文档字符串。

SQLBuilder

有关 SQLBuilder 的更多信息,请阅读 SQLBuilder 文档

Get SQLObject at SourceForge.net. Fast, secure and Free Open Source software downloads