通常情况下如果一张表与另一张表有外键关系,我们只需要使用foreignkey即可以做到,但如果一张表与很多张表有外键关系呢?不停的加外键吗?很显然不应该是这样的,事实上django已经提供了这样的组件,通用关系组件:contenttype.
ContentType是Django的内置的一个应用,可以追踪项目中所有的APP和model的对应关系,并记录在ContentType表中。当我们的项目做数据迁移后,会有很多django自带的表,其中就有django_content_type表
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
]
ContentType组件应用:
-- 在model中定义ForeignKey字段,并关联到ContentType表,通常这个字段命名为content-type
-- 在model中定义PositiveIntergerField字段, 用来存储关联表中的主键,通常我们用object_id
-- 在model中定义GenericForeignKey字段,传入上面两个字段的名字
-- 方便反向查询可以定义GenericRelation字段
下面看一个demo, 有两张表:文章表,评论表,在最开始阶段,我们通过外键来关联。
一篇文章可以有很多评论,所以我把外键放在评论表:
# blog.models.py
from django.utils import timezone
from django.db import models
from ckeditor_uploader.fields import RichTextUploadingField
# Create your models here.
class Article(models.Model):
title = models.CharField('Title', max_length=70)
content = RichTextUploadingField('content', config_name='my_config')
created_time = models.DateTimeField('created time', default=timezone.now)
read = models.PositiveIntegerField('read times', default=0)
def __str__(self):
return self.title
#comment.models.py
from django.db import models
# Create your models here.
class Comment(models.Model):
# 评论
objects = models.Manager() # 指定管理器
name = models.CharField('name', max_length=30)
email = models.EmailField('email', max_length=100)
content = models.TextField('content', )
created_time = models.DateTimeField('created time', auto_now_add=True)
article = models.ForeignKey(to='Article', to_field='id', on_delete=models.CASCADE)
def __str__(self):
return self.name
在很显然,在这种简单的情况下,不使用contenttype也能实现功能查询某文章评论的功能。
下面使用contenttype:
更改comment
from django.db import models
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
# Create your models here.
class Comment(models.Model):
# 评论
objects = models.Manager() # 指定管理器
name = models.CharField('name', max_length=30)
email = models.EmailField('email', max_length=100)
content = models.TextField('content', )
created_time = models.DateTimeField('created time', auto_now_add=True)
content_type = models.ForeignKey(ContentType, on_delete=models.DO_NOTHING)
object_id = models.PositiveIntegerField()
content_object = GenericForeignKey('content_type', 'object_id')
def __str__(self):
return self.name
表面上看comment已经与articel脱钩了,没有任何关系了,但是通过contenttype它却可以与任意的model建立关系。
同步数据库后,我们要怎么获取id为一的文章的评论呢?django shell中操作如下:
In [1]: from django.contrib.contenttypes.models import ContentType
In [2]: from comment.models import Comment
In [3]: from blog.models import Article
In [4]: article_obj = Article.objects.get(id=1)
In [5]: article_content_type = ContentType.objects.get_for_model(article_obj)
In [6]: comment_list = Comment.objects.filter(content_type=article_content_type, object_id=1)
In [7]: list(comment_list)
Out[7]: [<Comment: fafafad>]
原理:每个app注册后都会在django的contentType这个app中注册一个,id, app_label, 以及model
id app_label model
1 admin logentry
2 auth permission
3 auth group
4 auth user
5 contenttypes contenttype
6 sessions session
7 blog article
8 comment comment
我们来看上面的查询 语句:
article_obj = Article.objects.get(id=1)
首先,我们获取了一个article对象,接着我们获取article对象的contenttype
article_content_type = ContentType.objects.get_for_model(article_obj)
In [8]: article_content_type
Out[8]: <ContentType: article>
可以看到它从contentType表中找到了article model,当然这还不够,要找到具体的article,还需要article id,也就是指定object_id
在这里contentType充当中间商的角色:
文章和评论彼此不认识,但中间商知道文章,也知道评论,并且有他们两个的电话号码:通过这个中间商可以将任意的model之间相互联系起来:
因为每个app,以及app中的model都会在contentType中注册一下,相当于留下了自己的电话号码。
源码分析:
contentTypeManager即我们平时使用的model后面的objects,在django中每个model默认的manager指定为objects = modelsManager()
比如Article中, 默认的名字就叫objects,你也可能 对Objects赋值,使用自定义的管理器。
我们看看ContentType的管理器:(去年不重要的部分)
class ContentTypeManager(models.Manager):
use_in_migrations = True
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Cache shared by all the get_for_* methods to speed up
# ContentType retrieval.
self._cache = {}
def _get_from_cache(self, opts):
key = (opts.app_label, opts.model_name)
return self._cache[self.db][key]
def get_for_model(self, model, for_concrete_model=True):
"""
Return the ContentType object for a given model, creating the
ContentType if necessary. Lookups are cached so that subsequent lookups
for the same model don't hit the database.
"""
opts = self._get_opts(model, for_concrete_model)
try:
return self._get_from_cache(opts)
except KeyError:
pass
# The ContentType entry was not found in the cache, therefore we
# proceed to load or create it.
try:
# Start with get() and not get_or_create() in order to use
# the db_for_read (see #20401).
ct = self.get(app_label=opts.app_label, model=opts.model_name)
except self.model.DoesNotExist:
# Not found in the database; we proceed to create it. This time
# use get_or_create to take care of any race conditions.
ct, created = self.get_or_create(
app_label=opts.app_label,
model=opts.model_name,
)
self._add_to_cache(self.db, ct)
return ct
def get_for_models(self, *models, for_concrete_models=True):
"""
Given *models, return a dictionary mapping {model: content_type}.
"""
results = {}
# Models that aren't already in the cache.
needed_app_labels = set()
needed_models = set()
# Mapping of opts to the list of models requiring it.
needed_opts = defaultdict(list)
for model in models:
opts = self._get_opts(model, for_concrete_models)
try:
ct = self._get_from_cache(opts)
except KeyError:
needed_app_labels.add(opts.app_label)
needed_models.add(opts.model_name)
needed_opts[opts].append(model)
else:
results[model] = ct
if needed_opts:
# Lookup required content types from the DB.
cts = self.filter(
app_label__in=needed_app_labels,
model__in=needed_models
)
for ct in cts:
model = ct.model_class()
opts_models = needed_opts.pop(ct.model_class()._meta, [])
for model in opts_models:
results[model] = ct
self._add_to_cache(self.db, ct)
# Create content types that weren't in the cache or DB.
for opts, opts_models in needed_opts.items():
ct = self.create(
app_label=opts.app_label,
model=opts.model_name,
)
self._add_to_cache(self.db, ct)
for model in opts_models:
results[model] = ct
return results
def get_for_id(self, id):
"""
Lookup a ContentType by ID. Use the same shared cache as get_for_model
(though ContentTypes are obviously not created on-the-fly by get_by_id).
"""
try:
ct = self._cache[self.db][id]
except KeyError:
# This could raise a DoesNotExist; that's correct behavior and will
# make sure that only correct ctypes get stored in the cache dict.
ct = self.get(pk=id)
self._add_to_cache(self.db, ct)
return ct
def clear_cache(self):
"""
Clear out the content-type cache.
"""
self._cache.clear()
def _add_to_cache(self, using, ct):
"""Insert a ContentType into the cache."""
# Note it's possible for ContentType objects to be stale; model_class() will return None.
# Hence, there is no reliance on model._meta.app_label here, just using the model fields instead.
key = (ct.app_label, ct.model)
self._cache.setdefault(using, {})[key] = ct
self._cache.setdefault(using, {})[ct.id] = c
可以看到它在内部维护着一个_cache字典,其中放着{app_label, model} 一一对应。并且放在缓存中提高查询效率。
它主要提供三个方法:
get_for_model, get_for_models, get_for_id:
get_for_id通过content_type的id 查找contenttype,每个contenttype对象也有对应的id,但一般情况下应该不知道id。
我们看get_for_model:通过model去获取它的contentType对象,正如我们上面使用的,传入article对象,去获取它对应的contentType对象
In [5]: article_content_type = ContentType.objects.get_for_model(article_obj)
然后再使用文章id过滤即可。
而get_for_models其实做的也是一样的事,只不过它返回的是一个字典{model, contenttpe_obj}即model对应的contentype组成的字典:
In [9]: article_content_type = ContentType.objects.get_for_models(article_obj)
In [10]: article_content_type
Out[10]: {<Article: Salt-Stack简单使用>: <ContentType: article>}
ContentType的简单分析就到这里了,对应的源码后续会上传到github: https://github.com/Andy963
事实上contentType有其方便性,但不可否认这样使用效率低的问题。而对于个人博客这种,要求不高可以使用,比如写一个评论组件,写一个点赞组件,这样它就可以对任意一个其它app进行评论,点赞了。