07.与实体实例一起工作

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])

在上面的例子中,我们先创建两个团队成员,然后创建一个团队对象,将团队成员分配给团队,TeamMemberTeam对象之间的关系用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看到,在数据库中保存johnmary对象时,它需要知道团队的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会先将johnmary对象保存在数据库中,然后发出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不会检查对象是否在数据库中保持相同的状态。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,458评论 4 363
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,454评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,171评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,062评论 0 207
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,440评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,661评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,906评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,609评论 0 200
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,379评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,600评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,085评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,409评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,072评论 3 237
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,088评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,860评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,704评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,608评论 2 270