Two Scoops of Django 1.8 学习笔记二

6. Model 最佳实践

Models是大多数django项目的基础,在创建Model的时候请三思而后行,充分考虑之后再创建大而全的Model。

6.1 基础

6.1.1 拆分拥有过多models的app

如果一个app有20+ models,考虑下能不能把这些models拆分到更小的app里面,因为这可能意味着你的app做了太多的事情。
在实际中,我们一般会将这个数值减小至每个app不超过5个models

6.1.2 谨慎使用model继承

Django中的model继承是一个棘手的问题,Django提供了三种继承方式:

  • Abstract base classes
  • Multi-table inheritance
  • Proxy models

他们之间的具体优缺点比较如下:

model继承类型 优点 缺点
不使用model继承:如果models有一个共同的字段,给每个model都赋予那个字段 便于理解models和数据库表之间的关系 过多的字段穿插在models中,不利于维护
Abstract base classes(抽象基类):仅用于衍生模型 一个抽象父类中拥有共同的字段,从而减少了我们的输入。 不能单独使用父类。
Multi-table inheritance(多表继承):用于将父类子类联系起来,隐晦的意思就是OneToOneField 给了每个model一张表,因此我们既能查询父类也可以查询子类。而且我们从父类中得到了一个子对象:parent.child 增加了大量的开销,因为上一个子表的每个查询需要加入到所有父表。我们强烈建议不要使用多表继承。
Proxy models(代理模型):仅用于原始模型 让我们拥有具备不同Python行为的model别名。 我们不能修改model的字段

那么具体什么时候该使用哪一种 model继承呢?下面提供一个简单的规则:

  • 如果models之间只有极小的重叠部分(比如说两个模型只有一两个共同的字段),那么就不必使用 model继承,给每个model都加上那个字段就好啦
  • 如果models之间有很多重叠部分,而且这些重复的字段的维护会给我们造成困惑和意想不到的错误,
    那么在大多数情况下就需要考虑代码重构了。因此我们把这些共同的字段写到Abstract base classes中
  • Proxy models 是一个偶尔有用的便捷功能,但是他和其他两种 model继承方式有非常大的不同
  • 每个人都应该避免使用Multi-table inheritance,取而代之的是使用 OneToOneField 和 ForeignKey

6.1.3 实践中的 model继承: TimeStampedModel

在Django项目中大多数 model都会包含一个能创建和修改的时间戳字段。最好的方法就是写一个TimeStampedModel

# core/models.py
from django.db import models

class TimeStampedModel(models.Model):
    """
    An abstract base class model that provides
    self-updating created and modified fields.
    """
    created = models.DateTimeField(auto_now_add=True)
    modified = models.DateTimeField(auto_now=True)

    class Meta:
        abstract = True

注意上面代码的最后2句,他把这个model转化成了abstract base class.
我们定义一个继承TimeStampedModel的新的class的之后,运行migrate,
Django不会在数据库创建一张名为 core_timestampedmodel的表。

# flavors/models.py

from django.db import models
from core.models import TimeStampedModel

class Flavor(TimeStampedModel):
    title = models.CharField(max_length=200)

Flavor继承自 TimeStampedModel 当我们migrate的时候只会创建一张名为 flavors_flavor的表。这说明Abstract base classes不会单独的创建一张表,简直完美。

6.1.4 Database Migrations 数据迁移

创建迁移文件的tips:

  • 创建一个新的app 或者 model之后,花一点点时间来给这个 model创建一个初始的 django.db.migrations。我们只需要python manage.py makemigrations
  • 在运行之前检查一下生成的migration代码,特别是涉及到较复杂的改变的时候。并且复查一下SQL
  • MIGRATION_MODULES 来管理那些没有自己的django.db.migrations风格的迁移文件
  • 不用担心创建了多少个迁移文件,如果数量太过笨拙,使用 squashmigrations使他们就范

migrations的部署和管理:

  • 在项目部署之前,一定要检查是否可以回滚migrations。
  • 如果一个项目有上百万条数据,在生产环境服务器上运行migration之前,请在staging服务器上做粗犷测试,在真实数据上跑migrations会比预想中花更多更多更多时间!
  • 如果你在用Mysql:

    1. 在任一架构改变之前必须拆分数据库,MYSQL缺乏围绕架构更改的事务支持,因此不可能回滚
    2. 如果可以的话,在执行修改之前把这个项目设置成只读模式
    3. 如果不小心,在庞大的表中修改了架构,那么将会花很长的时间,不是几秒几分的概念,而是几小时

6.2 Django Model 设计

如何设计一个好的django models 是最难的主题之一

6.2.1 规范化开端

开始之前,请熟悉下数据库标准化(Database normalization)

6.2.2 什么时候用null和blank

字段类型 设置 null=True 设置 blank=True
CharField, TextField, SlugField, EmailField, CommaSepararedIntegerField, UUIDField 不行 可以
FileField, ImageField 不行 可以
BooleanField 不行 用NullBooleanField代替 不行
IntegerField, FloatField, DecimalField, DurationField, etc 可以 可以
DateTimeField, DateField, TimeField, etc 可以 可以
ForeignKey, ManyToManyField, OneToOneField 可以 可以
GenericIPAddressField 可以 可以
IPAddressField 已弃用 用GenericIPAddressField代替 已弃用 用GenericIPAddressField代替

6.2.3 避免使用Generic Relations

使用Generic Relations的弊大于利,简而言之:

  • 避免使用 Generic Relations 和 GenericForeignKey
  • 如果你觉得你需要使用 Generic Relations的话,看看能不能通过更好的 model设计或者 PostgreSQL里的新字段代替
  • 如果必须要用,尝试使用现成的第三方app,第三方app提供的隔离将有助于保证数据更干净

6.2.4 PostgreSQL中的特别字段什么时候用null和blank

字段类型 设置 null=True 设置 blank=True
ArrayField 可以 可以
HStoreField 可以 可以
IntegerRangeField, BigIntegerRangeField, FloatRangeField 可以 可以
DateTimeRangeField, DateRangeField 可以 可以

6.3 The Model _meta API

Django 1.8之前,Model _meta API 都还是非官方的,即使修改了api的内容也不会提及。
最初的目的只是让Model它自己储存一些额外的信息,然而这一功能已经证实_meta是相当有用的,所以现在有API文档了

大多数项目里你都不会需要 _meta,它的主要用途是,当你需要

  • 得到model的字段列表
  • 得到class的特定字段(或者它的继承链,或者它的派生)
  • 在跨未来的django版本中,确保如何让你得到这些信息保持不变

例如下面这些例子:

  • 构造一个django model的自省工具
  • 构造自定义的表单库
  • 创建一个类管理员工具来与django model进行编辑和交互
  • 写一个可视化的或者具有分析力的库

扩展阅读:

6.4 Model Managers 模型管理器

每当我们用django的ORM来查询model的时候, 我们都会用到一个名叫 Model Managers的接口来与数据库进行交互, Model Managers可以说是这个模型类的所有可能的实例。
Django给每个model都提供了一个默认的 Model Managers,但是我们依然可以定义自己的Model Managers.

from django.db import models
from django.utils import timezone

class PublishedManager(models.Manager):

    use_for_related_fields = True

    def published(self, **kwargs):
        return self.filter(pub_date__lte=timezone.now(), **kwargs)

class FlavorReview(models.Model):
    review = models.CharField(max_length=255)
    pub_date = models.DateTimeField()

    # add our custom model manager
    objects = PublishedManager()

现在如果我们想要先显示所有的评论,然后显示已发布的评论,可以这样做

>>> from reviews.models import FlavorReview
>>> FlavorReview.objects.count()
35
>>> FlavorReview.objects.published().count()
31

很简单不是吗?但是如果你只是增加了第二个Model Managers,你可能会用到下面的方法:

# Bad example
>>> from reviews.models import FlavorReview
>>> FlavorReview.objects.filter().count()
35
>>> FlavorReview.published().filter.count()
31

表面上看来,取代默认的Model Managers 是常理的做法,不幸的是,在实际的项目开发中当我们用到这个方法的时候应该非常的小心。为啥?

  • 第一,当我们用model继承的时候,abstract base classes的子类接受来自他们父类的manager,但是concrete base classes就不会。
  • 第二,应用到model class的第一个manager会被django当作默认的manager,这显然打破了python的模式,导致会从QuerySet中出现不可预知的结果
    了解到以上知识之后,你应该在自定义model manager之前先定义好objects = models.Managers()

扩展阅读:https://docs.djangoproject.com/en/1.8/topics/db/managers/

6.5 理解 Fat Models

之前提到过 Fat Models,意思是不要把数据相关的代码都放在Views和Templates里面,
而是将他们的逻辑封装在models methods, classmethods, properties,甚至可以是manager method里。
这样,任意一个views或者任务都可以用这个相同的逻辑。例如我们有个Ice Cream的评论model,我们可能会接触到以下的方法:

  • Review.create_review(cls, user, rating, title, description)
    一个创建评论的方法,从HTML和REST视图调用model class本身,以及一个接受电子表格的导入工具
  • review.product_average一个评论的实例属性,返回这个评论项目的平均排名。用于审查的细节视图,以便读者不离开页面就能感受到整体的意见
  • review.found_useful(self, user, yes)这个方法可以设置成是或者否,让读者发现评论是否有用。用于详情和列表视图,来实现HTML和REST