12. 進階搜尋 - Django 資料庫查詢¶
Important
更高階的課程!
一旦您開始做更多事情,瞭解 Django 的查詢語言將非常有用 Evennia 中的高階內容。但它並不是嚴格需要開箱即用的,而且可以 第一次閱讀時有點不知所措。因此,如果您是 Python 新手並且 Evennia,請隨意瀏覽本課程,稍後再參考 你獲得了更多的經驗。
我們在上一課中使用的搜尋功能和方法足以滿足大多數情況。 但有時你需要更具體:
你想找到所有
Characters………誰在標記為
moonlit的房間中……並且_誰的 Attribute
lycanthropy等級等於 2………因為他們應該立刻變身為狼人!
原則上,您可以透過現有的搜尋功能結合大量迴圈來實現這一點 和 if 語句。但對於像這樣的非標準的東西,直接查詢資料庫將是 效率更高。
Evennia 使用 Django 處理其與資料庫的連線。 一個 django queryset 代表一個資料庫查詢。人們可以將查詢集新增在一起來建立更複雜的查詢。只有當您嘗試使用查詢集的結果時,它才會真正呼叫資料庫。
建立查詢集的正常方法是透過取得其 .objects 資源來定義要搜尋的實體類,然後對其呼叫各種方法。我們之前見過這種情況的變體:
all_weapons = Weapon.objects.all()
現在這是一個表示 Weapon 的所有例項的查詢集。如果 Weapon 有一個子類別 Cannon 並且我們只想要大砲,我們會這樣做
all_cannons = Cannon.objects.all()
請注意,Weapon 和 Cannon _不同_typeclasses。這意味著您在 all_cannons 中找不到任何 Weapon 型別分類的結果。反之亦然,您不會在 all_weapons 中找到任何 Cannon 型別分類的結果。這可能不是您所期望的。
如果您想要取得具有 typeclass Weapon 的所有實體以及 Weapon 的所有子類,例如 Cannon,則需要使用 _family 型別的查詢:
really_all_weapons = Weapon.objects.all_family()
此結果現在包含 Weapon 和 Cannon 例項(以及任何其他
typeclasses 從 Weapon 任意距離繼承的實體,例如 Musket 或
Sword)。
要透過 Typeclass 以外的其他條件限制搜尋,您需要使用 .filter
(或 .filter_family):
roses = Flower.objects.filter(db_key="rose")
這是一個表示 db_key 等於 "rose" 的所有花的查詢集。
由於這是一個查詢集,您可以繼續新增它;這將充當 AND 條件。
local_roses = roses.filter(db_location=myroom)
我們也可以在一份宣告中寫下這一點:
local_roses = Flower.objects.filter(db_key="rose", db_location=myroom)
我們也可以從結果中.exclude得到一些東西
local_non_red_roses = local_roses.exclude(db_key="red_rose")
需要注意的是,我們還沒有呼叫資料庫!直到我們 實際上嘗試檢查資料庫是否會被呼叫。這裡的 當我們嘗試迴圈資料庫時,它會被呼叫(因為現在我們實際上需要 從中取得結果以便能夠迴圈):
for rose in local_non_red_roses:
print(rose)
從現在開始,查詢集是_evaluated_,我們不能繼續向其中新增更多查詢 - 如果我們想找到其他結果,我們需要建立一個新的查詢集。評估查詢集的其他方法是列印它,將其轉換為帶有 list() 的列表,或嘗試存取其結果。
注意我們如何使用 db_key 和 db_location。這是這些資料庫欄位的實際名稱。依照約定,Evennia 在每個資料庫欄位前面使用 db_。當您使用普通的 Evennia 搜尋幫助程式和物件時,您可以跳過 db_ 但這裡我們直接呼叫資料庫並且需要使用「真實」名稱。
以下是 objects 管理器最常用的方法:
filter- 根據搜尋條件查詢物件清單。如果沒有則給予空查詢集 被發現。get- 查詢單一符合專案 - 如果未找到或找到多個符合項,則會引發異常 成立。all- 取得特定型別的所有例項。filter_family- 與filter類似,但也搜尋所有子類別。get_family- 與get類似,但也搜尋所有子類別。all_family- 與all類似,但也傳回所有子類別的實體。
所有 Evennia 搜尋函式都在底層使用查詢集。
evennia.search_*函式實際上會傳回查詢集(到目前為止我們只是將它們視為列表)。這意味著原則上您可以將.filter查詢新增到evennia.search_object的結果中以進一步細化搜尋。
12.1. 查詢集欄位查詢¶
上面我們發現了玫瑰的db_key "rose"。這是一個_大小寫敏感_的_精確_匹配,因此它不會找到"Rose"。
# this is case-sensitive and the same as =
roses = Flower.objects.filter(db_key__exact="rose"
# the i means it's case-insensitive
roses = Flower.objects.filter(db_key__iexact="rose")
Django 欄位查詢語言使用 __ 類似於 Python 使用 . 存取資源。這是因為函式關鍵字中不允許使用 .。
roses = Flower.objects.filter(db_key__icontains="rose")
這將找到名稱中包含字串 "rose" 的所有花,例如 "roses"、"wild rose" 等。開頭的 i 使搜尋不區分大小寫。其他有用的變體有 __istartswith 和 __iendswith。您也可以使用 __gt、__ge 進行「大於」/「大於或等於」比較(__lt 和 __le 相同)。還有__in:
swords = Weapons.objects.filter(db_key__in=("rapier", "two-hander", "shortsword"))
也可以使用 __ 來存取外部物件,例如 Tags。例如,我們假設
這就是我們辨識法師的方式:
char.tags.add("mage", category="profession")
現在,在這種情況下,我們已經有一個 Evennia 助手來執行此搜尋:
mages = evennia.search_tags("mage", category="profession")
如果您只尋找吸血鬼法師,那麼查詢的內容如下:
sparkly_mages = (
Vampire.objects.filter(
db_tags__db_key="mage",
db_tags__db_category="profession")
)
這將檢視 Vampire 上的 db_tags 欄位並過濾每個 tag 的值
db_key 和 db_category 在一起。
有關更多欄位查詢,請參閱有關該主題的 django 檔案。
12.2. 讓我們抓住那個狼人…¶
我們來看看能不能查詢一開始提到的月光下的狼人 本課的。
首先,我們讓自己和目前位置符合標準,這樣我們就可以測試:
> py here.tags.add("moonlit")
> py me.db.lycanthropy = 2
這是更複雜查詢的範例。我們將把它視為一個例子 可能的。
1from typeclasses.characters import Character
2
3will_transform = (
4 Character.objects
5 .filter(
6 db_location__db_tags__db_key__iexact="moonlit",
7 db_attributes__db_key__iexact="lycanthropy",
8 db_attributes__db_value=2
9 )
10)
第 4 行 我們想要找到
Characters,因此我們訪問Charactertypeclass 上的.objects。我們開始篩選…
第 6 行:…透過存取
db_location欄位(通常這是一個房間)……在該位置,我們得到
db_tags的值(這是一個 many-to-many 資料庫欄位 that we can treat like an object for this purpose; it references all Tags on the location)… and from those
Tags, we looking forTagswhosedb_keyis “monlit” (non-case sensitive).
Line 7: … We also want only Characters with
Attributeswhosedb_keyis exactly"lycanthropy"Line 8 :… at the same time as the
Attribute’sdb_valueis 2.
執行此查詢會使我們新的狼人角色出現在 will_transform 中,因此我們知道要對其進行轉換。成功!
Important
您無法像其他資料型別一樣自由地查詢 Attribute db_value。這是因為 Attributes 可以儲存任何 Python 實體,並且實際上在資料庫端儲存為 strings 。因此,雖然您可以在上面的範例中使用 db_value=2,但您將無法使用 dbvalue__eq=2 或 __lt=2。有關處理屬性的更多資訊,請參閱屬性。
12.3. 使用 OR 或 NOT 的查詢¶
到目前為止,所有範例都使用 AND 關係。 .filter 的引數與 AND 一起新增
(「我們希望 tag 的空間是「monlit」_並且_狼性> 2」)。
對於使用OR和NOT的查詢,我們需要Django的Q物件。它是直接從 Django 匯入的:
from django.db.models import Q
例如,Q 是使用與 .filter 相同的引數建立的物件
Q(db_key="foo")
然後,您可以使用此 Q 例項作為 filter 中的引數:
q1 = Q(db_key="foo")
Character.objects.filter(q1)
# this is the same as
Character.objects.filter(db_key="foo")
Q 的有用之處在於,這些物件可以用特殊符號(位元運運算元)連結在一起:| 對應 OR,& 對應 AND。前面的波形符 ~ 否定 Q 內的表示式,因此
工作原理類似於 NOT。
q1 = Q(db_key="Dalton")
q2 = Q(db_location=prison)
Character.objects.filter(q1 | ~q2)
將獲得所有名為“道爾頓”或_不在監獄中的角色。結果是混合的 道爾頓和非囚犯。
讓我們擴充套件原來的狼人查詢。我們不僅想找到月光下的房間中具有一定等級lycanthropy的所有角色 - 我們決定,如果他們被_新近咬傷_,他們也應該轉身,_無論_他們的狼人等級如何(這樣更戲劇化!)。
假設被咬意味著你將被分配Tag recently_bitten。
這就是我們更改查詢的方式:
from django.db.models import Q
will_transform = (
Character.objects
.filter(
Q(db_location__db_tags__db_key__iexact="moonlit")
& (
Q(db_attributes__db_key="lycanthropy",
db_attributes__db_value=2)
| Q(db_tags__db_key__iexact="recently_bitten")
))
.distinct()
)
這是相當緊湊的。如果這樣寫,可能會更容易看出發生了什麼事:
from django.db.models import Q
q_moonlit = Q(db_location__db_tags__db_key__iexact="moonlit")
q_lycanthropic = Q(db_attributes__db_key="lycanthropy", db_attributes__db_value=2)
q_recently_bitten = Q(db_tags__db_key__iexact="recently_bitten")
will_transform = (
Character.objects
.filter(q_moonlit & (q_lycanthropic | q_recently_bitten))
.distinct()
)
這讀作「在月光照射的房間裡找到所有具有以下特徵的角色」:
Attribute lycanthropy 等於二,或 其中有 Tag
recently_bitten」。使用這樣的 OR- 查詢,可以找到相同的內容
字元透過不同的路徑,所以我們在末尾新增.distinct()。這使得
確保結果中每個角色只有一個例項。
12.4. 註解¶
如果我們想過濾某些不能輕易表示的條件該怎麼辦 物體上的場?一個例子是想要找到只包含_五個或更多物體_的房間。
我們可以這樣做(實際上不要這樣做!):
from typeclasses.rooms import Room
all_rooms = Rooms.objects.all()
rooms_with_five_objects = []
for room in all_rooms:
if len(room.contents) >= 5:
rooms_with_five_objects.append(room)
上面我們得到_all_房間,然後使用list.append()繼續增加正確的房間
房間數量不斷增加。這不是一個好主意,一旦你的資料庫
隨著增長,這將產生不必要的計算密集型。最好查詢一下
直接資料庫
Annotations 允許您在查詢中設定一個“變數”,然後您可以 從查詢的其他部分存取。讓我們像之前一樣做同樣的例子 直接在資料庫中:
1from typeclasses.rooms import Room
2from django.db.models import Count
3
4rooms = (
5 Room.objects
6 .annotate(
7 num_objects=Count('locations_set'))
8 .filter(num_objects__gte=5)
9)
Count 是一個 Django 類,用於計算資料庫中的事物數量。
第6-7行:這裡我們先建立一個型別為
Count的註解num_objects。它會建立一個資料庫內函式來計算資料庫內的結果數量。事實註釋意味著現在num_objects可用於查詢的其他部分。第 8 行 我們對此註解進行過濾,使用名稱
num_objects作為我們的名稱 可以過濾。我們使用num_objects__gte=5,這意味著num_objects應大於或等於5。
註釋可能有點難以理解,但比遍歷 Python 中的所有物件要有效得多。
12.5. F 物件¶
如果我們想在一個模型中比較兩個動態引數呢? 查詢?例如,如果我們不想要 5 個或更多物件,而只想 庫存比 tags 更大的物件(愚蠢的例子,但…)?
這可以使用 Django 的 F 物件。所謂的 F 表示式允許您執行檢視資料庫中每個物件的值的查詢。
from django.db.models import Count, F
from typeclasses.rooms import Room
result = (
Room.objects
.annotate(
num_objects=Count('locations_set'),
num_tags=Count('db_tags'))
.filter(num_objects__gt=F('num_tags'))
)
這裡我們使用 .annotate 建立兩個查詢內「變數」num_objects 和 num_tags。然後我們直接在過濾器中使用這些結果。使用 F() 還可以完全在資料庫內即時計算過濾條件的右側。
12.6. 僅分組並傳回某些屬性¶
假設您使用 tags 來標記屬於某個組織的某人。現在您想要製作一個列表,並且需要一次獲取每個組織的成員數量。
.annotate、.values_list 和 .order_by 查詢集方法對此很有用。通常,當您執行 .filter 時,您傳回的是一堆完整的 typeclass 例項,例如玫瑰或劍。使用 .values_list 您可以選擇僅取回物件的某些屬性。 .order_by 方法最終允許根據某些標準對結果進行排序:
1from django.db.models import Count
2from typeclasses.rooms import Room
3
4result = (
5 Character.objects
6 .filter(db_tags__db_category="organization")
7 .annotate(tagcount=Count('id'))
8 .order_by('-tagcount'))
9 .values_list('db_tags__db_key', "tagcount")
在這裡我們獲取所有的角色…
第 6 行:…有 tag 類別“組織”
第7行:…一路上我們計算為每個組織找到多少個不同的字元(每個
id都是唯一的),並使用.annotate和Count將其儲存在「變數」tagcount中第 8 行:…我們使用此計數按
tagcount的降序對結果進行排序(降序是因為有一個減號,預設是升序,但我們希望最受歡迎的組織排在第一位)。第 9 行:…最後,我們確保只返回我們想要的屬性,即組織 tag 的名稱以及我們為該組織找到的匹配項數量。為此,我們在查詢集上使用
values_list方法。這將立即評估查詢集。
結果將是按匹配數降序排列的元組列表, 格式如下:
[
('Griatch's poets society', 3872),
("Chainsol's Ainneve Testers", 2076),
("Blaufeuer's Whitespace Fixers", 1903),
("Volund's Bikeshed Design Crew", 1764),
("Tehom's Glorious Misanthropes", 1763)
]
12.7. 結論¶
我們在本課程中涵蓋了許多基礎知識,並涵蓋了幾個更複雜的主題。瞭解如何使用 Django 進行查詢是一項強大的技能。