為了做個點贊的功能,已經(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的改動,他都會隨之而更新。
來看下在數(shù)據(jù)庫中,contenttype是怎么樣的一種數(shù)據(jù)表存在,他會羅列出所有你項目包含的app,已經(jīng)在app內(nèi)被定義的models,當(dāng)然django項目默認(rèn)的models也存在于這個表單內(nèi)
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è)定
首先來看一下這個“自動化”的外鍵的名字和定義
正如官方文檔內(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號。
我們來看下content_type表內(nèi)的第13號模型
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