SQLObject 常见问题解答

SQLExpression

SomeTable.select(SomeTable.q.Foo > 30) 中,为什么内部参数 SomeTable.q.Foo > 30 没有被评估为某个布尔值?

q 是一个返回类型为 sqlbuilder.SQLExpression 的特殊属性的对象。SQLExpression 是一个特殊类,它覆盖了几乎所有 Python 魔术方法,并且在任何操作中,它不会对其进行评估,而是构造另一个 SQLExpression 实例,以记住它必须执行的操作。这类似于符号代数。示例

SQLExpression(“foo”) > 30

生成 SQLExpression(“foo”, “>”, 30)(实际上,它确实生成了 SQLExpression(SQLExpression(“foo”)…))

select(…) 方法如何知道要做什么?

简而言之,select() 将最顶层的 SQLExpression 递归地评估为一个字符串

SQLExpression(“foo”, “>”, 30) => “foo > 30”

并将结果作为字符串传递给 SQL 后端。

更长的但更详细且正确的解释是,select() 生成 SelectResults 类的实例,在对其进行迭代时,会生成 Iteration 类的实例,在调用其 next() 方法(它是迭代器!)时,会构造 SQL 查询字符串,将其传递给后端,获取结果,将每一行包装为 SQLObject 实例,并将它们传递回用户。

有关实现的详细信息,请参阅 sqlobject/main.py(SQLObject)、sqlobject/sqlbuilder.py(SQLExpression)、sqlobject/dbconnection.py(构造查询字符串的 DBConnection 类)和 Iteration 类,以及 sqlobject 的不同子目录(用于连接类的具体实现 - 不同的后端需要不同的查询字符串)。

为什么没有 __len__?

没有 __len__ 方法的原因有很多,尽管许多人认为拥有这些方法会让他们感觉更融入 Python。

原因之一是,len(foo) 预计会很快,但发出 COUNT 查询可能会很慢。更糟糕的是,当执行实际查询时,这通常会导致数据库执行本质上冗余的工作(通常在获取序列的长度之后会访问该序列中的项)。

另一种是 list(foo) 隐式地尝试首先执行 len,作为优化(因为预期 len 很便宜——请参见上一点)。更糟糕的是,它会吞下在调用 __len__ 期间发生的所有异常,因此如果它失败(例如查询中某个地方有错别字),则原始原因会被静默地丢弃,而你却会莫名其妙地遇到诸如“当前事务已中止,命令被忽略,直到事务块结束”之类的奇怪错误。

如何执行 LEFT JOIN?

简而言之:你不能。你不需要。那是一种关系方式,而不是一种对象方式。但没关系!即使不是使用相同的查询,执行相同操作也不难。

对于这些示例,想象你有一堆客户,带有联系人。并非所有客户都有联系人,有些客户有多个联系人。LEFT JOIN 如下所示

SELECT customer.id, customer.first_name, customer.last_name,
       contact.id, contact.address
FROM customer
LEFT JOIN contact ON contact.customer_id = customer.id

简单

for customer in Customer.select():
    print customer.firstName, customer.lastName
    for contact in customer.contacts:
        print '   ', contact.phoneNumber

效果与 LEFT JOIN 相同——你获得了所有客户,并获得了他们所有的联系人。然而,问题在于你将执行更多查询——为每个客户查询以获取联系人——而使用 LEFT JOIN 时你只需执行一个查询。从数据库返回的实际信息量将相同。很有可能这不会显著变慢。我建议以这种方式进行,除非你遇到了实际的性能问题。

高效

假设你真的不想执行所有这些查询。好吧,没问题

custContacts = {}
for contact in Contact.select():
    custContacts.setdefault(contact.customerID, []).append(contact)
for customer in Customer.select():
    print customer.firstName, customer.lastName
    for contact in custContacts.get(customer.id, []):
        print '   ', contact.phoneNumber

这样一来,最多只有两个查询。它有点粗糙,但这是一个优化,而优化通常看起来不太美观。

但是,假设你不想获取所有人,只想获取某一群人(大概是一个足够大的群体,以至于你仍然需要此优化)

query = Customer.q.firstName.startswith('J')
custContacts = {}
for contact in Contact.select(AND(Contact.q.customerID == Customer.q.id,
                                  query)):
    custContacts.setdefault(contact.customerID, []).append(contact)
for customer in Customer.select(query):
    print customer.firstName, customer.lastName
    for contact in custContacts.get(customer.id, []):
        print '   ', contact.phoneNumber

SQL 方式

SQLBuilder 中使用 LEFTJOIN()。

如何将表与自身连接?

SQLBuilder 中使用 Alias。请参见 示例

如何在多对多关系中定义我自己的中间表?

注意

在 User 和 Role 中,SQLRelatedJoin 与 createRelatedTable=False 一起使用,因此不会自动创建中间表。我们还使用 intermediateTable=’user_roles’ 设置中间表名称。UserRoles 是我们中间表的定义。UserRoles 创建一个唯一索引以确保我们在数据库中没有重复数据。我们还添加了一个名为 active 的额外字段,该字段具有布尔值。在此示例中,active 列可用于激活/停用用户给定的角色。在此中间表中添加的另一个常见字段可能是排序字段。如果你想直接从中间表获取行列表,请向 User 或 Role 类添加 MultipleJoin。

我们将在用户和角色示例的基础上进行扩展,并定义我们自己的 UserRoles 类,该类将成为用户和角色多对多关系的中间表。

示例

>>> class User(SQLObject):
...     class sqlmeta:
...         table = "user_table"
...     username = StringCol(alternateID=True, length=20)
...     roles = SQLRelatedJoin('Role',
...         intermediateTable='user_roles',
...         createRelatedTable=False)

>>> class Role(SQLObject):
...     name = StringCol(alternateID=True, length=20)
...     users = SQLRelatedJoin('User',
...         intermediateTable='user_roles',
...         createRelatedTable=False)

>>> class UserRoles(SQLObject):
...     class sqlmeta:
...         table = "user_roles"
...     user = ForeignKey('User', notNull=True, cascade=True)
...     role = ForeignKey('Role', notNull=True, cascade=True)
...     active = BoolCol(notNull=True, default=False)
...     unique = index.DatabaseIndex(user, role, unique=True)

继承如何运作?

SQLObject 并非旨在在 RDBMS 中表示每个 Python 继承结构,而是旨在将 RDBMS 结构表示为 Python 对象。因此,您可以在 Python 中执行许多操作,而无法使用 SQLObject 类执行。但是,某些形式的继承是可能的。

使用它的方法之一是创建本地约定。也许

class SiteSQLObject(SQLObject):
    _connection = DBConnection.MySQLConnection(user='test', db='test')
    _style = MixedCaseStyle()

    # And maybe you want a list of the columns, to autogenerate
    # forms from:
    def columns(self):
        return [col.name for col in self._columns]

由于 SQLObject 没有坚定的自省机制(至少目前还没有),因此该示例展示了一点即席自省的开端(在这种情况下,以更令人愉悦/公开的界面展示了 _columns 属性)。

然而,这与数据库继承根本无关,因为我们没有定义任何列。如果我们这样做呢?

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

class Employee(Person):
    position = StringCol()

不幸的是,结果架构可能看起来不像您想要的

CREATE TABLE person (
    id INT PRIMARY KEY,
    first_name TEXT,
    last_name TEXT
);

CREATE TABLE employee (
    id INT PRIMARY KEY
    first_name TEXT,
    last_name TEXT,
    position TEXT
)

来自 person 的所有列在 employee 表中只是重复的。更重要的是,Person 的 ID 与员工的 ID 不同,因此,例如,您必须选择 ForeignKey("Person")ForeignKey("Employee"),您不能拥有有时引用一个、有时引用另一个的外键。

总之,不是很有用。您可能想要一个 person 表,然后是一个 employee 表,这两个表之间存在一对一关系。当然,您可以拥有它,只需创建适当的类/表即可,但它将显示为两个不同的类,您必须执行类似 Person(1).employee.position 的操作。当然,您始终可以创建必要的快捷方式,例如

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

    def _get_employee(self):
        value = Employee.selectBy(person=self)
        if value:
            return value[0]
        else:
            raise AttributeError, '%r is not an employee' % self
    def _get_isEmployee(self):
        value = Employee.selectBy(person=self)
        # turn into a bool:
        return not not value
    def _set_isEmployee(self, value):
        if value:
            # Make sure we are an employee...
            if not self.isEmployee:
                Empoyee.new(person=self, position=None)
        else:
            if self.isEmployee:
                self.employee.destroySelf()
    def _get_position(self):
        return self.employee.position
    def _set_position(self, value):
        self.employee.position = value

class Employee(SQLObject):
    person = ForeignKey('Person')
    position = StringCol()

还有另一种继承。请参阅 Inheritance.html

复合/组合属性

复合属性是由两列形成的属性。例如

CREATE TABLE invoice_item (
    id INT PRIMARY KEY,
    amount NUMERIC(10, 2),
    currency CHAR(3)
);

现在,您可能希望处理一个金额/货币值,而不是两列。SQLObject 不直接支持此操作,但自己执行此操作很容易(并且受到鼓励)

class InvoiceItem(SQLObject):
    amount = Currency()
    currency = StringChar(length=3)

    def _get_price(self):
        return Price(self.amount, self.currency)
    def _set_price(self, price):
        self.amount = price.amount
        self.currency = price.currency

class Price(object):
    def __init__(self, amount, currency):
        self._amount = amount
        self._currency = currency

    def _get_amount(self):
        return self._amount
    amount = property(_get_amount)

    def _get_currency(self):
        return self._currency
    currency = property(_get_currency)

    def __repr__(self):
        return '<Price: %s %s>' % (self.amount, self.currency)

您会注意到,我们费了一些功夫来确保 Price 是一个不可变对象。这很重要,因为如果 Price 不是不可变对象,并且有人更改了属性,则包含的 InvoiceItem 实例将无法检测到更改并更新数据库。(此外,由于 Price 不是 SQLObject 的子类,因此我们必须明确创建属性)有些人将这种类型的类称为值对象,其用法类似于整数或字符串的用法。

您还可以使用可变复合类

class Address(SQLObject):
    street = StringCol()
    city = StringCol()
    state = StringCol(length=2)

    latitude = FloatCol()
    longitude = FloatCol()

    def _init(self, id):
        SQLObject._init(self, id)
        self._coords = SOCoords(self)

    def _get_coords(self):
        return self._coords

class SOCoords(object):
    def __init__(self, so):
        self._so = so

    def _get_latitude(self):
        return self._so.latitude
    def _set_latitude(self, value):
        self._so.latitude = value
    latitude = property(_get_latitude, set_latitude)

    def _get_longitude(self):
        return self._so.longitude
    def _set_longitude(self, value):
        self._so.longitude = value
    longitude = property(_get_longitude, set_longitude)

实际上,它几乎是一个代理,但是 SOCoords 可能包含其他逻辑,可能与非基于 SQLObject 的纬度/经度值交互,或者可能在具有纬度/经度列的多个对象中使用。

非整数 ID

是的,您可以使用非整数 ID。

如果您使用非整数 ID,您将无法使用自动 CREATE TABLE 生成(即 createTable);SQLObject 可以创建具有 int 或 str ID 的表。您还必须在创建对象时提供自己的 ID 值,如下所示

color = Something(id="blue", r=0, b=100, g=0)

ID 是,并且在未来版本中始终会被视为不可变的。现在尚未强制执行;您可以分配给 id 属性。但是如果您这样做,只会搞乱所有内容。这可能会在某个时候被取消,以避免可能令人困惑的错误(实际上,分配给 id 几乎肯定会导致令人困惑的错误)。

如果您担心强制执行 ID 类型(即使是整数 ID 也可能有问题),您可能需要执行此操作

def Color(SQLObject):
    def _init(self, id, connection=None):
        id = str(id)
        SQLObject._init(self, id, connection)

您可以使用 int() 或您想要的任何其他内容,而不是 str()。当 ID 列类型可以像其他列一样声明时,这将在未来版本中得到解决。

此外,您可以在 SQLObject 类中设置 idType=str。

二进制值

二进制值难以存储在数据库中,因为 SQL 没有广泛实现的方法将二进制值表示为文本,并且数据库中的支持不同。

模块 sqlobject.col 定义了验证器和列类,在一定程度上支持二进制值。BLOBCol 扩展了 StringCol,允许存储二进制值;目前它仅适用于 PostgreSQL 和 MySQL。PickleCol 扩展了 BLOBCol,允许在列中存储任何对象;该列在分配时自然会对对象进行腌制,并在从数据库中检索数据时对其进行反腌制。

将二进制数据保留在数据库中的另一种可能方法是使用编码。Base 64 是一种很好的编码,相当紧凑,但也安全。例如,假设您想将图像存储在数据库中

class Image(SQLObject):

    data = StringCol()
    height = IntCol()
    width = IntCol()

    def _set_data(self, value):
        self._SO_set_data(value.encode('base64'))

    def _get_data(self, value):
        return self._SO_get_data().decode('base64')

重新加载模块

如果您尝试重新加载定义 SQLObject 子类的模块,您可能会遇到各种奇怪的错误。简短的回答:您无法重新加载这些模块。

长答案:在 Python 中重新加载模块效果不佳。重新加载实际上意味着重新运行模块。每个 class 语句都会创建一个类,但您的旧类不会消失。当您重新加载模块时,将创建新类,它们将接管模块中的名称。

但是,SQLObject 不会搜索模块中的名称以查找类。当您说 ForeignKey('SomeClass') 时,SQLObject 会在任何地方查找具有名称 SomeClass 的任何 SQLObject 子类。这是为了避免循环导入和循环依赖的问题,因为表具有正向和反向引用,以及其他循环依赖。SQLObject 延迟解析这些依赖关系。

但是,当您重新加载模块时,进程中突然会出现两个具有相同名称的 SQLObject 类。SQLObject 不知道其中一个已过时。即使它知道,它也不知道系统中引用该过时类的所有其他位置。

由于这个原因和其他一些原因,重新加载模块极易出错且难以支持。

Python 关键字

如果你有一个表列是 Python 关键字,你应该知道 Python 属性不必与列的名称匹配。请参阅文档中的不规则命名

延迟更新和插入

延迟更新允许你推迟发送UPDATES,直到你同步对象。但是,没有办法进行延迟插入;只要你创建一个实例,INSERT就会执行。

此限制的原因是每个对象都需要一个数据库 ID,在许多数据库中,你无法在创建行之前获得 ID。

相互引用的表

如何创建相互引用的表?对于代码

class Person(SQLObject):
    role = ForeignKey("Role")

class Role(SQLObject):
    person = ForeignKey("Person")

Person.createTable()
Role.createTable()

Postgres 提出 ProgrammingError:错误:关系“role”不存在。

正确的方法是延迟约束创建,直到所有表都创建

class Person(SQLObject):
    role = ForeignKey("Role")

class Role(SQLObject):
    person = ForeignKey("Person")

constraints = Person.createTable(applyConstraints=False)
constraints += Role.createTable(applyConstraints=False)

for constraint in constraints:
    connection.query(constraint)

GROUP BY、UNION 等呢?

简而言之 - 并非每个查询都可以用 SQLObject 表示。SQLObject 的对象是“表”类的实例

class MyTable(SQLObject):
    ...

my_table_row = MyTable.get(id)

现在 my_table_row 是 MyTable 类的实例,表示 my_table 表中的一行。但是对于像这样的 GROUP BY 语句

SELECT my_column, COUNT(*) FROM my_table GROUP BY my_column;

没有表,没有相应的“表”类,SQLObject 无法返回有意义的对象列表。

你可以使用 SQLBuilder 中提供的较低级别的机制。

如何进行批量插入?

在 SQLObject 中使用高级 API 进行批量插入很慢。有很多原因。首先,在创建时,SQLObject 实例会通过验证器/转换器传递所有值,这很方便,但需要时间。其次,在 INSERT 查询后,SQLObject 执行 SELECT 查询以取回自动生成的值(id 和时间戳)。第三,有缓存和缓存维护。对于批量插入来说,这些都是不必要的,因此高级 API 不合适。

不太方便(没有验证器)但速度更快的 API 是 Insert,来自 SQLBuilder

如何指定要使用的 MySQL 引擎,或调整其他特定于 SQL 引擎的功能?

你可以在创建后使用sqlmeta属性createSQLALTER表,例如

class SomeObject(SQLObject):
    class sqlmeta:
        createSQL = { 'mysql' : 'ALTER TABLE some_object ENGINE InnoDB' }
    # your columns here

也许你也想指定字符集?没问题

class SomeObject(SQLObject):
    class sqlmeta:
        createSQL = { 'mysql' : [
            'ALTER TABLE some_object ENGINE InnoDB',
            '''ALTER TABLE some_object CHARACTER SET utf8
                COLLATE utf8_estonian_ci''']
            }
Get SQLObject at SourceForge.net. Fast, secure and Free Open Source software downloads