为了做个点赞的功能,已经利用下班时间陆陆续续看了快1周了,先练了一部分ajax,完了发现其实数据库这里的建立也非常重要。
参考了网上做点赞功能的帖子,发现好几篇都用到了ContentType这个框架功能,这篇就来记录一下如何使用
1:ContentType的定义
其实ContentType也是默认的一个类,他的字段一个是app_label,一个是model
他关联的是什么呢?其实就是你项目中所有的app内的model模型,在你第一次进行migrate的时候,他就生成了,而且之后你每次进行models的改动,他都会随之而更新。
来看下在数据库中,contenttype是怎么样的一种数据表存在,他会罗列出所有你项目包含的app,已经在app内被定义的models,当然django项目默认的models也存在于这个表单内
2:应用场景
其实光看定义,我根本不知道这个功能具体可以利用在什么场景下
所以我参考了别人的例子,发现,当一对“多”这个“多”的一侧,会被应用于很多模型的外键时,这个contenttype的框架会让整个数据模型看起来干净很多。
还是不明白的话可以看以下的例子。
比如我们的项目中,允许用户发布文章/评论/照片/视频/状态等等
那按照一般的理念来说,我们会需要建立以下几个模型
class Post(models.Model):
...
class Picture(models.Model):
...
class Comment(models.Model):
...
class Video(models.Model):
...
class Status(models.Model):
...
然后对于目前一般的网站来说,都会支持一部分的社交功能,比如点赞
而且这种点赞功能需要支持在各个功能上,可以给文章点赞,可以给评论点赞,可以给照片点赞,可以给视频点赞,等等。
那势必我们的这个“赞”的模型,会需要设立很多外键,因为任何地方都会需要他。
class Likes(models.Model):
post = models.Foreignkey(Post,....)
comment = models.Foreignkey(Comment,....)
picture = models.Foreignkey(Picture,....)
video = models.Foreignkey(Video,....)
status = models.Foreignkey(Status,....)
是不是发现外键非常得多?看上去一大坨,虽然Likes这个类是一对多关系里的“多”这一侧,但实际上他的模型字段也是广义上的“一”,因为他的外键字段和所连接的模型都是“一对一”建立连接的。
而Django里面的ContentType其实就是起到一个自动一对多的作用,和任何模型都能连接起来,保证了代码的干净。
3:GenericForeignkey的使用
接下来正式开始讲这个Likes的类应该如何设定
首先来看一下这个“自动化”的外键的名字和定义
正如官方文档内所描述的,普通的Foreignkey,只能“指向”单一的模型,而ContentType则可以允许和任意的模型进行连接,非常灵活。
设立这种外键,你需要3个字段
1:设定一个普通外键,连接于ContentType,一般名字叫“content_type”。
这个字段实际上是代码你在Likes这个点赞里面,是给哪个对应的模型在点赞,是文章/评论/视频,或是其他。
2:设立一个PostiveIntegerField的字段,一般名字叫做“object_id”。
以记录所对应的模型的实例的id号,比如我们给一篇文章点赞,这篇文章是Post类里的id为10的文章,那么这个object_id就是这个10。
其实看到这里,应该清楚了,当你有了模型的名字,也告诉了你这个模型的实例的id号,你就可以找出这个实例了。
3:第三个也是最后一个,就是设定这个GenericForeignkey外键了,这个外键需要传入两个参数,就是上面的1和2,如果你为上面2个字段取的名字就是content_type和object_id的话,你可以不需要输入,因为这个字段默认会读取这2个名字。如果你自定义过了,那就需要你手动添加。
4:实例使用
ok,回头我们所需要的应用场景上来,我创建了一个测试模型,叫做TestModel
,另外创建了一个点赞的Likes模型。
其中like_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视图函数,这里以为testmodel点赞为例子写(可以把testmodel想象成一篇博客文章,现在要点赞)
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,我现在要和TestModel这个模型建立连接
total_like:统计这个testmodel实例被点赞的总数
这里需要特别注意的是,content_object是一个GenericForeignkey外键,他不是一个普通意义上的字段,所以你如果使用queryset去进行读取的话,他不能被作为一个条件。
进行搜索读取等工作,你需要用content_type和object来作为条件。
就像上面函数内的Likes.objects.filter(content_type=test_model_ct,object_id=testmodel.id)
最后来看下前端的页面渲染和url路由设置,比较简单
一个点赞按钮+一个点赞总计数
<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,来看一下效果图
我们来看一下,实际数据库中的信息。
请注意到,我点的3个赞,都是给TestModel中id为1的实例点赞,所以这里object_id都是1,而content_type都是13是什么意思呢?就是他是和content_type表内的id为13的这个模型取得了连接,而最后的like_by_id则是我登录过2个不同的账号点赞,所以有不同的用户id号。
我们来看下content_type表内的第13号模型
5:GenericRelation的使用
通过like来查询点赞总数,可以说是正向查询
那是否有办法反向查询呢?答案是有的,就是在TestModel里建立一个GenericRelation的字段,请注意他并不在数据表中真实生成,所以无需migrate。
代码如下
class TestModel(models.Model):
title = models.CharField('测试模型',max_length=30)
like_info = GenericRelation(Likes)
接着修改下views视图函数和前段模板进行测试
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反向查询结果
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>
最后看下效果图,两种结果是一样的,正查和反查。
6: 优化使用
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 的基础上,在加上related_query_name这个选项
定义如下
然后我们看一下,加了related_query_name后,可以从ContentType直接进行查询。
从例子上可以看到,作为ContentType的TaggedItem,在filter里面,可以通过双下划线,查询到连接对象的字段,并可以添加条件,上面例子里的就是用到了包含功能。
下面我们实际操作到自己的例子里
首先修改models
class TestModel(models.Model):
title = models.CharField('测试模型',max_length=30)
like_info = GenericRelation(Likes,related_query_name='testmodels')
再修改views视图函数
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 '})
再上面的例子中,我已经把提取ContentType对应模型以及通过object_id和content_type来查询,整合成了testmodels__id来进行查询。这样代码就简洁得多。
参考资料:
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