什么是Django ContentTypes?
Django ContentTypes是由Django框架提供的一个核心功能,它对当前项目中所有基于Django驱动的model提供了更高层次的抽象接口。
然而,对于Django ContentTypes不熟悉的人来说,上面这句话说了跟没说一样,因此,笔者将一步一步解释Django ContentTypes在Django框架中做了什么,以及如何使用Django ContentTypes。
当然,如果对于ContentTypes有了初步了解而只是不了解它的应用场景,可以直接查阅以下这两个链接:
Django official documentation:The contenttypes framework
stackoverflow: How exactly do Django content types work?
Django ContentTypes做了什么?
当使用django-admin初始化一个django项目的时候,可以看到在默认的INSTALL_APPS已经包含了django.contrib.contenttypes:
INSTALLED_APPS?=?[ ????'django.contrib.admin', ????'django.contrib.auth', ????'django.contrib.contenttypes', ????'django.contrib.sessions', ????'django.contrib.messages', ????'django.contrib.staticfiles', ]
而且注意django.contrib.contenttypes是在django.contrib.auth之后,这是因为auth中的permission系统是根据contenttypes来实现的。
笔者紧接着查阅了一下django.contrib.contenttypes.models文件:
class?ContentType(models.Model): ????app_label?=?models.CharField(max_length=100) ????model?=?models.CharField(_('python?model?class?name'),?max_length=100) ????objects?=?ContentTypeManager() ????class?Meta: ????????verbose_name?=?_('content?type') ????????verbose_name_plural?=?_('content?types') ????????db_table?=?'django_content_type' ????????unique_together?=?(('app_label',?'model'),) ????def?__str__(self): ????????return?self.name
大家可以看到ContentType就是一个简单的django model,而且它在数据库中的表的名字为django_content_type。
有经验的Django开发者对于这个表的名字一般都不会陌生,在第一次对Django的model进行migrate之后,就可以发现在数据库中出现了一张默认生成的名为django_content_type的表。
如果没有建立任何的model,默认django_content_type是这样的:
sqlite>?select?*?from?django_content_type; 1|admin|logentry 2|auth|group 3|auth|user 4|auth|permission 5|contenttypes|contenttype 6|sessions|session
因此,django_content_type记录了当前的Django项目中所有model所属的app(即app_label属性)以及model的名字(即model属性)。
当然,django_content_type并不只是记录属性这么简单,在一开始的时候笔者就提及了contenttypes是对model的一次封装,因此可以通过contenttypes动态的访问model类型,而不需要每次import具体的model类型。
·ContentType实例提供的接口
·ContentType.model_class()
·获取当前ContentType类型所代表的模型类
·ContentType.get_object_for_this_type()
·使用当前ContentType类型所代表的模型类做一次get查询
·ContentType管理器(manager)提供的接口
·ContentType.objects.get_for_id()
·通过id寻找ContentType类型,这个跟传统的get方法的区别就是它跟get_for_model共享一个缓存,因此更为推荐。
·ContentType.objects.get_for_model()
·通过model或者model的实例来寻找ContentType类型
Django ContentTypes的使用场景
Permission对ContentType的使用
在之前,笔者简单地提及了auth中Permission有涉及到对ContentType的使用,下面来看一下Permission的model源码:
class?Permission(models.Model): ????""" ????The?permissions?system?provides?a?way?to?assign?permissions?to?specific ????users?and?groups?of?users. ????The?permission?system?is?used?by?the?Django?admin?site,?but?may?also?be ????useful?in?your?own?code.?The?Django?admin?site?uses?permissions?as?follows: ????????-?The?"add"?permission?limits?the?user's?ability?to?view?the?"add"?form ??????????and?add?an?object. ????????-?The?"change"?permission?limits?a?user's?ability?to?view?the?change ??????????list,?view?the?"change"?form?and?change?an?object. ????????-?The?"delete"?permission?limits?the?ability?to?delete?an?object. ????Permissions?are?set?globally?per?type?of?object,?not?per?specific?object ????instance.?It?is?possible?to?say?"Mary?may?change?news?stories,"?but?it's ????not?currently?possible?to?say?"Mary?may?change?news?stories,?but?only?the ????ones?she?created?herself"?or?"Mary?may?only?change?news?stories?that?have?a ????certain?status?or?publication?date." ????Three?basic?permissions?--?add,?change?and?delete?--?are?automatically ????created?for?each?Django?model. ????""" ????name?=?models.CharField(_('name'),?max_length=255) ????content_type?=?models.ForeignKey(ContentType,models.CASCADE,verbose_name=_('content?type'),) ????codename?=?models.CharField(_('codename'),?max_length=100) ????objects?=?PermissionManager() ????class?Meta: ????????verbose_name?=?_('permission') ????????verbose_name_plural?=?_('permissions') ????????unique_together?=?(('content_type',?'codename'),) ????????ordering?=?('content_type__app_label',?'content_type__model','codename')
大家可以看到Permission模型中设置了一个对ContentType的外键,这意味着每一个Permission的实例都具有关于一个ContentType的id作为外键,而ContentType的id恰恰代表着一个Model。
回想Permission模型在初始化的时候发生了什么,它为每个模型设置了三个权限,分别是add,change以及delete,那么它是如何跟每个模型联系起来的呢?就是通过一个到ContentType的外键。
大家可以看一下Permission表,其中第二行就是content_type,然后将主键于django_content_type对比:
sqlite>?select?*?from?auth_permission; 1|1|add_logentry|Can?add?log?entry 2|1|change_logentry|Can?change?log?entry 3|1|delete_logentry|Can?delete?log?entry 4|2|add_group|Can?add?group 5|2|change_group|Can?change?group 6|2|delete_group|Can?delete?group 7|3|add_user|Can?add?user 8|3|change_user|Can?change?user 9|3|delete_user|Can?delete?user 10|4|add_permission|Can?add?permission 11|4|change_permission|Can?change?permission 12|4|delete_permission|Can?delete?permission 13|5|add_contenttype|Can?add?content?type 14|5|change_contenttype|Can?change?content?type 15|5|delete_contenttype|Can?delete?content?type 16|6|add_session|Can?add?session 17|6|change_session|Can?change?session 18|6|delete_session|Can?delete?session
如此,Permission模型借助ContentType表达了对一个model的权限操作。
ContentType的通用类型
笔者将引用在顶部的stackoverflow中回答的例子讲述对通用类型的理解。
假设以下的应用场景:
from?django.db?import?models from?django.contrib.auth.models?import?User #?Create?your?models?here. class?Post(models.Model): ????author?=?models.ForeignKey(User) ????title?=?models.CharField(max_length=75) ????slug?=?models.SlugField(unique=True) ????body?=?models.TextField(blank=True) class?Picture(models.Model): ????author?=?models.ForeignKey(User) ????image?=?models.ImageField() ????caption?=?models.TextField(blank=True) class?Comment(models.Model): ????author?=?models.ForeignKey(User) ????body?=?models.TextField(blank=True) ????post?=?models.ForeignKey(Post,?null=True) ????picture?=?models.ForeignKey(Picture,?null=True)
注意笔者这里跟原回答做了一些更改,在原回答中Comment中没有null的选项,笔者觉得回答者真正要表达的是Comment是分别和Picture或者Post中其中一个对应即可,一个Comment并不既需要Post又需要Picture才能建立,可能是回答者写错没注意的缘故。
当笔者对以上model进行migrate之后,发现Comment表中的foreignkey是可以被设置为null的。
那么,如何通过Contenttype框架对以上代码进行改进呢?
ContentType提供了一种GenericForeignKey的类型,通过这种类型可以实现在Comment对其余所有model的外键关系。
修改后的Comment模型如下:
class?Comment(models.Model): ????author?=?models.ForeignKey(User) ????body?=?models.TextField(blank=True) ????content_type?=?models.ForeignKey(ContentType) ????object_id?=?models.PositiveIntegerField() ????content_object?=?fields.GenericForeignKey()
在这里,通过使用一个content_type属性代替了实际的model(如Post,Picture),而object_id则代表了实际model中的一个实例的主键,其中,content_type和object_id的字段命名都是作为字符串参数传进content_object的。即:
content_object?=?fields.GenericForeignKey('content_type',?'object_id')
笔者先准备一些测试用的数据:
user?=?User.objects.create_user(username='user1',?password='2333') post?=?Post.objects.create(author=user,title='title1',slug=slugify('title1'),body='') picture?=?Picture.objects.create(author=user,image="http://HdhCmsTestpicture1测试数据",caption='picture1')
接着在shell中创建Comment:
>>>?from?foreign.models?import?Post,?Picture,?Common >>>?from?django.contrib.auth.models?import?User >>>?user?=?User.objects.get(username='user1') >>>?post?=?Post.objects.get(title='title1') >>>?c?=?Comment.objects.create(author=user,?body='',?content_object=post) >>>?c <Comment:?Comment?object> >>>?c.content_type <ContentType:?post> >>>?c.object_id 1 >>>?picture?=?Picture.objects.get(caption='picuture1') >>>?c?=?Comment.objects.create(author=user,?body='',?content_object=picture) >>>?c.content_type <ContentType:?picture> >>>?c.object_id 1
在django中,也提供从诸如Post,Picture访问Comment的查询,通过GenericRelation类型。如:
class?Post(models.Model): ????author?=?models.ForeignKey(User) ????title?=?models.CharField(max_length=75) ????slug?=?models.SlugField(unique=True) ????body?=?models.TextField(blank=True) ????comment?=?GenericRelation('Comment')
值得注意的是,如果在Post中定义了GenericRelation,删除了一个实例,在Comment中所有的相关实例也会被删除,GenericForeignKey不支持设置on_delete参数。
因此,如果对级联删除不满意的话就不要设置GenericRelation。
查看更多关于Django contenttypes框架的详细内容...