Pony 入门

安装

要安装 Pony,请在命令提示符中输入以下命令

pip install pony

Pony 可以安装在 Python 2.7 或 Python 3 上。如果您要使用 SQLite 数据库,则无需安装其他任何内容。如果您希望使用其他数据库,则需要访问该数据库并安装相应的数据库驱动程序

要确保 Pony 已成功安装,请在交互模式下启动 Python 解释器并输入

>>> from pony.orm import *

这将导入使用 Pony 所需的整个(并且不太大)的类和函数集。最终您可以选择要导入的内容,但我们建议首先使用 import *

如果您不想将所有内容导入全局命名空间,则可以仅导入 orm

>>> from pony import orm

在这种情况下,您不会将所有 Pony 的函数加载到全局命名空间中,但这将要求您使用 orm 作为 Pony 的任何函数和装饰器的前缀。

熟悉 Pony 的最佳方法是在交互模式下试用它。让我们创建一个包含实体类 Person 的示例数据库,向其中添加三个对象,并编写一个查询。

创建数据库对象

Pony 中的实体连接到数据库。这就是为什么我们需要首先创建数据库对象。在 Python 解释器中,输入

>>> db = Database()

定义实体

现在,让我们创建两个实体 - Person 和 Car。Person 实体有两个属性 - name 和 age,Car 实体有属性 make 和 model。在 Python 解释器中,输入以下代码

>>> class Person(db.Entity):
...     name = Required(str)
...     age = Required(int)
...     cars = Set('Car')
...
>>> class Car(db.Entity):
...     make = Required(str)
...     model = Required(str)
...     owner = Required(Person)
...
>>>

我们创建的类是从 Database.Entity 属性派生的 Database 对象。这意味着它们不是普通类,而是实体。实体实例存储在与 db 变量绑定的数据库中。使用 Pony,您可以同时使用多个数据库,但每个实体都属于一个特定的数据库。

在实体 Person 中,我们创建了三个属性 - nameagecarsnameage 是必填属性。换句话说,这些属性不能具有 None 值。 name 是一个字符串属性,而 age 是数字属性。

cars 属性声明为 Set 并且具有 Car 类型。这意味着这是一个关系。它可以保存 Car 实体实例的集合。 "Car" 在这里指定为字符串,因为我们还没有声明实体 Car

Car 实体具有三个必填属性: makemodel 是字符串,而 owner 属性是一对多关系的另一端。Pony 中的关系始终由两个属性定义,它们代表关系的两端。

如果我们需要在两个实体之间创建多对多关系,我们应该在两端声明两个 Set 属性。Pony 会自动创建中间数据库表。

str 类型用于在 Python 3 中表示 Unicode 字符串。Python 2 具有两种字符串类型 - strunicode。从 Pony Release 0.6 开始,您可以对字符串属性使用 strunicode,它们都表示 Unicode 字符串。我们建议对字符串属性使用 str 类型,因为它在 Python 3 中看起来更自然。

如果您需要在交互模式下检查实体定义,可以使用 show() 函数。将实体类或实体实例传递给此函数以打印出定义

>>> show(Person)
class Person(Entity):
    id = PrimaryKey(int, auto=True)
    name = Required(str)
    age = Required(int)
    cars = Set(Car)

您可能会注意到实体获得了一个名为 id 的额外属性。为什么会这样呢?

每个实体都必须包含一个主键,它允许区分一个实体与另一个实体。由于我们没有手动设置主键属性,因此它被自动创建。如果主键是自动创建的,则它被命名为 id 并且具有数字格式。如果主键属性是手动创建的,则可以指定您选择的名称和类型。Pony 还支持复合主键。

当主键是自动创建时,它始终具有设置为 True 的选项 auto。这意味着此属性的值将使用数据库的增量计数器或数据库序列自动分配。

数据库绑定

数据库对象具有 Database.bind() 方法。它用于将声明的实体附加到特定数据库。如果您想在交互模式下试用 Pony,可以使用内存中创建的 SQLite 数据库

>>> db.bind(provider='sqlite', filename=':memory:')

目前 Pony 支持 5 种数据库类型: 'sqlite''mysql''postgresql''cockroach''oracle'。后续参数特定于每个数据库。它们与您通过 DB-API 模块连接到数据库时使用的参数相同。

对于 SQLite,必须将数据库文件名或字符串“:memory:”(如果您使用线程,则为“:sharedmemory:”)指定为参数,具体取决于数据库的创建位置。如果数据库是在内存中创建的,则在 Python 交互式会话结束后将被删除。为了使用存储在文件中的数据库,您可以用以下代码替换上一行

>>> db.bind(provider='sqlite', filename='database.sqlite', create_db=True)

在这种情况下,如果数据库文件不存在,它将被创建。在我们的示例中,我们可以使用在内存中创建的数据库。

如果您使用的是其他数据库,则需要安装特定的数据库适配器。对于 PostgreSQL,Pony 使用 psycopg2。对于 MySQL,可以使用 MySQLdb 或 pymysql 适配器。对于 Oracle,Pony 使用 cx_Oracle 适配器。

以下是如何连接到数据库的示例

# SQLite
db.bind(provider='sqlite', filename=':sharedmemory:')
# or
db.bind(provider='sqlite', filename='database.sqlite', create_db=True)

# PostgreSQL
db.bind(provider='postgres', user='', password='', host='', database='')

# MySQL
db.bind(provider='mysql', host='', user='', passwd='', db='')

# Oracle
db.bind(provider='oracle', user='', password='', dsn='')

# CockroachDB
db.bind(provider='cockroach', user='', password='', host='', database='', )

将实体映射到数据库表

现在我们需要创建数据库表来持久化我们的数据。为此,我们需要在 Database 对象上调用 generate_mapping() 方法

>>> db.generate_mapping(create_tables=True)

参数 create_tables=True 表示,如果表不存在,则将使用 CREATE TABLE 命令创建它们。

在调用 generate_mapping() 方法之前,必须定义所有连接到数据库的实体。

使用调试模式

使用 set_sql_debug() 函数,您可以查看 Pony 发送到数据库的 SQL 命令。要开启调试模式,请键入以下代码

>>> set_sql_debug(True)

如果此命令在调用 generate_mapping() 方法之前执行,那么在创建表期间,您将看到用于生成它们的 SQL 代码。

创建实体实例

现在,让我们创建五个对象来描述三个人和两辆车,并将这些信息保存到数据库中

>>> p1 = Person(name='John', age=20)
>>> p2 = Person(name='Mary', age=22)
>>> p3 = Person(name='Bob', age=30)
>>> c1 = Car(make='Toyota', model='Prius', owner=p2)
>>> c2 = Car(make='Ford', model='Explorer', owner=p3)
>>> commit()

Pony 不会立即将对象保存到数据库中。这些对象只有在调用 commit() 函数后才会被保存。如果调试模式已开启,那么在 commit() 期间,您将看到五个发送到数据库的 INSERT 命令。

db_session

与数据库交互的代码必须放在数据库会话中。当您使用 Python 的交互式 shell 时,您无需担心数据库会话,因为 Pony 会自动维护它。但是,当您在应用程序中使用 Pony 时,所有数据库交互都应该在数据库会话中进行。为此,您需要使用 db_session() 装饰器来包装与数据库交互的函数

@db_session
def print_person_name(person_id):
    p = Person[person_id]
    print p.name
    # database session cache will be cleared automatically
    # database connection will be returned to the pool

@db_session
def add_car(person_id, make, model):
    Car(make=make, model=model, owner=Person[person_id])
    # commit() will be done automatically
    # database session cache will be cleared automatically
    # database connection will be returned to the pool

当函数退出时,db_session() 装饰器会执行以下操作

  • 如果函数引发异常,则执行事务回滚

  • 如果数据已更改且未发生异常,则提交事务

  • 将数据库连接返回到连接池

  • 清除数据库会话缓存

即使函数只是读取数据而不进行任何更改,也应该使用 db_session() 来将连接返回到连接池。

实体实例仅在 db_session() 中有效。如果您需要使用这些对象渲染 HTML 模板,则应该在 db_session() 中进行。

使用数据库的另一种方法是使用 db_session() 作为上下文管理器,而不是装饰器

with db_session:
    p = Person(name='Kate', age=33)
    Car(make='Audi', model='R8', owner=p)
    # commit() will be done automatically
    # database session cache will be cleared automatically
    # database connection will be returned to the pool

编写查询

现在我们已经有了数据库,其中保存了五个对象,我们可以尝试一些查询。例如,以下查询返回年龄大于 20 岁的所有人的列表

>>> select(p for p in Person if p.age > 20)
<pony.orm.core.Query at 0x105e74d10>

函数 select() 将 Python 生成器转换为 SQL 查询,并返回 Query 类的实例。当我们开始遍历查询时,此 SQL 查询将被发送到数据库。获取对象列表的一种方法是对其应用切片运算符 [:]

>>> select(p for p in Person if p.age > 20)[:]

SELECT "p"."id", "p"."name", "p"."age"
FROM "Person" "p"
WHERE "p"."age" > 20

[Person[2], Person[3]]

作为结果,您可以看到发送到数据库的 SQL 查询文本以及提取的对象列表。当我们打印查询结果时,实体实例由实体名称及其主键表示,用方括号括起来,例如 Person[2]

要对结果列表进行排序,您可以使用 Query.order_by() 方法。如果您只需要结果集的一部分,可以使用切片运算符,与您在 Python 列表中使用的方式完全相同。例如,如果您想按姓名对所有人员进行排序并提取前两个对象,您可以这样做

>>> select(p for p in Person).order_by(Person.name)[:2]

SELECT "p"."id", "p"."name", "p"."age"
FROM "Person" "p"
ORDER BY "p"."name"
LIMIT 2

[Person[3], Person[1]]

有时,在交互模式下工作时,您可能希望查看所有对象属性的值。为此,您可以使用 Query.show() 方法

>>> select(p for p in Person).order_by(Person.name)[:2].show()

SELECT "p"."id", "p"."name", "p"."age"
FROM "Person" "p"
ORDER BY "p"."name"
LIMIT 2

id|name|age
--+----+---
3 |Bob |30
1 |John|20

方法 Query.show() 不会显示“多对多”属性,因为它需要向数据库发送额外的查询,并且可能很庞大。这就是为什么您在上面看不到有关相关汽车的信息的原因。但是,如果实例具有“一对一”关系,那么它将被显示

>>> Car.select().show()
id|make  |model   |owner
--+------+--------+---------
1 |Toyota|Prius   |Person[2]
2 |Ford  |Explorer|Person[3]

如果您不想获取对象列表,而是需要遍历结果序列,可以使用 for 循环,而无需使用切片运算符

>>> persons = select(p for p in Person if 'o' in p.name)
>>> for p in persons:
...     print p.name, p.age
...
SELECT "p"."id", "p"."name", "p"."age"
FROM "Person" "p"
WHERE "p"."name" LIKE '%o%'

John 20
Bob 30

在上面的示例中,我们获取了所有姓名属性包含字母“o”的 Person 对象,并显示了该人的姓名和年龄。

查询不一定必须返回实体对象。例如,您可以获取一个列表,其中包含对象属性

>>> select(p.name for p in Person if p.age != 30)[:]

SELECT DISTINCT "p"."name"
FROM "Person" "p"
WHERE "p"."age" <> 30

[u'John', u'Mary']

或元组列表

>>> select((p, count(p.cars)) for p in Person)[:]

SELECT "p"."id", COUNT(DISTINCT "car-1"."id")
FROM "Person" "p"
  LEFT JOIN "Car" "car-1"
    ON "p"."id" = "car-1"."owner"
GROUP BY "p"."id"

[(Person[1], 0), (Person[2], 1), (Person[3], 1)]

在上面的示例中,我们获取了一个元组列表,其中包含一个 Person 对象以及他们拥有的汽车数量。

使用 Pony,您还可以运行聚合查询。以下是一个查询示例,它返回人的最大年龄

>>> print max(p.age for p in Person)
SELECT MAX("p"."age")
FROM "Person" "p"

30

在本手册的后续部分,您将看到如何编写更复杂的查询。

获取对象

要通过主键获取对象,您需要在方括号中指定主键值

>>> p1 = Person[1]
>>> print p1.name
John

您可能会注意到没有查询被发送到数据库。这是因为此对象已经存在于数据库会话缓存中。缓存减少了需要发送到数据库的请求数量。

要通过其他属性检索对象,可以使用 Entity.get() 方法

>>> mary = Person.get(name='Mary')

SELECT "id", "name", "age"
FROM "Person"
WHERE "name" = ?
[u'Mary']

>>> print mary.age
22

在这种情况下,即使对象已经加载到缓存中,查询仍然必须发送到数据库,因为 name 属性不是唯一键。只有当我们通过主键或唯一键查找对象时,才会使用数据库会话缓存。

您可以将实体实例传递给 show() 函数,以显示实体类和属性值。

>>> show(mary)
instance of Person
id|name|age
--+----+---
2 |Mary|22

更新对象

>>> mary.age += 1
>>> commit()

Pony 会跟踪所有更改的属性。当 commit() 函数执行时,当前事务中更新的所有对象都将保存到数据库中。Pony 只保存那些在数据库会话期间更改的属性。

编写原始 SQL 查询

如果您需要通过原始 SQL 查询选择实体,您可以这样做。

>>> x = 25
>>> Person.select_by_sql('SELECT * FROM Person p WHERE p.age < $x')

SELECT * FROM Person p WHERE p.age < ?
[25]

[Person[1], Person[2]]

如果您想直接与数据库交互,避免使用实体,可以使用 Database.select() 方法。

>>> x = 20
>>> db.select('name FROM Person WHERE age > $x')
SELECT name FROM Person WHERE age > ?
[20]

[u'Mary', u'Bob']

Pony 示例

您可以查看 Pony 分发包中的示例,而不是手动创建模型。

>>> from pony.orm.examples.estore import *

您可以在这里看到此示例的数据库图表:https://editor.ponyorm.com/user/pony/eStore

在第一次导入时,将创建一个包含所有必要表的 SQLite 数据库。为了用数据填充它,您需要调用以下函数。

>>> populate_database()

此函数将创建对象并将它们放置在数据库中。

创建对象后,您可以尝试一些查询。例如,以下是如何显示我们拥有最多客户的国家/地区。

>>> select((customer.country, count(customer))
...        for customer in Customer).order_by(-2).first()

SELECT "customer"."country", COUNT(DISTINCT "customer"."id")
FROM "Customer" "customer"
GROUP BY "customer"."country"
ORDER BY 2 DESC
LIMIT 1

在此示例中,我们按国家/地区对对象进行分组,按第二列(客户数量)以反向顺序排序,然后提取第一行。

您可以在 test_queries() 函数中找到更多查询示例,该函数位于 pony.orm.examples.estore 模块中。