Django 之 注意事项及汇总

本文最后更新2018-11-23 12:15 URLS

学习Django框架,因为框架都是别人封装好的,所以使用起来确实方便;但是由于是别人咀嚼给我们吃的。。。(sorry for using this words),所以抽象程度很高,造成了易用难理解不好记,很多为什么这样做会感到费解,这是锻炼抽象理解能力的时候到了。要理解django框架最好从mtv(mvc)设计模式切入。但是每个点有很多注意事项,所以在这里按照django框架的主要modules分类注意事项。

本文主要记录自己在学习django过程中遇到的问题的记录,有新增会更新。每个表述上可能不是很清晰,见谅。

更新01:由于django2.2 在执行 python manage.py makemigrate/migrate 操作时报错“'str' object has no attribute 'decode'” 解决办法参考:https://www.cnblogs.com/xiaobinglife/articles/10716605.html

全局 settings

  1. 创建的app要在settings中注册定义,不然django将不会去使用这个app。因为django的哲学就是所有‘app’ 是即插即用。一个app可以在多个项目中,只要在项目的setting中注册该app。
  2. 引用注册到installed_apps列表有两种方式:使用应用目录下的app_name.apps.*Configig类 或者直接将应用路径名填入,就到应用文件夹层面为止;
  3. 只有注册了的app,后面的model,template等会使用注册了app的一些信息,没注册它们是不会使用的;如,template system查找模版,根据app列表。
  4. 设置输出到控制台的日志,打印出model api所执行时的sql语句,在项目setting中添加:
LOGGING = {
    'version': 1,
    'disable_existing_loggers': False,
    'handlers': {
        'console': {
            'level': 'DEBUG',
            'class': 'logging.StreamHandler',
        },
    },
    'loggers': {
        'django.db.backends': {
            'handlers': ['console'],
            'propagate': True,
            'level': 'DEBUG',
        },
    }
}
  1. 配置登录验证装饰器(@login_required),利用该装饰器用户登录状态验证失败会跳转到配置文件指定的url。即设置该参数settings.LOGIN_URL='/login/'
  2. 如果使用继承Abstractuser类的一个user 模式类,那么使用auth组件,就要在settings中设置一个配置AUTH_USER_MODEL = 'app.UserMedole' 点前面是应用名,点后面是自定义的user认证model。这里的ORM继承方式是TABLE_PER_CLASS继承方式。
  3. MEDIA_ROOT 默认是'', 这个路径是一个文件系统的绝对路径,利用配置文件中的BASE_DIR,通过os.path.join() 来获取到绝对路径。

model模块-模型模块

  1. 配置数据库时,如果使用的是mysql数据库;由于Django2.0默认需要mysqlclient模块;需要安装该模块,不然会启动报异常;替代方案是使用pymysql模块,做法是:安装pymysql,再在settings同目录的_init_.py文件中添加以下代码:
import pymysql
pymysql.install_as_MySQLdb()
  1. 三个manage.py 命令参数makemigrations和migrate:
    2.1. makemigrations 将setting.py文件中的apps中的model.py中的‘表对象’ 生成对应的迁移映射文件(存放在app目录下的migrations目录中);生成的映射文件用于对数据库进行建表操作。即用于下一步migrate操作
    2.2. migrate 应用映射文件到数据库中,即创建检查数据库中没有映射文件中涉及到的表。
    2.3. sqlmigrate 命里可以打印出要执行的sql语句,不会实质的执行。提供执行sql可以帮助我了解做了什么数据库操作,并且可以提供给dba即你心里修改。用法:python manage.py sqlmigrate app_name migrations_file_name

  2. 对于新建的Django项目,第一次执行migration操作会将django自带的auth,session组件需要的表一并创建。因为manage.py会遍历所有注册了的apps,然后构建他们需要但是数据库中没有的表。这其实是一个完整的migration过程。也充分证明了,django项目新建表需要先makemigration操作,才会migrate操作创建成功新建表到数据库。如果不想使用django提供的组件或者说modules,可以在migrate前,将相应的组件app从setting.py文件的installed_app中注释或移除。

  3. ORM 让代码与数据库解耦。

  4. 对于django.db提供的backends封装的MySQLclient会去检测安装的mysqlclient的版本,如果小于1.3.3会抛出异常,所以要么更新要么到代码中去注释掉检测代码。

我的文件路径:D:\Python\Python36\Lib\site-packages\django\db\backends\mysql\base.py

注释掉下面检测代码:
# if version < (1, 3, 3):
#     raise ImproperlyConfigured("mysqlclient 1.3.3 or newer is required; you have %s" % Database.__version__)
  1. models.py中的model class 之间可以表示通常数据库中存在的关系:many-to-one,one-to-one,many-to-many。虽然创建model class 只写了很少的代码,但是给与了Django很多的信息。有了model,django可以:创建数据库模型(创建表)为所在的app应用;创建一个python database-access API for accessing 相应的model class 对象

  2. model 和 表 一定要映射好,如果数据库表改变了,对应的model要改变,并进行makemigrations操作,最好在sqlmigrate看下会不会有sql操作,没有就不必再执行migrate操作。如果是改变了model,需要执行一次全流程:makemigrations-sqlmigrate-migrate 这样可以是映射双方一致,不然可能会出问题。

  3. 通过orm创建的表名会默认变为应用app名称下划线加上model名。这些都是可以自定义覆盖的。

  4. 为什么要分步走操作model到库:

The reason that there are separate commands to make and apply migrations is because you’ll commit migrations to your version control system and ship them with your app; they not only make your development easier, they’re also useable by other developers and in production.

Read the django-admin documentation for full information on what the manage.py utility can do. 
  1. 关于model relations 参考官档: For more information on model relations, see Accessing related objects. For more on how to use double underscores to perform field lookups via the API, see Field lookups. For full details on the database API, see our Database API reference.

  2. 创建model class 最后好重写__str_ _()方法,这样数据对象在admin显示的时候会友好点。

  3. 通过model class 创建记录,最好通过显示的save()操作。这是开发者尽可能的让sql运行的时间短,sql优化;所以才有了QuerySet对象select_related()方法的存在。而且底层执行join操作是自动的。而且设置者将每个对象都能访问和它相关的对象。也可以写原生sql语句。

  4. model class 在定义字段时,Field的null和blank选项的区别,null是关于数据库中字段是否将空值设成数据库中的null,默认是Flase,也就是空值就是空值(特别是字符串类型的字段,因为空字符串值对这些字段是有意义的)。而blank是在允许model object 的字段是否能为空值。默认Flase,不能为空。

  5. 从查询中得到Queryset,特别是在认证的时候,经常会通过Queryset[0]想获取model object对象,这是错误的做法,如果认证失败,Queryset没有,那么该方法取值就会报错,应该使用Queryset.first()取出来。

  6. 对于model的datetime字段,会使用USE_TZ和TIME_ZONE确定下的时区来存储时间,和mysql全局时区设置可能无关,因为这个是优先级更高的字段时区设置。

  7. 在修改了models.py中 model的字段后,需要重新同步到数据库当中,但是数据库如果表中已经有数据或者一些中间表创建的修改,会造成同步上的复杂情况处理。每每遇到这种情况,我脑壳都是大的,相信每个人脑壳都是大的,就算没有使用orm,在生产库上,对于大数据表的修改增加字段或者该字段属性,由于表中已经存在大量数据,这时候的原生sql修改都是非常复杂的工程或者不好办到的。但是如果非要操作,不论是通过orm还是原生sql,首先都是dump备份数据。然后再操作。 现在,这里只说下orm操作要修改model后要通过mange.py orm系统 同步到数据库中修改。我最初的想法是:清空migrations文件夹中所有文件;然后通过原生sql drop掉数据库中相关表;这时候再重新makemigrations appname -> migrate appname;结果出现了,migrations文件夹中有了migrations-files 但是数据库中没有同步更新,没有新建表,WTF!为什么呀!。那么就开启我们的解密之旅:前提知识是一,migrations文件是一个小的文件版本控制系统,每次只要models.py有变化并执行makemigrations提交变化到migrations版本仓库中,都会生成一个新的migrations-file,而且文件名是有序列号0001。。0002等开头索引的。每一个文件都是一个版本。版本系统然我们可以多人协作开发models.py,每个人的提交都是一个新文件,就好callback版本了。还有个前提知识而,就是,在使用manage.py第一初始化时,都会生成0001__init文件,同时这时会在数据库中创建一个表 django_migrations 里面记录的就是每一个app的哪个migration文件已经同步迁移到了数据库中,不了解的可以看下自己初始化后数据库中的这个表。那么我们要开始变形了,哦,不是解密了 当我们修改了models.py,我们进行makemigrations 可以看到新的migration-file创建到了我们的小型版本仓库中,有了新的序列化0002。。。或大于0002的。也就是我们app的model有了新的版本。这时候我们执行migrate,这时的django 迁移同步数据库系统 开始处理过程(我自己起的名字,其实就是mange.py migrate运行的过程):首先通过要迁移同步的app 去数据库的django_migrations表中检索该app名字的迁移记录,然后对应每一个小型创库中迁移文件去checking,检查是否文件已经做过迁移了,这时发现我们的有新的迁移文件0002....,那就进行一个和models与数据库中同步,同步成功,就将0002迁移记录插入到django_migrations表中,表示同步过的记录。到目前为止,我们假设成功进行进行了一次修改迁移。但是,就像开始我们提到的,世事难料,同步数据库操作总是复杂的,对于简单同步(如新增表,不影响其它表)是很容易成功,复杂同步,这时候migrate过程总是会出错的,就到了我们秘密之旅 前遇到的操作情况,删表,删migration-files 再重建,表没同步到数据库的情况了。按照解密同步逻辑。由于重新makemigrations,只会生成一个0001_initial的文件(为什么只生成一个呢?因为我们删除仓库,重建了版本库,第一次makemigrations会将所有改变放入一个文件中。这也是造成问题的起因),然后进行migrate迁移动作,这是 迁移同步过程 检查 django_migrations表 ,发现记录里面该app的0001_init是迁移过的,此时就在控制台输出“ No migrations to apply. ”。这回事数据库就不会同步创建表了,这就是出现问题的原因所在。 怎么解决呢? 既然要同步到数据库,查看的django_migration表中是否做过同步了,那就将表中app的记录删了,此时再执行manage.py migrate,就发现,表创建同步成功了!!! 来杯香槟庆祝!! 但是慢着,既然我们知道了原理,那我们操作时清空数据库表还要清空migrations 文件夹,就可以减少一步了,不删除migrations文件,而是只删表和清楚app对应的django_migration表中记录就可以了。如果删除小型版本库,那我们对model的修改记录都没了,是一笔损失,不好回退版本了。所以只需要删除表和删除django_migrations表中app记录就行了。 学会了吗? 可以自己实验下。

URLs模块

  1. 技术上讲,如:foo.com/bar 和 foo.com/bar/ 是两个不同的URLs, and 搜索引擎robots会将他们作为单独的页面。Django 应该 make effort to 'normalize' URLs so that search-engine robots don't get confused. Django 提供APPEND_SLASH来规范这样的困扰。
APPEND_SLASH
	Default:True
	When set to True, if the request URL not match any of the patterns in URLconf and it doesn't end in a slash, an HTTP redirect is issued to the same URL with a slash appended.Note that the redirect may cause any data submitted in a POST reuqest to be lost.
这是用到一个重定向,这可能造成困扰。
下面是抓包证明django确实做了重定向(win下用使用rawcap抓包)。
所以这里有两个方面的考虑:
1. 如果符合上面的情况,造成了重定向,要考虑重定向带来的后果。同时任何没匹配且没有已/结尾的请求都会有一次重定向给浏览器。
2. 如果将URLsconf中去掉了SLASH,那么如果要区分就两种情况都要添加到URLsconf中,就不产生重定向了。或者APPEND_SLASH:False.但是这样就只有严格匹配。无论是用户添加斜杠和URLsconf的route字符串中添加斜杠,将会带来更多的困扰。所以Django默认是APPEND_SLASH:True.
但是django对于没有已斜线结尾且没有url匹配的上会发送一个添加斜杠的url,这是针对没有post数据的情况。如果有post数据时且这个数据还在处理,django将不会自动添加。所以用ajax上传文件,不要依赖django提供的这个功能,要加上应该有的斜杠。

Django学习之五:Django 之 注意事项及汇总

  1. Django少斜杠 产生301重定向造成登陆态变化的困扰https://blog.csdn.net/handsomekang/article/details/78650513

  2. Django 2.0 使用include中定义命名空间的话,要在被include的url模块中加入app_name属性且属性值为实际app明;或者改变include为include(('app01.urls', 'app01'),namespace = 'app01').不然会报异常。这是怎么回事呢?有空再去看文档。

  3. Django2.0 提供path对象来匹配uri。新的path要求将正则表达式封装在一个converter转换器中,converter除了封装有regex还可以做很多类型转换,converter还必须实现两个函数:to_python
    和to_url两个方法。之所以提供path,是可以提供一些默认的转换器来替代正则表达式,为一些不会正则表达式的同学使用;同时解决复用问题,通过将正则表达式封装成转换器,转换器就可以多出被引用了,非常科学的path设计。

  4. urlpatterns列表中都是path对象或者re_path对象,他们都like-a关系都是有同样的接口,给django来调用是否匹配他们包含的正则表达式。

  5. include()可以是其它包含urlpattern列表的模块,也可以使一个包含path或re_path对象的列表变量。

from django.urls import include, path

from apps.main import views as main_views
from credit import views as credit_views

extra_patterns = [
    path('reports/', credit_views.report),
    path('reports/<int:id>/', credit_views.report),
    path('charge/', credit_views.charge),
]

urlpatterns = [
    path('', main_views.homepage),
    path('help/', include('apps.help.urls')),  # 这是include模块
    path('credit/', include(extra_patterns)),  # 这是include一个path列表
]

  1. 这里不针对django的url,所有html a标签,表单,包括jquery实现的ajax的url参数中,空的话默认是当前页面的完整url,如果填写了一个uri,这个uri最前面有没有'/'对浏览器来说是很重要的,如果没有,则是当前url+uri;如果有,那么就是当前的ip:port+uri,也就是说,有‘/’会从url的绝对路径访问,没有则是相对当前路径访问。关于django的中提供的反向解析大多都是产生一个绝对路径。

  2. 这里区分下django web 框架编程代码中,几个相对路径的意义,在web开发中比较容易混淆:a) 非djangoAPI中的相对路径,如open中的相对路径是相对与当前入口代码文件的路径,即main程序,即使在路径前使用一个像linux的/根目录的表示,如/aaa/bbb/,其实也是一个相对路径,也是相对当前文件目录的路径,要是用绝对路径必须从文件驱动器开始。b)
    在django的 render函数中,指定模版时,使用的路径是相对于每个注册app下的templates目录作为相对目录,包括settings中指定的额外templates存放目录作为相对参照目录。c) 另一个就是静态文件目录,我么经常在模版中使用static标签指定静态文件,这是指定的静态文件路径,就是相对于每个注册app的static目录,作为相对路径参考路径,包括settings配置STATICFILES_DIRS中额外指定的目录,类似templates的参照相对路径。以上三种路径在web编程时很容易混淆,所以要在使用路径时特别注意,使用在那种情况下,不同情况的参照路径是不同的。d) 还有就是settings配置文件中的STATICFILES_DIRS中使用的文件系统绝对路径,MEDIA_ROOT也是文件系统的绝对路径。

  3. 对于settings中 url的路由,有匹配的path或re_path放到urlpatterns列表的后面,把固定匹配的放在列表前面,避免被匹配截胡了。

  4. 利用re_path或url等匹配请求中url

  5. url_conf 路由系统(所谓路由就是根据url匹配,走向哪个view或者下一个url_conf路由),其实就是个匹配模糊映射,提供匹配的正则匹配。既然是正则匹配那就可严厉,可宽阔,即广度可控。但是由于路由匹配查找算法是django默认的,只会顺序查找列表树,即遍历树的算法是顺序遍历,这就存在新增路由记录时,要考虑插入位置还要考虑类似路由记录是否会截胡,即还要考虑类似记录的正则匹配广度是否可以更精确点减小广度,让路由到新增的记录上。

Templates System 模版模块

  1. 对于模版系统明确:模版语法;含有模版语法的文本是模版文件;模版文件渲染通过模版系统的render()函数,函数渲染后应该返回一个Httpresponse对象;过滤器filter; 两种模版语法的语法形式;两种简单逻辑branch分支和looping循环。Django模版系统是后端模版选型,大多页面渲染都是后端做,没有太多前端控制内容,所以很多时候要整个页面刷新或者重定向。模版设计创建可以是python麻瓜。。。就是纯页面设计人员不一定懂python,但是需要的数据信息要在该网站的范畴内,不然Django开发人员使用模版时无法提供这些超出范畴的数据。

  2. Django框架有自己的模版语法,但也支持来自第三方的模版语言,需要第三发组件。但是使用不信任的开发者的第三发组件用于templates是不安全的,因为可能没有防止恶意注入的功能,导致不安全。

  3. Django定义了一个标准的API 用于 loading 和 rendering, templates.loading 就是查找templates 和预处理 templates,通常编译templates到内存中;Rendering 向 内存中的templates 插入 环境数据 并放回一个结果字符串。

  4. templates system 查找模板的目录在settting配置文件中。两个参数都可以DIRS和APP_DIRS

  5. get_template() 和 select_template() 后者更加灵活,可用于fallback

  6. 注意每个注册了的app, 自己的模版一定要放在一个应用目录下的templates子目录中。

  7. 在模版语言中,我们可能常会用到一个{% static %}的标签,这个标签不是一个内置标签,这个是django.contrib.statifiles 应用的自有定义标签,所以要使用该标签必须使用{% load static %} 该应用中的自定义文件,这样后面就可以使用这个标签了。就是这个还要注意在template inheritance继承中,导入的自定义只会在出现导入语句的模版中有效,其子模版中是没有效果的,所以要在子模版也加上这个{% load static %}才能在子模版中使用{% static '' %}引入静态文件路径。经过实验,只有第三方自定义的才需要在父子模块中都要导入才行。DTL中的变量都从子模块能够得到解析。

  8. {% extends 'base.html' %} 继承一个模版这句话必须在模版文件最前面

  9. {% block.super %} 会使用父级模版的内容

View/HttpRequest/HttpResponse

  1. 在request中获取post数据时,如果通过request.POST获取有checkbox和select表单提交的多个数据时,不能使用get方法而是使用getlist才能取出,不然会取出多个数据的最后一个值,而通过getlist可以得到列表。
  2. JsonResponse 的第一个参数应该是一个dict 对象。如果safe参数设置为False,第一个参数可以是一个可序列化为Json的对象(列表,字符串,数字等)。由于safe参数默认值是True,所以JsonResponse的第一个参数要是一个字典对象。如果不是那么会报错'TypeError: In order to allow non-dict objects to be serialized set the safe parameter to False.' 为什么这样设计?
  3. request.META 有关http headers的数据会存放到这里,但是这个字典里面的key有可能变化了。而且自定义的header也是有格式要求和添加前缀的。而且META中的key都是大写处理了的。如自定义的header key如果是id_token,那么在META中就不会存放,必须在客户端将header key 设置为id-token,不要使用下划线,要用短中杠。而且在request.META中取时使用HTTP_ID_TOKEN取出对应的值。这是非常需要注意的。具体参考:https://docs.djangoproject.com/en/2.1/ref/request-response/#django.http.HttpRequest.META
    “Note that runserver strips all headers with underscores in the name, so you won’t see them in META. This prevents header-spoofing based on ambiguity between underscores and dashes both being normalizing to underscores in WSGI environment variables. It matches the behavior of Web servers like Nginx and Apache 2.4+.”
    “注意 django 的runserver 启动,会褪掉所有headers中包含下划线的header,所以在META中你是找不到带有下划线的自定义header的。这样做是为了防止 header-spoofing 区分不清楚 下划线 和 破折号 ,所以两者都会被格式化为下划线在 WSGI环境变量中。 在 很多WEB servers 像 nginx 和 apache 2.4+ 都有这种保护措施。”

cookie and session

  1. 浏览器的刷新,记住不是简单的访问浏览器地址栏看到的地址,而是重新访问的是,得到当前页面的请求,也就是刷新发送的请求报文和上一个请求报文的method,提交的参数都是一样的。不要被地址栏的蒙骗了,以为就是再次访问地址栏的地址。
    这个问题是在登录成功过后,返回一个成功页面,刷新页面发现提交的内容还是登录请求一样,不是访问地址栏的一个get请求。
  2. django session,如果数据库总的sessionid存在且有效(是否过期?或者其它限定),会沿用sessionid,不会覆盖。这种不会新建sessionid只更新session字典内容,就会存在一个问题:如果换另一用户登录,那么如果只更新用户名,没有更新了上一个用户访问其它页面存放的权限信息,那么这个用户就会得到上一个用户的session中保存的权限内容,这就造成了数据混淆。解决这种情况,只有增加判断,删除sessionid,新加sessionid。django提供了一个比较方便的组件,叫做auth,使用auth进行认证,就会有上面提到的删除老的sessionid记录,创建新的sessionid记录。

File storage API

Django 提供了两种方便的方式访问当前的storage class:

  1. class DefualtStorage
  2. get_storage_class

django中时区

  1. 配置文件中有 TIME_ZONE和 USE_TZ 这两个,结合在一起将django使用的时间时区有关都表述清楚了。具体就是前者TIME_ZONE规定了django中所有api在输出时间是使用显示的时区。而后者则是限制通过model进行数据存储时使用的时区是是否使用TIME_ZONE配置的时区。如果USE_TZ是True则使用UTC通过model存储,也就是所通过model获取的datetime时间是UTC时间。如果设置为False这model存储获取的datetime时间是TIME_ZONE指定的时区时间。所以从model读取的datetime时间是受两个设置所决定的,就是USE_TZ和TIME_ZONE了。
  2. django的utils提供了一个工具django.utils.timezone.now() 来获取和model相同的时区设置。如果使用datetime.datetime.now() 可能就不能和model中的时间进行时间间隔运算。