07.與實體實例一起工作
創(chuàng)建一個實體實例
在 Pony 中創(chuàng)建一個實體實例,就像在 Python 中創(chuàng)建一個普通對象一樣:
customer1 = Customer(login="John", password="***",
name="John", email="john@google.com")
在Pony中創(chuàng)建一個對象時,所有的參數(shù)都應(yīng)該作為關(guān)鍵字參數(shù)指定,如果一個屬性有一個默認(rèn)值,可以省略它。
所有創(chuàng)建的實例都屬于當(dāng)前的db_session(),在一些對象-關(guān)系映射器中,你需要調(diào)用對象的save()方法來保存它。
這很不方便,因為程序員必須跟蹤哪些對象被創(chuàng)建或更新了,而且不能忘記在每個對象上調(diào)用save()方法。
Pony會跟蹤哪些對象被創(chuàng)建或更新,并在當(dāng)前db_session()結(jié)束后自動保存在數(shù)據(jù)庫中。
如果你需要在離開db_session()作用域之前保存新創(chuàng)建的對象,可以通過flush()或commit()函數(shù)來完成。
從數(shù)據(jù)庫中加載對象
通過主鍵獲取對象
最簡單的情況是當(dāng)我們想通過主鍵來檢索一個對象。
在Pony中,用戶只需將主鍵放在類名之后的方括號中即可,例如,要提取一個主鍵值為123的客戶,我們可以這樣寫道
customer1 = Customer[123]
同樣的語法也適用于具有復(fù)合主鍵的對象,我們只需要按照實體類描述中定義屬性的順序列出復(fù)合主鍵的元素,用逗號隔開,就可以了。
order_item = OrderItem[order1, product1]
如果不存在這樣的主鍵對象,Pony會引發(fā)ObjectNotFound異常。
通過唯一的屬性組合獲得一個對象
如果你想通過屬性組合來檢索一個對象,你可以使用實體的get()方法。
在大多數(shù)情況下,它用于通過二級唯一性鍵來獲取一個對象,但它也可以用于通過其他任何屬性組合來搜索。
作為get()方法的參數(shù),你需要指定屬性的名稱和值,例如,如果你想接收一個名稱為 "Product 1 "的產(chǎn)品,而你認(rèn)為數(shù)據(jù)庫中只有一個產(chǎn)品在這個名稱下,你可以這么寫:
product1 = Product.get(name='Product1')
如果沒有找到對象,get()返回None,如果找到多個對象,則會拋出MultipleObjectsFoundError異常。
如果對象在數(shù)據(jù)庫中不存在,當(dāng)我們想獲得None而不是ObjectNotFound異常時,你可能想使用帶主鍵的get()方法。
方法get()也可以接收一個lambda函數(shù)作為一個定位參數(shù),這個方法返回的是一個實體的實例,而不是Query類的對象。
獲取多個對象
為了從數(shù)據(jù)庫中檢索多個對象,應(yīng)該使用實體的select()方法,它的參數(shù)是一個lambda函數(shù),它有一個單一的參數(shù),象征著數(shù)據(jù)庫中的一個對象的實例。
在這個函數(shù)中,你可以編寫條件,通過這些條件來選擇對象,例如,如果你想找到所有價格高于100的產(chǎn)品,你可以寫出:
products = Product.select(lambda p: p.price > 100)
這個 lambda 函數(shù)不會在 Python 中執(zhí)行,相反,它將被翻譯成下面的SQL查詢:
SELECT "p"."id", "p"."name", "p"."description",
"p"."picture", "p"."price", "p"."quantity"
FROM "Product" "p"
WHERE "p"."price" > 100
select()方法會返回一個Query類的實例,如果你開始迭代這個對象,SQL查詢將被發(fā)送到數(shù)據(jù)庫中,你將得到實體實例的序列,例如,你可以這樣打印出所有產(chǎn)品的名稱和價格:
for p in Product.select(lambda p: p.price > 100):
print(p.name, p.price)
如果你不想迭代查詢,但只需要得到一個對象的列表,你可以這樣做:
product_list = Product.select(lambda p: p.price > 100) [:]
這里我們從查詢中得到一個完整的切片[:],這相當(dāng)于將查詢轉(zhuǎn)換為一個列表:
product_list = list(Product.select(lambda p: p.price > 100))
在查詢中使用參數(shù)
你可以在查詢中使用變量,Pony將把這些變量作為參數(shù)傳遞給SQL查詢。
Pony中的聲明式查詢語法的一個重要優(yōu)勢是,它提供了完全的保護,防止SQL注入,因為所有的外部參數(shù)都會被正確地轉(zhuǎn)義。
下面是一個例子:
x = 100
products = Product.select(lambda p: p.price > x)
這個SQL查詢可能被轉(zhuǎn)換為下面這個樣子:
SELECT "p"."id", "p"."name", "p"."description",
"p"."picture", "p"."price", "p"."quantity"
FROM "Product" "p"
WHERE "p"."price" > ?
這樣一來,x的值就會作為SQL查詢參數(shù)傳遞,完全消除了SQL注入的風(fēng)險。
排序查詢結(jié)果
如果你需要按照一定的順序?qū)ο筮M行排序,你可以使用Query.order_by()方法。
Product.select(lambda p: p.price > 100).order_by(desc(Product.price))
在這個例子中,我們將所有價格高于100的產(chǎn)品的名稱和價格按降序顯示。
Query對象的方法修改了將被發(fā)送到數(shù)據(jù)庫的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函數(shù)作為參數(shù):
Product.select(lambda p: p.price > 100).order_by(lambda p: desc(p.price))
在lambda函數(shù)中的..code-block::中,Python 方法允許使用高級排序表達(dá)式,例如,這樣就可以按照客戶的訂單總價按降序排序:
Customer.select().order_by(lambda c: desc(sum(c.order.total_price)))
為了按多個屬性對結(jié)果進行排序,你需要用逗號將它們分開,例如,如果你想按價格按降序排序,同時按字母順序顯示價格相似的產(chǎn)品,你可以這樣做:
Product.select(lambda p: p.price > 100).order_by(desc(Product.price), Product.name)
同樣的查詢,但使用lambda函數(shù)將是這樣的。
Product.select(lambda p: p.price > 100).order_by(lambda p: (desc(p.price), p.name))
根據(jù)Python語法,如果從lambda中返回一個以上的元素,需要把它們放到括號中。
限制所選對象的數(shù)量
可以通過使用Query.limit()方法來限制查詢返回的對象數(shù)量,也可以使用更緊湊的Python 切片符號來限制。例如,你可以這樣得到最貴的十個產(chǎn)品:
Product.select().order_by(lambda p: desc(p.price))[:10]
切片的結(jié)果不是查詢對象,而是實體實例的最終列表。
你也可以使用Query.page()方法來方便地對查詢結(jié)果進行分頁:
Product.select().order_by(lambda p: desc(p.price)).page(1)
穿越關(guān)系
在Pony中,你可以遍歷對象關(guān)系:
order = Order[123]
customer = order.customer
print customer.name
Pony試圖盡量減少向數(shù)據(jù)庫發(fā)送的查詢次數(shù)。
為了提高數(shù)據(jù)庫的性能,要盡可能地發(fā)揮每一次數(shù)據(jù)庫查詢的效率,更要盡可能的減少SQL的請求次數(shù)——gthank
在上面的例子中,如果請求的Customer對象已經(jīng)被加載到緩存中,Pony將從緩存中返回該對象而不向數(shù)據(jù)庫發(fā)送查詢。
但是,如果一個對象還沒有加載,Pony仍然不會立即發(fā)送查詢,相反,它將首先創(chuàng)建一個"種子(seed) "對象。
種子是一個只初始化了主鍵的對象,Pony不知道這個對象將被如何使用,而且總是盡可能地只需要主鍵被提供。
在上面的例子中,當(dāng)訪問name屬性時,Pony從數(shù)據(jù)庫中獲取了第三行的對象。通過使用 "種子 "的概念,Pony實現(xiàn)了高效查詢,解決了很多其他映射器的痛點: "N+1 "的問題。
在"to-many "的方向上也可以進行遍歷。
例如,如果你有一個Customer對象,你要循環(huán)遍歷它的訂單屬性,你可以這樣做:
c = Customer[123]
for order in c.orders:
print order.state, order.price
更新一個對象
當(dāng)你給對象屬性賦予新值時,你不需要手動保存每個更新的對象,當(dāng)離開db_session()作用域時,更改將自動保存在數(shù)據(jù)庫中。
例如,為了將主鍵為123的產(chǎn)品數(shù)量增加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())
當(dāng)你需要從字典中一次性更新多個對象屬性時,set()方法很方便。
order.set(**dict_with_new_values)
如果需要在當(dāng)前數(shù)據(jù)庫會話結(jié)束前保存數(shù)據(jù)庫的更新,可以使用flush()或commit()函數(shù)。
Pony總是在執(zhí)行以下方法之前: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")
刪除一個對象
當(dāng)你調(diào)用一個實體實例的delete()方法時,Pony會將該對象標(biāo)記為刪除,在下面的提交中,該對象將被從數(shù)據(jù)庫中刪除。
例如,我們可以這樣刪除一個主鍵等于123的訂單:
Order[123].delete()
批量刪除
Pony支持使用delete()函數(shù)對對象進行批量刪除。這樣你可以刪除多個對象,而不需要將它們加載到緩存中。
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()鉤子不會在被刪除的對象上被調(diào)用。 - 如果一個對象被加載到內(nèi)存中,在批量刪除時不會從db_session()緩存中刪除。
級聯(lián)刪除
當(dāng)Pony刪除一個實體的實例時,它還需要刪除它與其他對象之間的關(guān)系。
兩個對象之間的關(guān)系是由兩個關(guān)系屬性定義的,如果關(guān)系的另一邊聲明為Set,那么我們只需要將該對象從該集合中刪除即可。
如果另一個邊被聲明為Optional,那么我們需要將其設(shè)置為None。
如果另一個邊被聲明為 Required,那么我們不能直接將 None指定為該關(guān)系屬性。
在這種情況下,Pony會嘗試對相關(guān)對象進行級聯(lián)刪除。
這個默認(rèn)行為可以使用屬性的cascade_delete選項來改變,默認(rèn)情況下,當(dāng)關(guān)系的另一邊被聲明為 Required時,這個選項被設(shè)置為 True,而對于所有其他的關(guān)系類型,這個選項被設(shè)置為 False。
如果關(guān)系的另一端被定義為 Required,并且cascade_delete=False,那么 Pony 會在嘗試刪除時引發(fā) ConstraintError 異常。
讓我們考慮幾個例子。
下面的例子在嘗試刪除一個有相關(guān)學(xué)生的組時,會引發(fā)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對象有一個相關(guān)的Passport對象,那么如果你將嘗試刪除Person對象,由于級聯(lián)刪除,Passport對象也會被刪除。
class Person(db.Entity):
name = Required(str)
passport = Optional("Passport", cascade_delete=True)
class Passport(db.Entity):
number = Required(str)
person = Required("Person")
保存數(shù)據(jù)庫中的對象
通常情況下,你不需要手動保存數(shù)據(jù)庫中的實體實例,Pony會在離開db_session()上下文時自動將所有的更改提交到數(shù)據(jù)庫中,這是非常方便的。
同時,在某些情況下,在離開當(dāng)前數(shù)據(jù)庫會話之前,你可能需要在數(shù)據(jù)庫中flush()或commit()數(shù)據(jù)。
如果你需要獲取新創(chuàng)建的對象的主鍵,可以在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會按照創(chuàng)建或修改對象的順序保存數(shù)據(jù)庫中的對象,在某些情況下,如果需要保存對象的話,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])
在上面的例子中,我們先創(chuàng)建兩個團隊成員,然后創(chuàng)建一個團隊對象,將團隊成員分配給團隊,TeamMember和Team對象之間的關(guān)系用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在創(chuàng)建john、mary和team對象時,理解為應(yīng)該重新排序SQL INSERT語句,并先在數(shù)據(jù)庫中創(chuàng)建一個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]
保存對象期間的循環(huán)鏈
現(xiàn)在我們假設(shè)想為一個團隊分配一個隊長,為了這個目的,我們需要在我們的實體中添加幾個屬性:Team.captain和反向?qū)傩?code>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')
這里是創(chuàng)建實體實例的代碼,并為團隊分配了一個隊長:
with db_session:
john = TeamMember(name='John')
mary = TeamMember(name='Mary')
team = Team(name='Tenacity', team_members=[john, mary], captain=mary)
當(dāng)Pony嘗試執(zhí)行上面的代碼時,會產(chǎn)生以下異常:
pony.orm.core.commitException: 無法保存循環(huán)鏈。TeamMember -> Team -> TeamMember
為什么會出現(xiàn)這種情況呢?
我們來看一下,Pony看到,在數(shù)據(jù)庫中保存john和mary對象時,它需要知道團隊的id,并嘗試著重新排序插入語句。
但對于保存有隊長屬性分配的團隊對象,它需要知道m(xù)ary對象的id,在這種情況下,Pony無法解決這個循環(huán)鏈,并提出一個異常。
為了保存這樣一個循環(huán)鏈,你必須通過添加flush()命令來幫助Pony:
with db_session.john = TeamMember(name)
john = TeamMember(name='John')
mary = TeamMember(name='Mary')
flush() # 將此刻創(chuàng)建的對象保存在數(shù)據(jù)庫中
team = Team(name='Tenacity', team_members=[john, mary], captain=mary)
在這種情況下,Pony會先將john和mary對象保存在數(shù)據(jù)庫中,然后發(fā)出SQL UPDATE語句來建立與團隊對象的關(guān)系:
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允許序列化實體實例、查詢結(jié)果和集合。如果你想將實體實例緩存在外部緩存中(如memcache),你可能需要使用它。
當(dāng)Pony 序列化(pickles)實體實例時,它保存了除集合以外的所有屬性,以避免挑出大量的數(shù)據(jù)集。
如果你需要對集合屬性進行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)
現(xiàn)在我們可以把序列化好的數(shù)據(jù)放到緩存中,以后,當(dāng)我們再次需要我們的實例時,我們可以把它解開:
>>> products = cPickle.loads(pickled_data)
>>> products
[Product[1], Product[2], Product[6]]
你可以使用pickling將對象存儲在外部緩存中,以提高應(yīng)用程序的性能。當(dāng)你反序列化對象時,Pony會把它們添加到當(dāng)前的db_session()中,就像剛剛從數(shù)據(jù)庫中加載一樣,Pony不會檢查對象是否在數(shù)據(jù)庫中保持相同的狀態(tài)。