使用实体实例

创建实体实例

在 Pony 中创建实体实例类似于在 Python 中创建普通对象。

customer1 = Customer(login="John", password="***",
                     name="John", email="john@google.com")

在 Pony 中创建对象时,所有参数都应指定为关键字参数。如果属性具有默认值,则可以省略它。

所有创建的实例都属于当前的 db_session()。在一些对象关系映射器中,您需要调用对象的 save() 方法才能保存它。这很不方便,因为程序员必须跟踪哪些对象被创建或更新,并且不能忘记在每个对象上调用 save() 方法。

Pony 会跟踪哪些对象被创建或更新,并在当前 db_session() 结束时自动将它们保存到数据库中。如果您需要在离开 db_session() 范围之前保存新创建的对象,您可以使用 flush()commit() 函数。

从数据库加载对象

通过主键获取对象

最简单的情况是当我们想要通过主键检索对象时。为了在 Pony 中实现这一点,用户只需要将主键放在方括号中,放在类名之后。例如,要提取主键值为 123 的客户,我们可以编写

customer1 = Customer[123]

相同的语法也适用于具有复合键的对象;我们只需要在实体类描述中定义属性的相同顺序中列出复合主键的元素,并用逗号分隔。

order_item = OrderItem[order1, product1]

如果具有该主键的对象不存在,Pony 会引发 ObjectNotFound 异常。

通过属性的唯一组合获取一个对象

如果您想检索一个对象,不是通过其主键,而是通过其他属性组合,您可以使用实体的 get() 方法。在大多数情况下,它用于通过辅助唯一键获取对象,但它也可以用于通过任何其他属性组合进行搜索。作为 get() 方法的参数,您需要指定属性的名称及其值。例如,如果您想接收名为“产品 1”的产品,并且您认为数据库中只有一个名为“产品 1”的产品,您可以编写

product1 = Product.get(name='Product1')

如果未找到对象,get() 将返回 None。如果找到多个对象,则会引发 MultipleObjectsFoundError 异常。

当我们想要获取 None 而不是 ObjectNotFound 异常(如果对象不存在于数据库中)时,您可能希望使用带有主键的 get() 方法。

方法 get() 也可以接收 lambda 函数作为单个定位参数。此方法返回实体的实例,而不是 Query 类的对象。

获取多个对象

为了从数据库中检索多个对象,您应该使用实体的 select() 方法。它的参数是一个 lambda 函数,它有一个参数,代表数据库中对象的实例。在这个函数中,您可以编写条件,根据这些条件您想要选择对象。例如,如果您想查找价格高于 100 的所有产品,您可以编写

products = Product.select(lambda p: p.price > 100)

此 lambda 函数不会在 Python 中执行。相反,它将被转换为以下 SQL 查询

SELECT "p"."id", "p"."name", "p"."description",
       "p"."picture", "p"."price", "p"."quantity"
FROM "Product" "p"
WHERE "p"."price" > 100

select() 方法返回 Query 类的实例。如果您开始遍历此对象,SQL 查询将被发送到数据库,您将获得实体实例的序列。例如,以下是如何打印所有产品名称及其价格

for p in Product.select(lambda p: p.price > 100):
    print(p.name, p.price)

如果您不想遍历查询,而只需要获取对象列表,您可以这样做

product_list = Product.select(lambda p: p.price > 100)[:]

在这里,我们从查询中获取完整的切片 [:]。这相当于将查询转换为列表

product_list = list(Product.select(lambda p: p.price > 100))

在查询中使用参数

您可以在查询中使用变量。Pony 会将这些变量作为参数传递给 SQL 查询。Pony 中声明式查询语法的另一个重要优势是它提供了对 SQL 注入的完全保护,因为所有外部参数都将被正确转义。

以下是一个示例

x = 100
products = Product.select(lambda p: p.price > x)

生成的 SQL 查询将如下所示

SELECT "p"."id", "p"."name", "p"."description",
       "p"."picture", "p"."price", "p"."quantity"
FROM "Product" "p"
WHERE "p"."price" > ?

这样,x 的值将作为 SQL 查询参数传递,这完全消除了 SQL 注入的风险。

对查询结果进行排序

如果您需要按特定顺序对对象进行排序,可以使用 Query.order_by() 方法。

Product.select(lambda p: p.price > 100).order_by(desc(Product.price))

在此示例中,我们以降序显示所有价格高于 100 的产品的名称和价格。

Query 对象的方法会修改将发送到数据库的 SQL 查询。以下是为上一个示例生成的 SQL

SELECT "p"."id", "p"."name", "p"."description",
       "p"."picture", "p"."price", "p"."quantity"
FROM "Product" "p"
WHERE "p"."price" > 100
ORDER BY "p"."price" DESC

Query.order_by() 方法也可以接收 lambda 函数作为参数

Product.select(lambda p: p.price > 100).order_by(lambda p: desc(p.price))

在 .. code-block:: python 方法中使用 lambda 函数允许使用高级排序表达式。例如,以下是如何按客户订单的总价格降序对客户进行排序

Customer.select().order_by(lambda c: desc(sum(c.orders.total_price)))

为了按多个属性对结果进行排序,您需要用逗号分隔它们。例如,如果您想按价格降序对产品进行排序,同时按字母顺序显示价格相似的产品,您可以这样做

Product.select(lambda p: p.price > 100).order_by(desc(Product.price), Product.name)

相同的查询,但使用 lambda 函数将如下所示

Product.select(lambda p: p.price > 100).order_by(lambda p: (desc(p.price), p.name))

请注意,根据 Python 语法,如果从 lambda 函数中返回多个元素,则需要将它们放在括号中。

限制选定对象的数量

可以使用 Query.limit() 方法或更紧凑的 Python 切片表示法来限制查询返回的对象数量。例如,以下是如何获取最昂贵的十种产品

Product.select().order_by(lambda p: desc(p.price))[:10]

切片的结果不是查询对象,而是实体实例的最终列表。

您还可以使用 Query.page() 方法作为对查询结果进行分页的便捷方式

Product.select().order_by(lambda p: desc(p.price)).page(1)

遍历关系

在 Pony 中,您可以遍历对象关系

order = Order[123]
customer = order.customer
print customer.name

Pony 试图将发送到数据库的查询数量降到最低。在上面的示例中,如果请求的 Customer 对象已加载到缓存中,Pony 将从缓存中返回对象,而不会将查询发送到数据库。但是,如果对象尚未加载,Pony 仍然不会立即发送查询。相反,它将首先创建一个“种子”对象。种子是一个仅初始化主键的对象。Pony 不知道如何使用此对象,并且始终有可能仅需要主键。

在上面的示例中,Pony 在访问 name 属性时,在第三行中从数据库中获取对象。通过使用“种子”概念,Pony 实现了高效率并解决了“N+1”问题,这是许多其他映射器的弱点。

也可以在“多对多”方向上进行遍历。例如,如果您有一个 Customer 对象,并且您遍历其 orders 属性,您可以这样做

c = Customer[123]
for order in c.orders:
    print order.state, order.price

更新对象

当您将新值分配给对象属性时,您无需手动保存每个更新的对象。更改将在离开 db_session() 范围时自动保存到数据库中。

例如,为了将主键为 123 的产品的数量增加 10,您可以使用以下代码

Product[123].quantity += 10

要更改同一对象的多个属性,您可以分别进行操作

order = Order[123]
order.state = "Shipped"
order.date_shipped = datetime.now()

或者在一行中使用实体实例的 set() 方法

order = Order[123]
order.set(state="Shipped", date_shipped=datetime.now())

当您需要从字典中一次更新多个对象属性时,set() 方法可能很方便

order.set(**dict_with_new_values)

如果您需要在当前数据库会话结束之前将更新保存到数据库,可以使用 flush()commit() 函数。

Pony 始终在执行以下方法之前自动保存累积在 db_session() 缓存中的更改:select()get()exists()execute()commit()

将来,Pony 将支持批量更新。它将允许在不将对象加载到缓存的情况下更新磁盘上的多个对象

update(p.set(price=price * 1.1) for p in Product
                                if p.category.name == "T-Shirt")

删除对象

当您调用实体实例的 delete() 方法时,Pony 会将该对象标记为已删除。该对象将在下次提交时从数据库中删除。

例如,以下是如何删除主键等于 123 的订单

Order[123].delete()

批量删除

Pony 支持使用 delete() 函数对对象进行批量删除。这样,您可以删除多个对象,而无需将它们加载到缓存中

delete(p for p in Product if p.category.name == 'SD Card')
#or
Product.select(lambda p: p.category.name == 'SD Card').delete(bulk=True)

注意

小心使用批量删除

级联删除

当 Pony 删除实体实例时,它还需要删除与其他对象的关联关系。两个对象之间的关联关系由两个关联属性定义。如果关联关系的另一端被声明为 Set,那么我们只需要从该集合中删除该对象。如果另一端被声明为 Optional,那么我们需要将其设置为 None。如果另一端被声明为 Required,我们不能只将 None 分配给该关联属性。在这种情况下,Pony 将尝试对相关对象进行级联删除。

可以使用属性的 cascade_delete 选项更改此默认行为。默认情况下,当关联关系的另一端被声明为 Required 时,此选项设置为 True,而对于所有其他关联关系类型,则设置为 False

如果关联关系在另一端被定义为 Required 并且 cascade_delete=False,那么 Pony 在删除尝试时会引发 ConstraintError 异常。

让我们考虑几个例子。

以下示例在尝试删除与学生相关的组时引发 ConstraintError 异常

class Group(db.Entity):
    major = Required(str)
    items = Set("Student", cascade_delete=False)

class Student(db.Entity):
    name = Required(str)
    group = Required(Group)

在以下示例中,如果 Person 对象具有相关的 Passport 对象,那么如果您尝试删除 Person 对象,Passport 对象也将由于级联删除而被删除

class Person(db.Entity):
    name = Required(str)
    passport = Optional("Passport", cascade_delete=True)

class Passport(db.Entity):
    number = Required(str)
    person = Required("Person")

将对象保存到数据库

通常,您不需要手动将实体实例保存到数据库中 - Pony 会在退出 db_session() 上下文时自动将所有更改提交到数据库。这非常方便。同时,在某些情况下,您可能希望在退出当前数据库会话之前 flush()commit() 数据库中的数据。

如果您需要获取新创建对象的 主键值,您可以在 db_session() 中手动执行 flush() 以获取此值。

class Customer(db.Entity):
    id = PrimaryKey(int, auto=True)
    email = Required(str)

@db_session
def handler(email):
    c = Customer(email=email)
    # c.id is equal to None
    # because it is not assigned by the database yet
    c.flush()
    # c is saved as a table row to the database.
    # c.id has the value now
    print(c.id)

当调用 flush() 时,该对象仅在当前会话中保存。这意味着它将在手动调用 commit()(在大多数情况下并非必需)或在退出当前数据库会话之前自动持久化到数据库。

保存对象的顺序

通常,Pony 按创建或修改对象的顺序将对象保存到数据库中。在某些情况下,如果需要保存对象,Pony 可以重新排序 SQL INSERT 语句。让我们考虑以下示例

from pony.orm import *

db = Database()

class TeamMember(db.Entity):
    name = Required(str)
    team = Optional('Team')

class Team(db.Entity):
    name = Required(str)
    team_members = Set(TeamMember)

db.bind('sqlite', ':memory:')
db.generate_mapping(create_tables=True)
set_sql_debug(True)

with db_session:
    john = TeamMember(name='John')
    mary = TeamMember(name='Mary')
    team = Team(name='Tenacity', team_members=[john, mary])

在上面的示例中,我们创建了两个团队成员,然后创建了一个团队对象,并将团队成员分配给该团队。TeamMember 和 Team 对象之间的关系由 TeamMember 表中的一个列表示

CREATE TABLE "Team" (
  "id" INTEGER PRIMARY KEY AUTOINCREMENT,
  "name" TEXT NOT NULL
)

CREATE TABLE "TeamMember" (
  "id" INTEGER PRIMARY KEY AUTOINCREMENT,
  "name" TEXT NOT NULL,
  "team" INTEGER REFERENCES "Team" ("id")
)

当 Pony 创建 johnmaryteam 对象时,它会理解应该重新排序 SQL INSERT 语句,并首先在数据库中创建 Team 对象的实例,因为这将允许使用团队 ID 来保存 TeamMember 行

INSERT INTO "Team" ("name") VALUES (?)
[u'Tenacity']

INSERT INTO "TeamMember" ("name", "team") VALUES (?, ?)
[u'John', 1]

INSERT INTO "TeamMember" ("name", "team") VALUES (?, ?)
[u'Mary', 1]

保存对象期间的循环链

现在假设我们希望能够将队长分配给团队。为此,我们需要向我们的实体添加几个属性:Team.captain 和反向属性 TeamMember.captain_of

class TeamMember(db.Entity):
    name = Required(str)
    team = Optional('Team')
    captain_of = Optional('Team')

class Team(db.Entity):
    name = Required(str)
    team_members = Set(TeamMember)
    captain = Optional(TeamMember, reverse='captain_of')

以下是创建将队长分配给团队的实体实例的代码

with db_session:
    john = TeamMember(name='John')
    mary = TeamMember(name='Mary')
    team = Team(name='Tenacity', team_members=[john, mary], captain=mary)

当 Pony 尝试执行上面的代码时,它会引发以下异常

pony.orm.core.CommitException: Cannot save cyclic chain: TeamMember -> Team -> TeamMember

为什么会发生这种情况?让我们看看。Pony 看到为了在数据库中保存 johnmary 对象,它需要知道团队的 ID,并尝试重新排序插入语句。但是,为了保存具有已分配 captain 属性的 team 对象,它需要知道 mary 对象的 ID。在这种情况下,Pony 无法解决此循环链,并引发异常。

为了保存这样的循环链,您必须通过添加 flush() 命令来帮助 Pony

with db_session:
    john = TeamMember(name='John')
    mary = TeamMember(name='Mary')
    flush() # saves objects created by this moment in the database
    team = Team(name='Tenacity', team_members=[john, mary], captain=mary)

在这种情况下,Pony 将首先在数据库中保存 johnmary 对象,然后将发出 SQL UPDATE 语句来构建与 team 对象的关系

INSERT INTO "TeamMember" ("name") VALUES (?)
[u'John']

INSERT INTO "TeamMember" ("name") VALUES (?)
[u'Mary']

INSERT INTO "Team" ("name", "captain") VALUES (?, ?)
[u'Tenacity', 2]

UPDATE "TeamMember"
SET "team" = ?
WHERE "id" = ?
[1, 2]

UPDATE "TeamMember"
SET "team" = ?
WHERE "id" = ?
[1, 1]

实体方法

有关详细信息,请参阅 API 参考中的 实体方法 部分。

实体钩子

有关详细信息,请参阅 API 参考中的 实体钩子 部分。

使用 pickle 序列化实体实例

Pony 允许对实体实例、查询结果和集合进行 pickle 操作。如果您想将实体实例缓存到外部缓存(例如 memcache)中,您可能需要使用它。当 Pony 对实体实例进行 pickle 操作时,它会保存除集合之外的所有属性,以避免对大量数据进行 pickle 操作。如果您需要对集合属性进行 pickle 操作,则必须单独对它进行 pickle 操作。示例

>>> from pony.orm.examples.estore import *
>>> products = select(p for p in Product if p.price > 100)[:]
>>> products
[Product[1], Product[2], Product[6]]
>>> import cPickle
>>> pickled_data = cPickle.dumps(products)

现在我们可以将 pickle 的数据放到缓存中。稍后,当我们再次需要我们的实例时,我们可以对它进行 unpickle 操作

>>> products = cPickle.loads(pickled_data)
>>> products
[Product[1], Product[2], Product[6]]

您可以使用 pickle 将对象存储在外部缓存中,以提高应用程序性能。当您对对象进行 unpickle 操作时,Pony 会将它们添加到当前的 db_session() 中,就好像它们是从数据库中加载的一样。Pony 不会检查对象是否在数据库中保持相同状态。