转载于这里

说明

Django创建项目后,在settings.py中默认加载了以下几个app:

1
2
3
4
5
6
7
8
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
]

其中有个app为django.contrib.contenttypes,在使用了python manage.py makemigrationspython manage.py migrate命令后,这个app会在数据库创建表 django_content_type

django_content_type表存储了用户所提交的所有model名称与model所在的app名称

其中的model字段为用户定义的模型类的名称,app_label为该模型所在app名称

实例

使用的例子,现在有四个model,我们自己创建的Post,Picture和Comment,加上django原生的User。

  • django原生的User model即用户表

  • Comment为用户提交的评论表

  • Picture为存储图片信息的表

  • Post为存储文章信息的表

场景如下:用户可以对图片或者文章提交评论,那么Comment中首先要存储的是,该评论对应的用户,再就是该评论是针对哪张图片或者哪篇文章的评论,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
from django.db import models
from django.contrib.auth.models import User
class Post(models.Model):
author = models.ForeignKey(User, on_delete=models.CASCADE)
body = models.TextField(blank=True)
class Picture(models.Model):
author = models.ForeignKey(User, on_delete=models.CASCADE)
body = models.TextField(blank=True)
class Comment(models.Model):
author = models.ForeignKey(to=User, on_delete=models.CASCADE)
body = models.TextField(blank=True)
pic = models.ForeignKey(to=Picture, on_delete=models.CASCADE, null=True)
post = models.ForeignKey(to=Post, on_delete=models.CASCADE, null=True)

为了建立Comment与Post或者Picture的关系,我们就需要创建外键,如果该Comment与Picture有关联,那么pic字段是有值的,而其余的关联字段为null。这样实现的缺点是,每增加一种被评论的类型,在Comment model中就需要多新增一个外键,扩展性极差。

操作

如果使用contenttypes就不需要再使用多个Foreignkey,因为在django_content_type表已经存储了app名和model名,所以我们只需要将我们创建的模型与django_content_type表关联起来,然后再存储一个实例 id 即可准确定位到某个app中某个模型的具体实例。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from django.db import models
from django.contrib.contenttypes.models import ContentType
from django.contrib.auth.models import User
from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation
class Post(models.Model):
author = models.ForeignKey(User, on_delete=models.CASCADE)
body = models.TextField(blank=True)
comments = GenericRelation('Comment') # 使用GenericRelation可以建立该类与Comment类的反向关联,
# 那么可以通过该类的实例来创建对应的comment
class Picture(models.Model):
author = models.ForeignKey(User, on_delete=models.CASCADE)
body = models.TextField(blank=True)
comments = GenericRelation('Comment')
class Comment(models.Model):
# user_name = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
author = models.ForeignKey(to=User, on_delete=models.CASCADE)
body = models.TextField(blank=True)
# step1
# ForeignKey为外键,即在Comment类中,content_type对应了django_content_type表中的某个对象
content_type = models.ForeignKey(to=ContentType, on_delete=models.CASCADE) # 与数据库中的django_content_type表关联起来
# step2
object_id = models.PositiveIntegerField() # 正整数类型,在被关联的表中的实例id,以此来定位具体的实例
# step3
content_object = GenericForeignKey() # 根据content_type和object_id来指向一个模型中的具体实例

首先看Comment这个模型,与contenttypes相关的就三个字段:content_tpye object_id content_object

content_type字段为外键,指向ContentType这个模型,也就是上面提到的django_content_type

object_id为一个整数,存储了实例id,实例id不理解可以看下面的数据表结构分析

content_objectGenericForeignKey类型,主要作用是根据content_type字段和object_id字段来定位某个模型中具体某个实例

所以实际上在使用GenericForeignKey()函数时需要传入两个参数,即content_typeobject_id字段的名字,注意是名字不是value。但是默认在该函数的构造函数中已经赋予了默认值,即content_typeobject_id,所以如果我们定义的变量名为这两个,那么我们可以省略该参数。该字段不会存储到数据库中,在查询的时候ORM会用到。

GenericForeignKey构造函数如下:

1
2
3
4
5
6
7
def __init__(self, ct_field='content_type', fk_field='object_id', for_concrete_model=True):
self.ct_field = ct_field
self.fk_field = fk_field
self.for_concrete_model = for_concrete_model
self.editable = False
self.rel = None
self.column = None

将模型同步到数据库,并创建几个测试数据,使用manage.py自带的shell可以轻松的创建测试数据。

python manage.py shell

1
2
3
4
5
6
7
8
from app1.models import Post, Picture, Comment  # app1即为你自己所创建的app名
from django.contrib.auth.models import User # django原生用户模型
user = User.objects.get(username='admin-x')
post = Post.objects.create(author=user, body='admin-x post test')
picture = Picture.objects.create(author=user, body='admin-x pic test')

pic = Picture.objects.get(id=1) # 获取id为1的pic对象
c1 = Comment.objects.create(author=user, body='test', content_object=pic) # 创建一个评论并与上述的pic对象关联起来

还可以注意到,在Post和Picture模型中我们定义了comments = GenericRelation('Comment')

GenericRelation的作用是建立该model与Comment的反向关联,我们这样就可以通过Post实例或者Picture实例直接查询属于该实例的所有评论,或者创建一个属于该实例的新评论。

1
2
3
pic = Picture.objects.get(id=1) # 获取id为1的pic对象
pic.comments.create(author=user, body='test02') # 创建一个属于pic对象的评论
pic.comments.all() # 获取属于pic对象的所有评论

实际上在数据表中,根据content_type_id就可以在django_content_type这个表中定位到哪个app的哪个model,然后根据object_id定位到该model中具体实例。