Django 之ContentType和GenericForeignkey使用

為了做個點贊的功能,已經(jīng)利用下班時間陸陸續(xù)續(xù)看了快1周了,先練了一部分ajax,完了發(fā)現(xiàn)其實數(shù)據(jù)庫這里的建立也非常重要。
參考了網(wǎng)上做點贊功能的帖子,發(fā)現(xiàn)好幾篇都用到了ContentType這個框架功能,這篇就來記錄一下如何使用

1:ContentType的定義

其實ContentType也是默認(rèn)的一個類,他的字段一個是app_label,一個是model
他關(guān)聯(lián)的是什么呢?其實就是你項目中所有的app內(nèi)的model模型,在你第一次進(jìn)行migrate的時候,他就生成了,而且之后你每次進(jìn)行models的改動,他都會隨之而更新。


contenttype的定義

來看下在數(shù)據(jù)庫中,contenttype是怎么樣的一種數(shù)據(jù)表存在,他會羅列出所有你項目包含的app,已經(jīng)在app內(nèi)被定義的models,當(dāng)然django項目默認(rèn)的models也存在于這個表單內(nèi)


contenttype數(shù)據(jù)表

2:應(yīng)用場景

其實光看定義,我根本不知道這個功能具體可以利用在什么場景下
所以我參考了別人的例子,發(fā)現(xiàn),當(dāng)一對“多”這個“多”的一側(cè),會被應(yīng)用于很多模型的外鍵時,這個contenttype的框架會讓整個數(shù)據(jù)模型看起來干凈很多。
還是不明白的話可以看以下的例子。

比如我們的項目中,允許用戶發(fā)布文章/評論/照片/視頻/狀態(tài)等等
那按照一般的理念來說,我們會需要建立以下幾個模型

class Post(models.Model):
    ...

class Picture(models.Model):
    ...

class Comment(models.Model):
    ...

class Video(models.Model):
    ...

class Status(models.Model):
    ...

然后對于目前一般的網(wǎng)站來說,都會支持一部分的社交功能,比如點贊
而且這種點贊功能需要支持在各個功能上,可以給文章點贊,可以給評論點贊,可以給照片點贊,可以給視頻點贊,等等。
那勢必我們的這個“贊”的模型,會需要設(shè)立很多外鍵,因為任何地方都會需要他。

class Likes(models.Model):
    post = models.Foreignkey(Post,....)
    comment = models.Foreignkey(Comment,....)
    picture = models.Foreignkey(Picture,....)
    video = models.Foreignkey(Video,....)
    status = models.Foreignkey(Status,....)

是不是發(fā)現(xiàn)外鍵非常得多?看上去一大坨,雖然Likes這個類是一對多關(guān)系里的“多”這一側(cè),但實際上他的模型字段也是廣義上的“一”,因為他的外鍵字段和所連接的模型都是“一對一”建立連接的。
而Django里面的ContentType其實就是起到一個自動一對多的作用,和任何模型都能連接起來,保證了代碼的干凈。

3:GenericForeignkey的使用

接下來正式開始講這個Likes的類應(yīng)該如何設(shè)定
首先來看一下這個“自動化”的外鍵的名字和定義


GenericForeignkey的定義

正如官方文檔內(nèi)所描述的,普通的Foreignkey,只能“指向”單一的模型,而ContentType則可以允許和任意的模型進(jìn)行連接,非常靈活。
設(shè)立這種外鍵,你需要3個字段

1:設(shè)定一個普通外鍵,連接于ContentType,一般名字叫“content_type”。
這個字段實際上是代碼你在Likes這個點贊里面,是給哪個對應(yīng)的模型在點贊,是文章/評論/視頻,或是其他。

2:設(shè)立一個PostiveIntegerField的字段,一般名字叫做“object_id”。
以記錄所對應(yīng)的模型的實例的id號,比如我們給一篇文章點贊,這篇文章是Post類里的id為10的文章,那么這個object_id就是這個10。
其實看到這里,應(yīng)該清楚了,當(dāng)你有了模型的名字,也告訴了你這個模型的實例的id號,你就可以找出這個實例了。

3:第三個也是最后一個,就是設(shè)定這個GenericForeignkey外鍵了,這個外鍵需要傳入兩個參數(shù),就是上面的1和2,如果你為上面2個字段取的名字就是content_type和object_id的話,你可以不需要輸入,因為這個字段默認(rèn)會讀取這2個名字。如果你自定義過了,那就需要你手動添加。

4:實例使用

ok,回頭我們所需要的應(yīng)用場景上來,我創(chuàng)建了一個測試模型,叫做TestModel
,另外創(chuàng)建了一個點贊的Likes模型。
其中l(wèi)ike_by這個字段我是用來定義這篇文章是誰點的贊。

class TestModel(models.Model):
    title = models.CharField('測試模型',max_length=30)

class Likes(models.Model):
    like_by = models.ForeignKey(User,on_delete=models.CASCADE,default=1)
    content_type = models.ForeignKey(ContentType,on_delete=models.CASCADE)
    object_id = models.PositiveIntegerField()
    content_object = GenericForeignKey()

模型建立完了,我們再來寫views視圖函數(shù),這里以為testmodel點贊為例子寫(可以把testmodel想象成一篇博客文章,現(xiàn)在要點贊)

def test_page(request):
    testmodel = TestModel.objects.get(id=1)
    test_model_ct = ContentType.objects.get_for_model(TestModel)
    total_like = len(Likes.objects.filter(content_type=test_model_ct,object_id=testmodel.id))

    if request.method == 'POST':
        like = Likes(content_object=testmodel,like_by=request.user)
        like.save()
        return redirect('test_area:test_page',)
    return render(request,'test_page.html',{'total_like':total_like})

來具體講一下定義的幾個變量的意義和用途
testmodel:取出這個測試實例,也就是將要被點贊的對象
test_model_ct:告訴ContentType,我現(xiàn)在要和TestModel這個模型建立連接
total_like:統(tǒng)計這個testmodel實例被點贊的總數(shù)

這里需要特別注意的是,content_object是一個GenericForeignkey外鍵,他不是一個普通意義上的字段,所以你如果使用queryset去進(jìn)行讀取的話,他不能被作為一個條件。
進(jìn)行搜索讀取等工作,你需要用content_type和object來作為條件。
就像上面函數(shù)內(nèi)的Likes.objects.filter(content_type=test_model_ct,object_id=testmodel.id)

最后來看下前端的頁面渲染和url路由設(shè)置,比較簡單
一個點贊按鈕+一個點贊總計數(shù)

<head>
    <meta charset="UTF-8">
    <title>Test Page</title>
</head>
<body>
<form method="post" action="{% url 'test_area:test_page' %}">
    {% csrf_token %}
    <input type="submit" value="點贊">
</form>
{{total_like}}
</body>
</html>
app_name='test_area'
urlpatterns=[
            path('test_page',test_page,name='test_page'),
]

ok,來看一下效果圖


點贊效果圖

我們來看一下,實際數(shù)據(jù)庫中的信息。
請注意到,我點的3個贊,都是給TestModel中id為1的實例點贊,所以這里object_id都是1,而content_type都是13是什么意思呢?就是他是和content_type表內(nèi)的id為13的這個模型取得了連接,而最后的like_by_id則是我登錄過2個不同的賬號點贊,所以有不同的用戶id號。

Likes表里面建立的連接

我們來看下content_type表內(nèi)的第13號模型

id為13的模型,就是text_area下面的testmodel模型

5:GenericRelation的使用

通過like來查詢點贊總數(shù),可以說是正向查詢
那是否有辦法反向查詢呢?答案是有的,就是在TestModel里建立一個GenericRelation的字段,請注意他并不在數(shù)據(jù)表中真實生成,所以無需migrate。
代碼如下

class TestModel(models.Model):
    title = models.CharField('測試模型',max_length=30)
    like_info = GenericRelation(Likes)

接著修改下views視圖函數(shù)和前段模板進(jìn)行測試

def test_page(request):
    testmodel = TestModel.objects.get(id=1)
    test_model_ct = ContentType.objects.get_for_model(TestModel)
    total_like = len(Likes.objects.filter(content_type=test_model_ct,object_id=testmodel.id))
    total_like_query = len(testmodel.like_info.all())
    #添加total_like_query,通過testmodel反向查詢結(jié)果
    if request.method == 'POST':
        like = Likes(content_object=testmodel,like_by=request.user)
        like.save()
        return redirect('test_area:test_page',)
    return render(request,'test_page.html',{'total_like':total_like,'total_like_query':total_like_query})

前端頁面,添加變量

<body>
<form method="post" action="{% url 'test_area:test_page' %}">
    {% csrf_token %}
    <input type="submit" value="點贊">
</form>
{{total_like}}    <br>
{{total_like_query}}
</body>

最后看下效果圖,兩種結(jié)果是一樣的,正查和反查。


兩種查詢方式

6: 優(yōu)化使用

    testmodel = TestModel.objects.get(id=1)
    test_model_ct = ContentType.objects.get_for_model(TestModel)
    total_like = len(Likes.objects.filter(content_type=test_model_ct,object_id=testmodel.id))

這樣的查詢方式,看上去有些累,先取出model的instance,再查出所代表的ContentyType,最后查出ContentType的instance.
有沒有更簡單的一個方法呢?答案是有的
需要在用到GenericRelation 的基礎(chǔ)上,在加上related_query_name這個選項
定義如下


定義

然后我們看一下,加了related_query_name后,可以從ContentType直接進(jìn)行查詢。

使用實例

從例子上可以看到,作為ContentType的TaggedItem,在filter里面,可以通過雙下劃線,查詢到連接對象的字段,并可以添加條件,上面例子里的就是用到了包含功能。

下面我們實際操作到自己的例子里
首先修改models

class TestModel(models.Model):
    title = models.CharField('測試模型',max_length=30)
    like_info = GenericRelation(Likes,related_query_name='testmodels')

再修改views視圖函數(shù)

def test_ajax(request):
    testmodel = TestModel.objects.filter(id=3).first()
    #testmodel_ct = ContentType.objects.get_for_model(testmodel)
    #like_query = Likes.objects.filter(object_id=2, content_type=testmodel_ct, like_by=request.user.id)
    like_query = Likes.objects.filter(testmodels__id=3,like_by=request.user.id).first()

    if like_query is None:
        like = Likes(content_object=testmodel,like_by=request.user)
        like.save()
        total_like_query = len(testmodel.like_info.all())
        return JsonResponse({'notice':'Thanks for your vote','total_like_query':total_like_query})
    else:
        return JsonResponse({'notice':'You cant vote twice '})

再上面的例子中,我已經(jīng)把提取ContentType對應(yīng)模型以及通過object_id和content_type來查詢,整合成了testmodels__id來進(jìn)行查詢。這樣代碼就簡潔得多。

參考資料:
https://docs.djangoproject.com/en/2.1/ref/contrib/contenttypes/
http://yshblog.com/blog/159
https://django.cowhite.com/blog/where-should-we-use-content-types-and-generic-relations-in-django/
https://micropyramid.com/blog/understanding-genericforeignkey-in-django/
http://dev.cravefood.services/python/django/models/2016/12/07/generic-relations.html

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容