Django 之ContentType和GenericForeignkey使用

为了做个点赞的功能,已经利用下班时间陆陆续续看了快1周了,先练了一部分ajax,完了发现其实数据库这里的建立也非常重要。
参考了网上做点赞功能的帖子,发现好几篇都用到了ContentType这个框架功能,这篇就来记录一下如何使用

1:ContentType的定义

其实ContentType也是默认的一个类,他的字段一个是app_label,一个是model
他关联的是什么呢?其实就是你项目中所有的app内的model模型,在你第一次进行migrate的时候,他就生成了,而且之后你每次进行models的改动,他都会随之而更新。


contenttype的定义

来看下在数据库中,contenttype是怎么样的一种数据表存在,他会罗列出所有你项目包含的app,已经在app内被定义的models,当然django项目默认的models也存在于这个表单内


contenttype数据表

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的类应该如何设定
首先来看一下这个“自动化”的外键的名字和定义


GenericForeignkey的定义

正如官方文档内所描述的,普通的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号。

Likes表里面建立的连接

我们来看下content_type表内的第13号模型

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

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,772评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,458评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,610评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,640评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,657评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,590评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,962评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,631评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,870评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,611评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,704评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,386评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,969评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,944评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,179评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,742评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,440评论 2 342

推荐阅读更多精彩内容