07.与实体实例一起工作
创建一个实体实例
在 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()
方法的参数,你需要指定属性的名称和值,例如,如果你想接收一个名称为 "Product 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))
在lambda函数中的..code-block::
中,Python 方法允许使用高级排序表达式,例如,这样就可以按照客户的订单总价按降序排序:
Customer.select().order_by(lambda c: desc(sum(c.order.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试图尽量减少向数据库发送的查询次数。
为了提高数据库的性能,要尽可能地发挥每一次数据库查询的效率,更要尽可能的减少SQL的请求次数——gthank
在上面的例子中,如果请求的Customer对象已经被加载到缓存中,Pony将从缓存中返回该对象而不向数据库发送查询。
但是,如果一个对象还没有加载,Pony仍然不会立即发送查询,相反,它将首先创建一个"种子(seed) "
对象。
种子是一个只初始化了主键的对象,Pony不知道这个对象将被如何使用,而且总是尽可能地只需要主键被提供。
在上面的例子中,当访问name
属性时,Pony从数据库中获取了第三行的对象。通过使用 "种子 "的概念,Pony实现了高效查询,解决了很多其他映射器的痛点: "N+1 "的问题。
在"to-many "
的方向上也可以进行遍历。
例如,如果你有一个Customer
对象,你要循环遍历它的订单属性,你可以这样做:
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总是在执行以下方法之前:select()、get()、resores()、execute()和commit()
,自动保存在db_session()
缓存中积累的修改。
在未来,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)
在进行批量删除时要小心:
-
before_delete()
和after_delete()
钩子不会在被删除的对象上被调用。 - 如果一个对象被加载到内存中,在批量删除时不会从db_session()缓存中删除。
级联删除
当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()
中手动进行commit()
,以获取这个值:
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
commit()
# the new object is persisted in the database
# c.id has the value now
print(c.id)
保存对象的顺序
通常情况下,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在创建john、mary和team
对象时,理解为应该重新排序SQL INSERT
语句,并先在数据库中创建一个Team
对象的实例,因为这样可以使用teamid
来保存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: 无法保存循环链。TeamMember -> Team -> TeamMember
为什么会出现这种情况呢?
我们来看一下,Pony看到,在数据库中保存john
和mary
对象时,它需要知道团队的id,并尝试着重新排序插入语句。
但对于保存有队长属性分配的团队对象,它需要知道mary对象的id,在这种情况下,Pony无法解决这个循环链,并提出一个异常。
为了保存这样一个循环链,你必须通过添加flush()命令来帮助Pony:
with db_session.john = TeamMember(name)
john = TeamMember(name='John')
mary = TeamMember(name='Mary')
flush() # 将此刻创建的对象保存在数据库中
team = Team(name='Tenacity', team_members=[john, mary], captain=mary)
在这种情况下,Pony会先将john
和mary
对象保存在数据库中,然后发出SQL UPDATE
语句来建立与团队对象的关系:
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参考中的Entity方法部分。
实体钩子
详情请参见API参考中的Entity hooks部分。
使用pickle对实体实例进行序列化
Pony允许序列化实体实例、查询结果和集合。如果你想将实体实例缓存在外部缓存中(如memcache),你可能需要使用它。
当Pony 序列化(pickles)实体实例时,它保存了除集合以外的所有属性,以避免挑出大量的数据集。
如果你需要对集合属性进行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)
现在我们可以把序列化好的数据放到缓存中,以后,当我们再次需要我们的实例时,我们可以把它解开:
>>> products = cPickle.loads(pickled_data)
>>> products
[Product[1], Product[2], Product[6]]
你可以使用pickling
将对象存储在外部缓存中,以提高应用程序的性能。当你反序列化对象时,Pony会把它们添加到当前的db_session()
中,就像刚刚从数据库中加载一样,Pony不会检查对象是否在数据库中保持相同的状态。
网友评论