Django 3.2.2 Making queries 進(jìn)行查詢

譯者注: 本文翻譯自Django的官方文檔2.1版本。因此只適用于2.1版本和其它未改動(dòng)本章節(jié)的版本。
文檔盡量全文翻譯,但是以下情況下可能沒(méi)有翻譯:章節(jié)的開頭和結(jié)尾沒(méi)有實(shí)質(zhì)內(nèi)容的引導(dǎo)語(yǔ)和結(jié)束語(yǔ),原文中的所有標(biāo)題、一些斜體和粗體的術(shù)語(yǔ)或鏈接,代碼中的部分注釋,一些和Django本身關(guān)系不太大的技術(shù)細(xì)節(jié)。
文中的部分英文斜體和粗體在原文中是轉(zhuǎn)到相關(guān)章節(jié)的超鏈接,但譯文中沒(méi)做鏈接,后期翻譯的章節(jié)足夠時(shí)會(huì)逐漸添加。
受譯者水平所限,文中一些術(shù)語(yǔ)和專業(yè)的表達(dá)可能有錯(cuò)誤,歡迎通過(guò)簡(jiǎn)信或評(píng)論提出錯(cuò)誤和修改意見(jiàn)。
祝各位Django開發(fā)者和愛(ài)好者閱讀使用愉快!

3.2.2 Making queries

縱觀整個(gè)教程,我們都要依賴下面的模型,這些模型構(gòu)造了一個(gè)網(wǎng)絡(luò)博客應(yīng)用:

from django import models

class Blog(models.Model):
    name = models.CharField(max_length=100)
    tagline = models.TextField()
    
    def __str__(self):
        return self.name
        
class Author(models.Model):
    name = models.CharField(max_length=200)        
    email = models.EmailField()
    
    def __str__(self):
        return self.name
        
class Entry(models.Model):
    blog = models.ForeignKey(Blog)        
    headline = models.CharField(max_length=255)
    body_text = models.TextField()
    pub_date = models.DateField()
    mod_date = models.DateField()
    authors = models.ManyToManyField(Author)
    n_comments = models.IntegerField()
    n_pingbacks = models.IntegerField()
    rating = models.IntegerField()
    
    def __str__(self):
        return self.headline
Creating objects

為創(chuàng)建一個(gè)對(duì)象,使用模型類的關(guān)鍵字參數(shù)來(lái)實(shí)例化它,然后調(diào)用save() 來(lái)保存到數(shù)據(jù)庫(kù)。
假設(shè)模型在文件mysite/blog/models.py中,但這有一個(gè)例子:

>>> from blog.models import Blog
>>> b = Blog(name='Beatles Blog', tagline='All the latest Beatles news.')
>>> b.save()

這會(huì)在SQL中執(zhí)行INSERT語(yǔ)句。Django直到你顯式調(diào)用save() 時(shí)才會(huì)和數(shù)據(jù)庫(kù)交互。
save() 方法沒(méi)有返回值。
亦見(jiàn):
save() 有許多高級(jí)選項(xiàng)這里沒(méi)有提及。見(jiàn)文檔中save() 的完整細(xì)節(jié)。
為一步創(chuàng)建和保存一個(gè)對(duì)象,使用create() 方法。

Saving changes to objects

想保存對(duì)已在數(shù)據(jù)中的對(duì)象的修改,使用save()。
給定一個(gè)已在數(shù)據(jù)庫(kù)中保存的Blog實(shí)例b5,這個(gè)例子改變了它的名字并在數(shù)據(jù)庫(kù)中更新:

>>> b5.name = 'New name'
>>> b5.save()

這執(zhí)行了SQL的UPDATE語(yǔ)句。Django直到你顯式調(diào)用save() 時(shí)才會(huì)和數(shù)據(jù)庫(kù)交互。

Saving ForeignKey and ManyToManyField fields

更新ForeignKey字段和保存普通字段的方式一樣,把一個(gè)正確類型的對(duì)象賦給要求的字段就可以了。這個(gè)例子更新了blog屬性的Entry的實(shí)例entry,假設(shè)數(shù)據(jù)庫(kù)中已經(jīng)保存了合適的EntryBlog實(shí)例:

>>> from blog.models import Entry
>>> entry = Entry.objects.get(pk=1)
>>> cheese_blog = Blog.objects.get(name="Cheddar Talk")
>>> entry.blog = cheese_blog
>>> entry.save()

更新ManyToManyField有些不同——在字段上使用add() 方法來(lái)增加一條關(guān)系的記錄。下例向entry對(duì)象增加了Author實(shí)例joe

>>> from blog.models import Author
>>> joe = Author.objects.create(name="Joe")
>>> entry.authors.add(joe)

為在ManyToManyField中一次添加多條記錄,在一次add() 調(diào)用中傳入多個(gè)參數(shù),如下:

>>> john = Author.objects.create(name="John")
>>> paul = Author.objects.create(name="Paul")
>>> george = Author.objects.create(name="George")
>>> ringo = Author.objects.create(name="Ringo")
>>> entry.authors.add(john, paul, george, ringo)

如果你傳入的對(duì)象類型錯(cuò)誤,Django會(huì)抱怨。

Retrieving objects

為從數(shù)據(jù)庫(kù)獲取對(duì)象,通過(guò)模型類的Manager構(gòu)造一個(gè)QuerySet。
一個(gè)QuerySet代表數(shù)據(jù)庫(kù)對(duì)象的一個(gè)集合。可以包括0個(gè),一個(gè)或更多filters。過(guò)濾器基于給定的參數(shù)縮減了查詢結(jié)果的范圍。在SQL術(shù)語(yǔ)中,一個(gè)QuerySet相當(dāng)于一個(gè)SELECT語(yǔ)句,一個(gè)過(guò)濾器相當(dāng)于一個(gè)限制語(yǔ)句比如WHERELIMIT
通過(guò)使用模型的Manger來(lái)獲取QuerySet。每個(gè)模型有至少一個(gè)Manager,默認(rèn)被稱為objects。直接通過(guò)模型類訪問(wèn)它,像這樣:

>>> Blog.objects
<django.db.models.manager.Manager.object at ...>
>>> b = Blog(name='Foo', tagline='Bar')
>>> b.objects
Traceback:
    ...
AttributeError: "Manager isn't accessible via Blog instance."

注意: Managers只能通過(guò)模型類訪問(wèn),而不是從模型實(shí)例,這使得“表級(jí)別”和“記錄級(jí)別”的操作分離。


Manager是模型的QuerySets的主要來(lái)源。例如Blog.objects.all()返回包含數(shù)據(jù)庫(kù)中Blog所有對(duì)象的QuerySet。

Retrieving all objects

最簡(jiǎn)單的獲取方式是獲取一個(gè)表中所有對(duì)象。使用Manager中的all() 方法。

>>> all_entries = Entry.objects.all()

all()方法返回?cái)?shù)據(jù)庫(kù)中所有對(duì)象的QuerySet。

Retrieving specific objects with filters

兩個(gè)最常用的過(guò)濾條件:

  • filter(**kwargs) 返回新的Queryset包含匹配給定查詢參數(shù)的對(duì)象。
  • exclude(**kwargs) 返回新的Queryset包含排除匹配給定查詢參數(shù)的對(duì)象。

查詢參數(shù)應(yīng)該按照Field lookup中的格式。
例如為獲取2006年的博客入口的QuerySet,這樣使用filter()

Entry.objects.filter(pub_date__year=2006)

使用默認(rèn)管理器類時(shí),它等同于:

Entry.objects.all().filter(pub_date__year=2006)
Chaining filters

QuerySet提煉的結(jié)果還是QuerySet,所以可以鏈?zhǔn)教釤?,比如?/p>

>>> Entry.objects.filter(
...     headline__startswith='What'
... ).exclude(
...     pub_date__gte=datetime.date.today()
... ).filter(
...     pub_date__gte=datetime(2005, 1, 30)
... )
Filtered QuerySets are unique

每次提煉QuerySet時(shí)你都得到了一個(gè)全新的QuerySet,它和之前的QuerySet沒(méi)有關(guān)系。每次提煉都創(chuàng)建了一個(gè)分離且獨(dú)特的QuerySet,它能用來(lái)儲(chǔ)存,使用和復(fù)用。
例子:

>>> q1 = Entry.objects.filter(headline__startswith="What")
>>> q2 = q1.exclude(pub_date__gte=datetime.date.today())
>>> q3 = q1.filter(pub_date__gte=datetime.date.today())

這三個(gè)QuerySet是獨(dú)立的。第一個(gè)是一個(gè)基本的QuerySet辦函了所有包括以'What'開頭的標(biāo)題。第二個(gè)是第一個(gè)的子集,但有一個(gè)篩選,排除了pub_date是今天或是未來(lái)的記錄。第三個(gè)是第一個(gè)的子集,額外的篩選是只要pub_date是今天或未來(lái)的記錄。最初的QuerySet(q1)沒(méi)有受到提煉過(guò)程的影響。

QuerySet are lazy

QuerySets是懶惰的——?jiǎng)?chuàng)建QuerySet的行為不含任何數(shù)據(jù)庫(kù)活動(dòng)。你可以一直堆棧過(guò)濾器,Django直到QuerySet被計(jì)算之前都不會(huì)真的進(jìn)行查詢??匆谎蹖?shí)例:

>>> q = Entry.objects.filter(headline__startswith="What")
>>> q = q.filter(pub_date__lte=datetime.date.today())
>>> q= q.exclude(bod_text__icontains="food")
>>> print(q)

看起來(lái)像是訪問(wèn)了數(shù)據(jù)庫(kù)三次,但是實(shí)際上只訪問(wèn)了一次,在最后一行print(q)的時(shí)候。通常來(lái)說(shuō),QuerySet的結(jié)果在你真的要用到他們之前不會(huì)被取得。當(dāng)你用到時(shí),QuerySet才通過(guò)訪問(wèn)數(shù)據(jù)庫(kù)計(jì)算。對(duì)于更多信息,參見(jiàn)When QuerySets are evaluated

Retrieving a single object with get()

filter()總會(huì)給你一個(gè)QuerySet,即使只有一個(gè)對(duì)象匹配查詢條件。在這種情況下,返回的是只含一個(gè)元素的QuerySet
如果你事先知道只有一個(gè)結(jié)果匹配你的查詢,你可以在Manager上使用get() 方法直接返回對(duì)象。

>>> one_entry = Entry.objects.get(pk=1)

你可以在get()使用任意表達(dá)式,就像使用filter()——詳見(jiàn)Field lookups。
注意在使用get()filter()\[0\]切片是不同的。如果沒(méi)有結(jié)果匹配查詢,get()會(huì)引發(fā)DoesNotExist異常。該異常是查詢語(yǔ)句操作的模型類的一個(gè)屬性。所以上方的Entry對(duì)象沒(méi)有主鍵1的話,Django會(huì)拋出Entry.DoesNotExist。
類似地,Django會(huì)抱怨如果有多余一個(gè)對(duì)象匹配了get() 查詢。在這種情況下,會(huì)拋出MultipleObjectsReturned,仍然是模型類的屬性。

Other QuerySet methods

大多時(shí)候當(dāng)你想從數(shù)據(jù)庫(kù)查找對(duì)象時(shí),你會(huì)使用all(), get(),filter()exclude()。但查詢遠(yuǎn)遠(yuǎn)不止這些;見(jiàn)QuerySet API Reference來(lái)獲得多種QuerySet方法的完整列表。\

Limiting QuerySet

使用Python的數(shù)組切片語(yǔ)法的一部分來(lái)限制QuerySet的特定數(shù)量的結(jié)果。這等同于SQL的LIMITOFFSET語(yǔ)句。
比如,這會(huì)返回前5個(gè)對(duì)象(LIMIT 5):

>>> Entry.objects.all()[:5]

這返回了第六個(gè)到第十個(gè)對(duì)象(OFFSET 5 LIMIT 5):

>>> Entry.objects.all()[5:10]

負(fù)索引(例如:Entry.objects.all()[-1])并不支持。
通常,切片QuerySet返回一個(gè)新的QuerySet,并不會(huì)計(jì)算查詢。唯一的意外是如果你是用了Python切片中的“步數(shù)”參數(shù)時(shí)。比如,下面為了返回前十中每?jī)蓚€(gè)元素的列表會(huì)真的進(jìn)行查詢:

>>> Enrty.objects.all()[:10:2]

為獲得一個(gè)對(duì)象而不是列表(比如:SELECT foo FROM bar LIMIT 1),使用單個(gè)索引而不是切片。例如,這會(huì)在把入口按照標(biāo)題的字母順序排序后返回?cái)?shù)據(jù)庫(kù)中的首個(gè)Entry

>>> Entry.objects.order_by('headline')[0]

這大致上相當(dāng)于:

>>> Entry.objects.order_by('headline')[0:1].get()

但是注意如果匹配對(duì)象不存在,第一個(gè)會(huì)引發(fā)IndexError而第二個(gè)會(huì)引發(fā)DoesNotExist。詳見(jiàn)get()

Field lookups

字段查詢是你如何指定SQL的WHERE語(yǔ)句。他們被指定為QuerySet的方法filter(),exclude()get() 參數(shù)。
基本的查詢語(yǔ)句關(guān)鍵字按照field__lookuptype=value的形式(注意是雙下劃線)。例如:

Entry.objects.filter(pub_date__lte='2006-01-01')

大致上會(huì)被翻譯成SQL語(yǔ)句:

SELECT * FROM blog_entry WHERE pub_date <= '2006-01-01';

這是怎么做到的
Python有能力定義接收任意鍵值對(duì)參數(shù)的函數(shù),能實(shí)時(shí)解析鍵值對(duì)。想知道更多信息,見(jiàn)官方Python教程Keyword Arguments。


查詢中指定的字段必須是模型的字段名。盡管在ForeignKey情況下有例外,你可以指定字段名跟后綴_id。在這種情況下,值參數(shù)應(yīng)該是外鍵模型的主鍵原始值。比如:

>>> Entry.objects.filter(blog_id=4)

如果你傳入了非法的鍵值對(duì),查詢函數(shù)會(huì)拋出TypeError。
數(shù)據(jù)庫(kù)接口支持二十多個(gè)查詢類型:完整的參考可見(jiàn)field lookup reference。為先介紹個(gè)大概,這里舉些常用的例子:

  • exact 一個(gè)完全匹配,例如:
>>> Entry.objects.get(headline__exact="Cat bites dog")

會(huì)形成SQL語(yǔ)句:

SELECT ... WHERE headline = 'Cat bites dog';

如果你沒(méi)有提供查詢類型,也就是說(shuō)查詢參數(shù)沒(méi)有雙下劃線,查詢就會(huì)假設(shè)使用了exact。
例如,下面的兩個(gè)語(yǔ)句是等價(jià)的:

>>> Blog.objects.get(id__exact=14)  # 顯式形式
>>> Blog.objects.get(id=14)         # __exact是隱式的

這是為了方便,因?yàn)?code>exact查詢是最常見(jiàn)的。

  • iexact 一個(gè)大小寫不敏感匹配,查詢:
>>> Blog.objects.get(name__iexact="beatles blog")

這會(huì)匹配一個(gè)標(biāo)題為"Beatles Blog","beatles blog"甚至"BeAtlES blOG"的Blog。

  • contains 大小寫敏感的包含語(yǔ)句。例如:
Entry.objects.get(headlines__contains='Lennon')

大致會(huì)翻譯成:

SELECT ... WHERE headline LIKE '%Lennon%';

注意這會(huì)匹配標(biāo)題'Today Lennon honored'而不是'today lennon honored'。
還有個(gè)大小寫不敏感的版本,icontains。

  • startswith,endswith以...開頭,以...結(jié)尾的獨(dú)立查詢。也有不敏感版本,istartswithiendswith
Lookups that span relationship

Django提供了一種強(qiáng)有力且符合直覺(jué)的方式在查詢中來(lái)跟蹤關(guān)系,自動(dòng)為你處理了SQL的JOIN語(yǔ)句。為跨越表關(guān)系,只要使用跨模型中相關(guān)字段名。用雙下劃線分割,直到你達(dá)到了你想要的字段。
下例獲取了name為'Beatles Blog'的Blog有關(guān)的所有Entry對(duì)象:

>>> Entry.objects.filter(blog__name='Beatles Blog')

跨表的深度可以如你所愿。
它也能反過(guò)來(lái)運(yùn)作。為查詢一個(gè)反向的關(guān)系,使用模型的小寫名就可以。
下例獲取了所有至少有一個(gè)headline包含了‘Lennon’的Entry的Blog對(duì)象:

>>> Blog.objects.filter(entry__headline__contains='Lennon')

如果你在通過(guò)多重關(guān)系進(jìn)行過(guò)濾且其中一個(gè)內(nèi)部關(guān)系模型并沒(méi)有滿足過(guò)濾關(guān)系的值,Django會(huì)處理成一個(gè)空的(所有值為NULL)但有效的對(duì)象。就是說(shuō)不會(huì)拋出錯(cuò)誤:比方說(shuō):

Blog.objects.filter(entry__authors__name='Lennon')

(假設(shè)有一個(gè)相關(guān)聯(lián)的Author模型)。如果沒(méi)有和entry相關(guān)聯(lián)的author,也會(huì)被當(dāng)做沒(méi)有相關(guān)的name,而不是因?yàn)闆](méi)找到author拋出異常。通常這就是你想要的,但有種情況可能會(huì)讓你弄混,就是使用isnull時(shí),也就是:

Blog.objects.filter(entry__authors__name__isnull=True)

這會(huì)返回關(guān)聯(lián)authorname為空的Blog對(duì)象,也會(huì)返回關(guān)聯(lián)entryauthor為空的對(duì)象。如果不想后者也包括在內(nèi),應(yīng)該寫:

Blog.objects.filter(entry__author__isnull=False, entry__authors__name__isnull=True)
Spanning multi-valued relationships

當(dāng)你基于ManyToManyField或反向的ForeignKey時(shí),有兩個(gè)不同類型的過(guò)濾器可能是你想要的。考慮下Blog/Entry關(guān)系(Blog對(duì)Entry是一對(duì)多關(guān)系)。我們可能想找到在標(biāo)題中含有'Lennon'且在2008發(fā)布的文章的博客,也有可能想找到那些博客的文章標(biāo)題中含有'Lennon'或是文章發(fā)布于2008的。因?yàn)槎鄠€(gè)文章都和一個(gè)Blog相關(guān)聯(lián),這兩種查詢都是可能發(fā)生的且在某些情況下都是有意義的。
同樣的情況也會(huì)在ManyToManyField上發(fā)生。比如如果一個(gè)EntryManyToManyField叫做tags,我們可能想找到連接到稱為"music"和"bands"標(biāo)簽的文章,也可能想找到含"music"的文章或是含"public"的文章。
為了處理這兩種情況,Django有一個(gè)處理起來(lái)比較一致的調(diào)用filter() 的方式。一個(gè)filter() 中的所有條件需要同時(shí)滿足。成功的filter() 的調(diào)用會(huì)顯示對(duì)象集合,但還是對(duì)于多值關(guān)系,他們只適用于連接到主模型的對(duì)象,而不是從早先的filter() 調(diào)用開始限制。
聽起來(lái)可能有些迷惑,所以但愿能用個(gè)例子說(shuō)清楚。為了選在同時(shí)含有'Lennon'的標(biāo)題且在2008年發(fā)布的文章的博客,我們這樣寫代碼:

Blog.objects.filter(entry__headline__contains='Lennon', entry__pub_date__year=2008)

為了選擇有標(biāo)題包含'Lennon'的文章和2008年發(fā)布的文章的博客,我們這樣寫:

Blog.objects.filter(entry__headline__contains='Lennon').filter(entry__pub_date__year=2008)

假設(shè)只有一個(gè)博客有各滿足其中一個(gè)條件的文章,而沒(méi)有任何一個(gè)博客有同時(shí)滿足兩個(gè)條件的文章。第一個(gè)查詢不會(huì)返回任何博客,而第二個(gè)查詢會(huì)返回前面提到的那個(gè)博客。
在第二個(gè)例子中,第一個(gè)過(guò)濾器限制了查詢集為所有連接到了標(biāo)題含"Lennon"的文章的博客。第二個(gè)過(guò)濾器限制為連接到發(fā)布時(shí)間為2008年的文章的博客。第二個(gè)過(guò)濾器和第一個(gè)過(guò)濾器可能有也可能沒(méi)有交集。每個(gè)過(guò)濾器過(guò)濾的是Blog元素,而不是Entry元素。


注意:filter() 的跨表行為和exclude() 是不同的。單個(gè)exclude() 查詢不需要指同一個(gè)對(duì)象。
例如,下面的查詢排除了標(biāo)題包含"Lennon"的文章和2008年發(fā)布的文章:

Blog.objects.exclude(
    entry__headline__contains='Lennon',
    entry__pub_date__year=2008,
)

然而,不像使用filter() ,這不會(huì)限制到滿足兩個(gè)條件的文章的博客,為了這么做,也就是說(shuō)為了選擇所有不包含滿足兩個(gè)條件的,需要做兩次查詢:

Blog.objects.exclude(
    entry__in=Entry.objects.filter(
        headline__contains='Lennon',
        pub_date__year=2008,
    ),
)
Filters can reference fields on the model

到目前為止,構(gòu)造的過(guò)濾器都是字段值和常量比較。但是如果你想比較字段值和同模型中其它字段值呢?
Django提供了F expressions來(lái)允許這樣的比較。F()的實(shí)例像是查詢語(yǔ)句時(shí)模型字段的引用。這些應(yīng)用可以用在查詢過(guò)濾器中來(lái)比較同一個(gè)模型實(shí)例的不同字段的值的比較。
比如,為了找出所有博客文章中評(píng)論數(shù)多于轉(zhuǎn)發(fā)數(shù)的,我們構(gòu)造F()對(duì)象來(lái)引用轉(zhuǎn)發(fā)的計(jì)數(shù),然后在查詢中使用F()對(duì)象:

>>> from django.db.models import F
>>> Entry.objects.filter(n_comments_gt=F('n_pingbacks'))

Django支持對(duì)F()對(duì)象的加減乘除求余和冪計(jì)算,參與計(jì)算的可以是常量和F()對(duì)象。為了找到評(píng)論數(shù)多于兩倍轉(zhuǎn)發(fā)數(shù)的文章的博客,修改查詢語(yǔ)句:

>>> Entry.objects.filter(n_comments_gt=F('n_pingbacks') * 2)

為了找出所有文章排名小于轉(zhuǎn)發(fā)和評(píng)論數(shù)之和的文章:

>>> Entry.objects.filter(rating__lt=F('n_comments')+F('n_pingbacks'))

也可以在F()對(duì)象中是用雙下劃線來(lái)跨表。一個(gè)含雙下劃線的F()對(duì)象會(huì)引入必要的聯(lián)表操作來(lái)訪問(wèn)相關(guān)對(duì)象。。比如說(shuō),為了取得所有作者名和博客名相同的文章,我們可以:

>>> Entry.objects.filter(authors__name=F('blog__name'))

對(duì)于日期和日期時(shí)間字段,你可以加減一個(gè)timedelta對(duì)象,下例會(huì)返回所有在發(fā)布后超過(guò)3天又修改的文章:

>>> from datetime import timedelta
>>> Entry.objects.filter(mod_date__gt=F('pub_date') + timedelta(days=3))

F()對(duì)象支持按位運(yùn)算,通過(guò).bitand().bitor()。例如:

>>> F('somefield').bitand(16)
The pk lookup shortcut

為了方便,Django提供一個(gè)pk查詢快捷方式,pk代表"primary key"(主鍵)。
在示例的Blog模型中,主鍵是id字段,所以這三個(gè)語(yǔ)句是等價(jià)的:

>>> Blog.objects.get(id__exact=14)  # 顯式形式
>>> Blog.objects.get(id=14)  # __exact是隱式的
>>> Blog.objects.get(pk=14)  # pk 暗示 id__exact

pk的使用并不僅限于__exact查詢,所有的查詢術(shù)語(yǔ)都能和pk組合形成主鍵的查詢語(yǔ)句:

# Get blogs entries with id 1, 4 and 7
>>> Blog.objects.filter(pk__in=(1,4,7))

# Get all blog entries with id > 14
>>> Blog.objects.filter(pk__gt=14)

pk查詢也可以跨表,比如,這三個(gè)語(yǔ)句是等價(jià)的:

>>> Entry.objects.filter(blog__id__exact=3)  # Explicit form
>>> Entry.objects.filter(blog__id=3)  # __exact是隱式的
>>> Entry.objects.filter(blog__pk=3)  # __pk 暗示了__id__exact
Escaping percent signs and underscores in LIKE statements

等價(jià)于LIKESQL語(yǔ)句的字段查詢(iexact,contains,icontains,startswith,istartswith,endswithiendswith)會(huì)自動(dòng)轉(zhuǎn)義LIKE語(yǔ)句里兩個(gè)特殊的字符——百分號(hào)和下劃線。(在LIKE語(yǔ)句中,百分號(hào)代表多個(gè)字符通配而下劃線代表單個(gè)字符通配。)
例如,為獲得所有包含百分號(hào)的文章,就像其他符號(hào)一樣使用百分號(hào)就行了:

>>> Entry.objects.filter(headline__contains='%')

Django為你處理了引用部分;最終的SQL看起來(lái)是這樣的:

SELECT ... WHERE headline LIKE '%\%%';

下劃線也是同理。下劃線和百分號(hào)的處理都是透明的。

Caching and QuerySet

每個(gè)QuerySet包含一個(gè)緩存來(lái)最小化數(shù)據(jù)庫(kù)訪問(wèn)。理解他是如何工作的能幫你寫出更有效率的代碼。
在一個(gè)新創(chuàng)建的QuerySet中,緩存是空的。第一次QuerySet計(jì)算后,數(shù)據(jù)庫(kù)查詢就進(jìn)行了——Django把查詢結(jié)果保存到QuerySet的緩存中并且返回顯式請(qǐng)求的結(jié)果。接下來(lái)的對(duì)這個(gè)QuerySet計(jì)算會(huì)使用緩存的結(jié)果。
踩坑舉例,下例會(huì)創(chuàng)建兩個(gè)QuerySet,計(jì)算,然后拋棄掉:

>>> print([e.headline for e in Entry.objects.all()])
>>> print([e.pub_date for e in Entry.objects.all()])

這意味著同個(gè)數(shù)據(jù)庫(kù)查詢執(zhí)行了兩次,加重了數(shù)據(jù)庫(kù)負(fù)擔(dān)。還有,有可能兩個(gè)列表的數(shù)據(jù)記錄可能是不相同的,因?yàn)?code>Entry有可能在兩次請(qǐng)求間做了增減。
為避免這個(gè)問(wèn)題,保存QuerySet然后反復(fù)使用:

>>> queryset = Entry.objects.all()
>>> print([e.headline for e in Entry.objects.all()])
>>> print([e.pub_date for e in Entry.objects.all()])
When QuerySets are not cached

查詢集并不總是緩存結(jié)果。當(dāng)只計(jì)算查詢集的一部分時(shí),會(huì)查詢緩存。但是如果沒(méi)有填充完整,接下來(lái)的查詢不會(huì)緩存。就是說(shuō),使用數(shù)組切片或索引的limiting the queryset不會(huì)填充緩存。
例如,重復(fù)獲取查詢對(duì)象的某個(gè)索引每次都會(huì)查詢數(shù)據(jù)庫(kù):

>>> queryset = Entry.objects.all()
>>> print(queryset[5])  # 查詢數(shù)據(jù)庫(kù)
>>> print(queryset[5])  # 再次查詢數(shù)據(jù)庫(kù)

然而如果整個(gè)查詢集被計(jì)算過(guò),就會(huì)檢查緩存了:

>>> queryset = Entry.objects.all()
>>> [entry for entry in queryset]  # 查詢數(shù)據(jù)庫(kù)
>>> print(queryset[5])  # 查詢緩存
>>> print(queryset[5])  # 查詢緩存

下面是其它會(huì)引起查詢集計(jì)算并填充緩存的情況:

>>> [entry for entry in queryset]
>>> bool(queryset)
>>> entry in queryset
>>> list(queryset)

注意:?jiǎn)渭兊卮蛴〔樵兗粫?huì)填充緩存,因?yàn)檎{(diào)用__repr__()只返回整個(gè)查詢集的一部分。

Complex lookups with Q objects

關(guān)鍵字查詢語(yǔ)句——比如說(shuō)filter() 里的——都是“AND”關(guān)系。如果需要更復(fù)雜的查詢(比如說(shuō)OR語(yǔ)句),使用Q objects。
Q object(django.db.models.Q)是一個(gè)用來(lái)包裝關(guān)鍵字參數(shù)的集合。這些關(guān)鍵字可以像上面的字段查詢一樣指定。
比如,這個(gè) Q 對(duì)象包裝了一個(gè)LIKE查詢:

from django.db.models import Q
Q(question__startswith='What')

Q對(duì)象可以使用&|操作符結(jié)合。當(dāng)在兩個(gè)Q對(duì)象間使用操作符時(shí),會(huì)產(chǎn)生一個(gè)新的Q對(duì)象。
比如說(shuō),接下來(lái)的語(yǔ)句產(chǎn)生了一個(gè)Q對(duì)象代表了兩個(gè)"question__startswith"的"OR"關(guān)系:

Q(question__startswith='Who') | Q(question__startswith='What')

這相當(dāng)于下面的SQL WHERE語(yǔ)句:

WHERE question LIKE 'Who%' OR question LIKE 'What%'

Q對(duì)象也能使用~操作符來(lái)表示否定,允許組合正常查詢和一個(gè)否定(NOT)查詢:

Q(question__startswith='Who') | ~Q(pub_date__year=2005)

每個(gè)接收關(guān)鍵字參數(shù)的查詢函數(shù)也能傳送一個(gè)或不止一個(gè)Q對(duì)象作為位置參數(shù)。如果你給一個(gè)查詢函數(shù)提供多個(gè)Q對(duì)象參數(shù),參數(shù)間則為"AND"關(guān)系:

Poll.objects.get(
    Q(question__startswith='Who'),
    Q(pub_date=date(2005, 5, 2) | Q(pub_date=date(2005, 5, 6)))
)

大致可以翻譯成SQL語(yǔ)句:

SELECT * from polls WHERE question LIKE 'Who%'
    AND (pub_date = '2005-05-02' OR pubdate = '2005-05-06')

查詢函數(shù)可以混合使用Q對(duì)象和關(guān)鍵字參數(shù)。所有提供給查詢函數(shù)的參數(shù)都是"AND"關(guān)系。然而如果提供了Q對(duì)象,必須定義在關(guān)鍵字參數(shù)前:

Poll.objects.get(
    Q(pub_date=date(2005, 5, 2) | Q(pub_date=date(2005, 5, 6)),
    question__startswith='Who',
)

上面是一個(gè)合法查詢但是:

# 非法查詢
Poll.objects.get(
    question__startswith='Who',
    Q(pub_date=date(2005, 5, 2)) | Q(pub_date=date(2005, 5, 6))
)

這就是個(gè)非法查詢了。
亦見(jiàn):Django單元測(cè)試?yán)锏?strong>OR lookups examples展示了Q的更多用法。

Comparing objects

為比較兩個(gè)模型實(shí)例,可以使用標(biāo)準(zhǔn)的Python比較操作符,雙等號(hào):==。在后臺(tái),進(jìn)行的是兩個(gè)模型的主鍵值的比較。
使用Entry例,下面兩個(gè)句子是等價(jià)的:

>>> scene_entry == other_entry
>>> some_entry.id == other_entry.id

如果一個(gè)模型的主鍵不叫id,沒(méi)關(guān)系。無(wú)論主鍵叫什么,總是會(huì)拿主鍵進(jìn)行比較。比如,如果一個(gè)模型的主鍵字段叫做name,這兩個(gè)語(yǔ)句是等價(jià)的:

>>> some_obj == other_obj
>>> some_obj.name == other_obj.name
Deleting objects

刪除模型的方法叫做delete()。這個(gè)方法會(huì)立刻刪除對(duì)象并返回對(duì)象名和一個(gè)字典,字典里時(shí)刪除的每個(gè)對(duì)象類型的數(shù)量。比如:

>>> e.delete()
(1, {'weblog.Entry': 1})

返回值增加了刪除對(duì)象的數(shù)量。
也可以批量刪除對(duì)象。每個(gè)QuerySet有一個(gè)delete() 方法,刪除所有QuerySet的成員。
例如,接下來(lái)會(huì)刪除所有pub_date為2005的Entry對(duì)象:

>>> Entry.objects.filter(pub_date__year=2005).delete()
(5, {'webapp.Entry': 5})

記住,在可能的情況下,完全用SQL來(lái)操作,這樣在進(jìn)程中獨(dú)立的對(duì)象實(shí)例就不會(huì)需要調(diào)用delete()方法。如果你為一個(gè)模型類提供了delete()方法并想保證它被調(diào)用了,你需要手動(dòng)刪除每個(gè)實(shí)例(通過(guò)迭代QuerySet并在每個(gè)獨(dú)立對(duì)象上調(diào)用delete())而不是使用QuerySet的批量delete() 方法。
返回值增加了刪除對(duì)象的數(shù)量。
當(dāng)Django刪除對(duì)象時(shí),默認(rèn)情況下它觸發(fā)了SQL限制ON DELETE CASCADE行為——換句話說(shuō),對(duì)象被刪除時(shí),外鍵指向該對(duì)象的對(duì)象也會(huì)被刪除。
例如:

b = Blog.objects.get(pk=1)
# 這會(huì)刪除Blog對(duì)象和所有它的Entry對(duì)象
b.delete()

這個(gè)事務(wù)行為通過(guò)ForeignKeyon_delete參數(shù)定義。
注意delete() 是唯一的沒(méi)有提供給ManagerQuerySet方法。這是防止意外請(qǐng)求Entry.objects.delete()刪除了所有文章的安全機(jī)制。如果你確實(shí)想刪除所有對(duì)象,可以顯示請(qǐng)求一個(gè)完整的查詢集:

Entry.objects.all().delete()
Copying model instances

盡管沒(méi)有復(fù)制實(shí)例的內(nèi)置方法,可以簡(jiǎn)單的復(fù)制所有字段值來(lái)創(chuàng)建新實(shí)例。在最簡(jiǎn)單的情況下,你可以把pk設(shè)為None就行了。使用博客例子:

blog = Blog(name='My blog', tagline='Blogging is easy')
blog.save()  # blog.pk == 1

blog.pk = None
blog.save()  # blog.pk == 2

使用了繼承的情況會(huì)復(fù)雜些。考慮下Blog的子類:

class ThemeBlog(Blog):
    theme = models.CharField(max_length=200)

django_blog = ThemeBlog(name='Django', tagline='Django is easy', theme='python')
django_blog.save()  # django_blog.pk == 3    

因?yàn)槔^承的工作機(jī)制,你必須要把pk和id設(shè)為None:

django_blog.pk = None
django_blog.id = None
django_blog.save()  # django_blog.pk == 4

這個(gè)過(guò)程沒(méi)有復(fù)制不在這個(gè)模型數(shù)據(jù)表里的關(guān)系。比如,Entry有個(gè)鏈接到AuthorManyToManyField的字段。在復(fù)制文章后,你必須手動(dòng)為新文章設(shè)置多對(duì)多關(guān)系:

entry = Entry.objects.all()[0]  # 某個(gè)先前的文章
old_authors = entry.authors.all()
entry.pk = None
entry.save()
entry.authors.set(old_authors)

對(duì)于OneToOneField,你必須復(fù)制相關(guān)的對(duì)象并賦值給新對(duì)象來(lái)避免一對(duì)一關(guān)系的唯一性限制。比如,假設(shè)entry早就復(fù)制過(guò)了:

detail = EntryDetail.objects.all()[0]
detail.pk = None
detail.entry = entry
detail.save()
Updating multiple objects at once

有時(shí)你想把QuerySet里的所有對(duì)象的某個(gè)字段設(shè)置為新的值。你可以通過(guò)update() 方法。比如:

# Update all the headline with pub_date in 2007.
Entry.objects.filter(pub_date__year=2007).update(headline='Everything is the same')

使用這個(gè)方法你只能設(shè)置非關(guān)字段和ForeignKey字段。更新非關(guān)系字段時(shí)給新的值提供常量。更新ForeignKey的字段時(shí),把新值設(shè)為想指定的新的模型實(shí)例。例如:

>>> b = Blog.objects.get(pk=1)

# Change every Entry so that it belongs to this Blog.
>>> Entry.objects.all().update(blog=b)

update()方法立刻生效,并且返回匹配的行數(shù)(但不一定是更新的行數(shù)因?yàn)橛械男兄翟缫咽切轮盗?。更新的QuerySet唯一的限制是它只能訪問(wèn)一個(gè)數(shù)據(jù)庫(kù)表:模型主表。你可以基于關(guān)系字段來(lái)過(guò)濾,但是只能更新模型主表里的列,比如:

>>> b = Blog.objects.get(pk=1)

# Update all the headlines belonging to this Blog.
>>> Entry.objects.select_related().filter(blog=b).update(headline='Everything is the same')

注意update方法會(huì)直接轉(zhuǎn)變成SQL語(yǔ)句。這是直接更新的批量操作。他不會(huì)運(yùn)行模型的save() 方法,或是觸發(fā)pre_savepost_save信號(hào)(他們是調(diào)用save() 的序列),或是接受auto_now() 字段選項(xiàng)。如果你想保存QuerySet里的每個(gè)項(xiàng)目并且確保每個(gè)實(shí)例都調(diào)用了save() 方法。不用特殊的函數(shù)來(lái)做這件事,只要循環(huán)然后調(diào)用save()

for item in my_queryset:
    item.save()

更新也可以使用F expression來(lái)基于同模型的另一個(gè)字段值來(lái)更新字段。這對(duì)于基于字段當(dāng)前值來(lái)增加計(jì)數(shù)尤其有用。比如,為了增加博客中每個(gè)文章的轉(zhuǎn)發(fā)數(shù):

>>> Entry.objects.all().update(n_pingbacks=F('n_pingbacks') + 1)

然而,不像F()在過(guò)濾和排除語(yǔ)句中那樣,在更新中使用F()時(shí)你不能引入聯(lián)表——你只能引用待更新的模型內(nèi)部的字段。如果你想對(duì)F()引入聯(lián)表,就會(huì)拋出FieldError異常:

# This will raise a FieldError
>>> Entry.objects.update(headline=F('blog__name'))
Related objects

當(dāng)在模型中定義關(guān)系后,模型的實(shí)例有很方便的接口訪問(wèn)相關(guān)對(duì)象。
比如使用開頭的模型,一個(gè)Entry對(duì)象e可以獲得相關(guān)聯(lián)的Blog對(duì)象通過(guò)訪問(wèn)blog屬性:e.blog。
Django在關(guān)系的另一邊也創(chuàng)建了接口——從相關(guān)模型到定義關(guān)系模型的。比如一個(gè)Blog對(duì)象b能訪問(wèn)所有相關(guān)的Entry對(duì)象的列表,通過(guò)entry_set屬性:b.entry_set.all()。

One-to-many relationships
Forward

如果一個(gè)模型有一個(gè)ForeignKey,模型實(shí)例就能通過(guò)一個(gè)簡(jiǎn)單的模型屬性訪問(wèn)相關(guān)的外部對(duì)象。
例如:

>>> e = Entry.objects.get(id=2)
>>> e.blog  # 返回相關(guān)的Blog對(duì)象

你可以獲取和設(shè)置外鍵屬性。直到調(diào)用save() 方法時(shí)才會(huì)改動(dòng)外鍵。例子:

>>> e = Entry.objects.get(id=2)
>>> e.blog = some_blog
>>> e.save()

如果ForeignKey字段有null=True設(shè)置(也就是允許NULL值),你可以設(shè)置None來(lái)移除關(guān)系。比如:

>>> e = Entry.objects.get(id=2)
>>> e.blog = None
>>> e.save()  # "UPDATE blog_entry SET blog_id = NULL ... ;"

在第一次一對(duì)多關(guān)系的正向訪問(wèn)相關(guān)對(duì)象時(shí)就緩存了。隨后訪問(wèn)外鍵指向的同一對(duì)象時(shí)都是緩存的。比如:

>>> e = Entry.objects.get(id=2)
>>> print(e.blog)  # 訪問(wèn)數(shù)據(jù)庫(kù)來(lái)獲取相關(guān)博客
>>> print(e.blog)  # 沒(méi)有訪問(wèn)數(shù)據(jù)庫(kù),使用的緩存版本

注意select_related() QuerySet方法提前遞歸的填充所有一對(duì)多關(guān)系的緩存:

>>> e = Entry.objects.select_related().get(id=2)
>>> print(e.blog)  # 沒(méi)有訪問(wèn)數(shù)據(jù)庫(kù),使用緩存
>>> print(e.blog)  # 沒(méi)有訪問(wèn)數(shù)據(jù)庫(kù),使用緩存
Following relationships "backward"

如果一個(gè)模型有ForeignKey,外鍵模型的實(shí)例將訪問(wèn)Manager來(lái)返回第一個(gè)模型的所有實(shí)例。默認(rèn)情況下,這個(gè)管理器稱為FOO_set,FOO是原模型名,小寫。這個(gè)Manager返回QuerySet,可以用上面描述的“取得對(duì)象”部分過(guò)濾和操作。
舉例:

>>> b = Blog.objects.get(id=1)
>>> b.entry_set.all()  # 返回關(guān)聯(lián)博客的所有文章入口

# b.entry_set is a Manager that returns QuerySets.
>>> b.entry_set.filter(headline__contains='Lennon')
>>> b.entry_set.count()

你可以通過(guò)ForeignKey定義中的related_name屬性來(lái)重寫FOO_set名。比如,如果Entry模型改成blog = ForeignKey(Blog, on_delete=models.CASCADE, related_name='entries')上例代表會(huì)變成:

>>> b = Blog.objects.get(id=1)
>>> b.entries.all()  # 返回關(guān)聯(lián)博客的所有文章入口

# b.entries is a Manager that returns QuerySets.
>>> b.entries.filter(headline__contains='Lennon')
>>> b.entries.count()
Using a custom reverse manager

默認(rèn)情況下,用來(lái)反向查詢的RelatedManager是該模型default manager的子類。如果你想為給定的查詢指定不同的管理器,你可以使用下面的語(yǔ)法:

from django.db import models

class Entry(models.Model):
    #...
    objects = models.Manager()  # Default Manager
    entries = EntryManager()    # Custom Manager
    
b = Blog.objects.get(id=1)
b.entry_set(manager='entries').all()

如果EntryManager在它的get_queryset()方法上進(jìn)行了默認(rèn)的過(guò)濾,過(guò)濾會(huì)影響到all()的調(diào)用。
當(dāng)然,指定一個(gè)自定義反向管理器能讓你也是用它的自定義方法:

b.entry_set(manager='entries').is_published()
Additional methods to handle related objects

除了上面在“取回對(duì)象”中定義的QuerySet方法外,ForeignKey Manager還有其他的方法來(lái)處理關(guān)聯(lián)對(duì)象集合。下面是個(gè)總覽,完整列表可以在related objects reference中找到。\

  • add(obj1, obj2, ...) 向相關(guān)對(duì)象集合增加指定的模型對(duì)象。
  • create(**kwargs) 創(chuàng)建一個(gè)新的對(duì)象,保存并把它放進(jìn)相關(guān)對(duì)象集合。返回新創(chuàng)建的對(duì)象。
  • remove(obj1, obj2, ...) 從相關(guān)對(duì)象集合中移除指定的模型對(duì)象。
  • clear() 從相關(guān)對(duì)象集合中移除所有對(duì)象。
  • set(objs) 替代相關(guān)對(duì)象集合。

為指定相關(guān)集合的的成員,使用set()方法,參數(shù)應(yīng)該是可迭代的對(duì)象實(shí)例或者主鍵值的列表,比如:

b = Blog.objects.get(id=1)
b.entry_set.set([e1, e2])

在這個(gè)例子中,e1e2可以是完整的文章實(shí)例,也可以是整數(shù)主鍵值。
如果clear()方法可用,任何在所有可迭代對(duì)象中的對(duì)象加入集合前,先前存在的對(duì)象都會(huì)從entry_set移除。如果clear()方法不可用,所有可迭代對(duì)象中的對(duì)象會(huì)加入集合但是不移除先前存在的元素。
這一部分描述的每個(gè)“反向”操作都是立刻作用于數(shù)據(jù)庫(kù)的。每個(gè)增加,創(chuàng)建,刪除操作都是立刻自動(dòng)存到數(shù)據(jù)庫(kù)的。

Many-to-many relationships

多對(duì)多關(guān)系的兩端都有自動(dòng)化的接口來(lái)訪問(wèn)另一端。接口就像上面“逆向”的一對(duì)多關(guān)系一樣進(jìn)行。
唯一的區(qū)別在于屬性名:定義ManyToManyField的模型使用的是字段名本身,而“反向”模型使用的是原模型的模型名的小寫,追加'_set'。
舉個(gè)例子可能更好理解些:

e = Entry.objects.get(id=3)
e.authors.all()  # Returns all Author objects for this Entry.
e.authors.count()
e.authors.filter(name__contains='John')

a =Author.objects.get(id=5)
a.entry_set.all()  # Returns all Entry objects for this Author.

就像ForeignKey, ManyToManyField可以指定related_name。在上面的例子中,如果Entry中的ManyToManyField制定了related_name='entries',那么每個(gè)Author實(shí)例就會(huì)有個(gè)entries屬性而不是entry_set。

One-to-one relationships

一對(duì)一關(guān)系和多對(duì)一關(guān)系很類似。如果你定義了一個(gè)OneToOneField,模型的實(shí)例能夠通過(guò)一個(gè)簡(jiǎn)單的模型屬性訪問(wèn)相關(guān)對(duì)象。
比如:

class EntryDetail(models.Model):
    entry = models.OneToOneField(Entry, on_delete=models.CASCADE)
    details = models.TextField()

ed = EntryDetail.objects.get(id=2)
ed.entry  # Returns the related Entry object.

區(qū)別在于反向查詢。一對(duì)一關(guān)系的相關(guān)模型也能訪問(wèn)Manager對(duì)象,但是Manager代表了一個(gè)對(duì)象,而不是對(duì)象的集合:

e = Entry.objects.get(id=2)
e.entrydetail  # returns the related EntryDetail object

如果沒(méi)有對(duì)象指定到關(guān)系中,Django會(huì)拋出DoesNotExist異常。
在逆向關(guān)系中指定實(shí)例的方式和正向的關(guān)系的指定一樣:

e.enrtydetail = ed
How are backward relationships possible?

給定的一個(gè)模型是如何得知?jiǎng)e的加載的模型是關(guān)聯(lián)到自己的呢?
答案是app registry。當(dāng)Django啟動(dòng)時(shí),它導(dǎo)入INSTALLED_APPS中的每個(gè)應(yīng)用,然后導(dǎo)入每個(gè)應(yīng)用中的models模塊。當(dāng)一個(gè)新模型類創(chuàng)建時(shí),Django向相關(guān)模型增加反向關(guān)系。如果相關(guān)模型還沒(méi)被導(dǎo)入,Django保持跟蹤關(guān)系并在相關(guān)模型最終導(dǎo)入時(shí)添加關(guān)系。
因此,所有你定義模型的應(yīng)用都要在INSTALLED_APPS中。不然逆向查詢無(wú)法正常工作。

Queries over related objects

包含相關(guān)對(duì)象的查詢遵從普通字段的查詢規(guī)則。當(dāng)指定查詢的值時(shí),你可以使用對(duì)象實(shí)例本身,或是對(duì)象的主鍵值。
例如,如果你有一個(gè)博客對(duì)象bid=5,下面三個(gè)查詢是等價(jià)的:

Entry.objects.filter(blog=b)  # Query using object instance
Entry.objects.filter(blog=b.id)  # Query using id from instance 
Entry.objects.filter(blog=5)  # Query using id directly
Falling back to raw SQL

Django 有很多寫原生SQL查詢的選項(xiàng);見(jiàn)Performing raw SQL queries。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 原文:https://my.oschina.net/liuyuantao/blog/751438 查詢集API 參...
    陽(yáng)光小鎮(zhèn)少爺閱讀 3,963評(píng)論 0 8
  • Django 1.8.2 文檔Home | Table of contents | Index | Modules...
    軒轅小愛(ài)閱讀 2,425評(píng)論 0 2
  • 模塊間聯(lián)系越多,其耦合性越強(qiáng),同時(shí)表明其獨(dú)立性越差( 降低耦合性,可以提高其獨(dú)立性)。軟件設(shè)計(jì)中通常用耦合度和內(nèi)聚...
    riverstation閱讀 2,226評(píng)論 0 8
  • Django ORM用到三個(gè)類:Manager、QuerySet、Model。Manager定義表級(jí)方法(表級(jí)方法...
    廖馬兒閱讀 4,804評(píng)論 1 4
  • 某年某月某日 與某人初見(jiàn) 小城春日舊時(shí)光 可某個(gè)路口 丁香依舊
    筆耕翰墨閱讀 265評(píng)論 0 2

友情鏈接更多精彩內(nèi)容