Django模型是与数据库相关的,与数据库相关的代码一般写在models.py中,Django支持sqlite3、MySQL、PostgreSQL等数据库,只需要在settings.py中配置即可,不用更改models.py中的代码,因为ORM封装了底层,并且提供了丰富的API极大的方便了使用。
一、ORM介绍
对象-关系映射(OBJECT/RELATION MAPPING,简称ORM),是随着面向对象的软件开发方法发展而产生的。
一个关系数据库中都有什么?没错,简单来说,就是一张张表。表中又有什么?行和列,一行就是一条记录,一列就代表着一条记录的某个属性。举例来说,一个学 籍数据库可能包含一张学生信息表,表中每行记录着一个学生的信息,由很多列组成,每一列表示学生的一个属性,比如姓名、年龄、入学时间……
有没有觉得和python中的类、实例对象以及成员属性的概念有某种映射关系呢?哈,没错,orm其实就是把表映射成了类,它包含一些成员属性,相当于一个模板,通过这个模板,我们给相应的属性填上一个值,可以创建一个实例对象也就是一条记录。然后把常用的一些SQL操作封装成对应的方法,比如select封装为get方法,这样就实现了一个ORM。
简单来说就是使用Python的方法、数据结构访问数据库,并且数据库返回的数据类型也是Python数据类型;使用ORM还有一个好处就是不用关心底层数据库类型,通过ORM都给统一输出了API。
二、设置数据库
Django默认数据库使用sqlite3,SQLite是一种嵌入式数据库,它的数据库就是一个文件。由于SQLite本身是C写的,而且体积很小,所以,经常被集成到各种应用程序中,甚至在iOS和Android的App中都可以集成。
Python就内置了SQLite3,所以,在Python中使用SQLite,不需要安装任何东西,直接import导入使用即可。
当你上生产时可能需要更改数据库为MySQL,只需要在 settings.py 中配置即可,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
# sqlite3,默认数据库; DATABASES = { 'default': { 'ENGINE': 'django.db.backends.sqlite3', 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), } } # mysql; DATABASES = { 'default': { 'ENGINE': 'django.db.backends.mysql', 'NAME': 'mydatabase', 'USER': 'mydatabaseuser', 'PASSWORD': 'mypassword', 'HOST': '127.0.0.1', 'PORT': 3306, 'OPTIONS': {'charset': 'utf8mb4'} } } |
接下来就是安装Django连接MySQL数据库要使用的驱动了,Python 2默认使用MySQLdb(已经不支持Python 3)。Python 3可以使用mysqlclient,就是在MySQLdb基础上支持了Python 3,所以操作都一样。默认可能没有安装mysqlclient,直接使用pip安装即可。
1 |
$ pip3 install mysqlclient |
当然,也可以使用PyMySQL驱动,目前是Python 3操作MySQL用的比较多的一个。另外mysqlclient也是PyMySQL作者维护的哦。
如果使用PyMySQL,首先也是使用pip安装,然后在Django应用(startapp)的__init__.py
文件中添加如下两行:
1 2 |
import pymysql pymysql.install_as_MySQLdb() |
这是 pymysql 提供兼容 MySQLdb 的方法,方法很简单,改变了模块的名称。
另外,如果你碰到 mysqlclient 1.3.13 or newer is required; you have 0.9.3 错误,可以重写一下 pymysql 提供的 version_info 变量。
1 |
pymysql.version_info = (1, 3, 13, "final", 0) |
Django支持的后端数据库:https://docs.djangoproject.com/en/2.0/ref/databases
三、编写Model
下面在models.py中创建一个问题表和一个选择表:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
$ cat ./polls/models.py # -*- coding: utf-8 -*- from django.db import models # 投票问题表; class Question(models.Model): question_text = models.CharField(max_length=200) pub_date = models.DateTimeField('date published') # 投票选项表; class Choice(models.Model): question = models.ForeignKey(Question, on_delete=models.CASCADE) choice_text = models.CharField(max_length=200) votes = models.IntegerField(default=0) |
所有模型都是django.db.models.Model类的子类。每个类将被转换为数据库表。每个字段由django.db.models.Field子类(内置在Django core)的实例表示,它们并将被转换为数据库的列。
字段CharField,DateTimeField等等,都是 django.db.models.Field 的子类,包含在Django的核心里面-随时可以使用。在这里,我们仅使用 CharField,TextField,DateTimeField,和 ForeignKey 字段来定义我们的模型。不过在Django提供了更广泛的选择来代表不同类型的数据,例如 IntegerField,BooleanField, DecimalField和其它一些字段。下面会有一些说明。
有些字段需要参数,例如CharField。我们应该始终设定一个 max_length。这些信息将用于创建数据库列。Django需要知道数据库列需要多大。该 max_length参数也将被Django Forms API用来验证用户输入。
在Question模型中,pub_date字段有一个可选参数,auto_now_add设置为True。这将告诉Django创建Question对象时为当前日期和时间。
模型之间的关系使用ForeignKey字段,它将在模型之间创建一个连接,并在数据库级别创建适当的关系(注:外键关联)。该ForeignKey字段需要一个位置参数on_delete,必须指定,表示当外键删除时Django应该怎么做,有很多参数可选。
还有一个可选位置参数related_name,用于引用它关联的模型。例如,在Choice模型中,question字段是Question模型的ForeignKey。它告诉Django,一个Choice实例只涉及一个Question实例。related_name参数将用于创建反向关系,如果设置related_name=choices,表示Question实例可通过choices属性访问属于这个问题下的Choice列表。通俗一点就是在Question那边可以使用Question.choices来查看这个问题有哪些可选答案。
Django自动创建这种反向关系,related_name是可选项。但是,如果我们不为它设置一个名称,Django会自动生成它:(class_name)_set。例如,在Question模型中,所有Choice列表将用choice_set属性表示。而如果我们将其重新命名为了choices,可以使其感觉更自然。如果设置related_name=’+’,这指示Django我们不需要这种反向关系,所以它会被忽略。
这时,我们可以看到,做外键关联时,我们并没有指定关联模型的字段。这是因为Django默认会关联其主键,如果我们没有为模型指定主键,Django会自动为我们生成它。所以现在一切正常。
常见字段说明
CharField(max_length=None, **options)
表示数据库中varchar数据类型,max_length属性用来设置varchar长度。
AutoField
自动递增的主键BigIntegerField类型,使用方式models.AutoField(primary_key=True)。你通常不需要直接使用它,如果没有另外指定,主键字段将自动添加到您的模型中。
BigAutoField
自动递增的主键IntegerField类型,使用方式models.BigAutoField(primary_key=True)。你通常不需要直接使用它,如果没有另外指定,主键字段将自动添加到您的模型中。
DateTimeField(auto_now=False, auto_now_add=False, **options)
表示数据库中的datetime数据类型,在数据库中是datetime(6),表示存储6位毫秒数。由Python datetime.datetime实例表示,如果你用datetime.now()得到的时间就是带6位毫秒数的。如果你显示不需要毫秒,那么读取的时候格式化一下即可。
有一些额外的可选参数:
- auto_now
这个参数的默认值为false,设置为true时,每一次保存对象时,Django都会自动将该字段的值设置为当前时间。一般用来表示 ”最后修改” 时间。需要注意的是,设置该参数为true时,并不简单地意味着字段的默认值为当前时间,而是指字段会被“强制”更新到当前时间,你无法程序中手动为字段赋值;如果使用django自带的admin管理器,那么该字段在admin中是只读的。
虽然每次修改model,都会自动更新时间。但是仅在调用 Model.save() 方法时才会自动更新,以其他方式更新其他字段时,字段不会更新,例如QuerySet.update(),你可以在更新中为字段指定自定义值。
- auto_now_add
这个参数的默认值也为False,设置为True时,会在model对象第一次被创建时,将字段的值设置为创建时的时间,以后修改对象时,字段的值不会再更新。它使用的同样是当前日期,而非默认值。即使你在创建对象时为此字段设置了一个值,它也会被忽略。
该属性通常被用在存储“创建时间”的场景下。与auto_now类似,auto_now_add也具有强制性,一旦被设置为True,就无法在程序中手动为字段赋值,在admin中字段也会成为只读的。
Django 管理后台使用一个带有 Javascript 日历的 <input type=”text”> 来表示该字段,它带有一个当前日期的快捷选择。那个 JavaScript 日历总是以星期天做为一个星期的第一天。如果你希望能够修改此字段,请设置以下内容,而不是auto_now_add=True。
1 2 3 4 5 |
# DateField default = date.today - from datetime.date.today() # DateTimeField default = timezone.now - from django.utils.timezone.now() |
那么问题来了。实际场景中,往往既希望在对象的创建时间默认被设置为当前值,又希望能在日后修改它。怎么实现这种需求呢?
django中所有的model字段都拥有一个default参数,用来给字段设置默认值。可以用default=timezone.now来替换auto_now=True或auto_now_add=True。timezone.now对应着django.utils.timezone.now(),因此需要写成类似下面的形式:
1 2 3 4 5 6 |
from django.db import models import django.utils.timezone as timezone class Doc(models.Model): crtTime = models.DateTimeField('保存日期', default = timezone.now) updTime = models.DateTimeField('最后修改日期', default = timezone.now) |
选项auto_now_add,auto_now和default互斥,这些选项的任何组合都会导致错误。
DateField(auto_now=False, auto_now_add=False, **options)
表示数据库中datetime数据类型。由Python datetime.date实例表示。
TimeField(auto_now=False, auto_now_add=False, **options)
表示数据库中的time数据类型。由Python datetime.time实例表示。
DecimalField(max_digits=None, decimal_places=None, **options)
一个固定精度的十进制数,用Python在Decimal实例中表示。有两个必需的参数:
- max_digits
数字中允许的最大位数。请注意,此数字必须大于或等于decimal_places。
- decimal_places
存储的小数位数。
例如,要存储的数字最大值是999,而带有两个小数位,你可以使用:
1 |
models.DecimalField(..., max_digits=5, decimal_places=2) |
要存储大约是十亿级且带有10个小数位的数字,就这样写:
1 |
models.DecimalField(..., max_digits=19, decimal_places=10) |
Django 管理后台使用 <input type=”text”> (单行输入框) 表示该字段。
FloatField(**options)
表示数据库中的float,存储浮点数。
TextField(**options)
表示数据库中的text字段,最大存储65535 bytes。
BinaryField(**options)
存储二进制数据类型。
EmailField(max_length=254, **options)
表示邮件类型,Django自己封装的,它用于EmailValidator验证输入。
ForeignKey(to, on_delete, **options)
表示数据库中Foreign Key外键约束,Django中只需要指定表,不需要指定关联表字段,因为默认会关联主键。on_delete参数必须指定,表示当删除外键引用对象时Django应该怎么做,有很多参数可选。另外related_name参数用来指定反查管理器的名称,可以不用指定,Django会自动生成它:(class_name)_set。
简单介绍一下on_delete的常用参数:
model.CASCADE
级联删除。 Django模拟SQL约束ON DELETE CASCADE的行为,并删除包含ForeignKey的对象。
model.PROTECT
通过引发ProtectedError(django.db.IntegrityError的子类)异常来防止删除引用的对象。
model.SET_NULL
非级联删除,就是仅仅删除外键引用对象,而不删除外键对象(在数据库层面开启约束的情况下这是不可行的,破坏参照完整性),外键对象字段被默认设置为null。所以,必须设置ForeignKey为null,只有在null为True时才有可以。
model.SET_DEFAULT
与SET_NULL类似,设置ForeignKey为其默认值,必须设置ForeignKey的默认值。
model.DO_NOTHING
不采取任何行动,如果数据库后端强制实施参照完整性,则除非你手动将SQL ON DELETE约束添加到数据库字段,否则将导致IntegrityError。
IntegerField(**options)
表示数据库中int数据类型,default属性用来设置默认值。
BigIntegerField(**options)
表示数据库中bigint数据库类型。
GenericIPAddressField(protocol='both', unpack_ipv4=False, **options)
用来存储IP地址的。字符串格式是IPv4或IPv6地址(例如192.0.2.30或 2a02:42fe::4)。
protocol
限制有效输入到指定的协议。接受的值是’both’(默认值)、’IPv4′ 或’IPv6’,匹配不区分大小写。
unpack_ipv4
解包IPv4映射地址::ffff:192.0.2.1。如果启用此选项,该地址将被解压缩到 192.0.2.1。默认是禁用的。只能在protocol设置为’both’时使用。
如果允许blank值,则必须允许null值,因为blank值存储为null。
URLField(max_length=200,**options)
用来存储URL的,像所有的CharField一样,URLField采用可选 max_length参数,如果你未指定max_length,则使用默认值200。
FilePathField(path=None, match=None, recursive=False, max_length=100, **options)
存储文件路径的。
FileField(upload_to=None, max_length=100, **options)
文件上传字段。不支持primary_key参数,如果使用,将会引发错误。
有两个可选参数:
- upload_to
该属性提供了一种设置上传目录和文件名的方法,可以通过两种方式进行设置。在这两种情况下,该值都传递给Storage.save()方法。
如果你指定一个字符串值,它可能包含strftime()格式,它将被文件上传的日期/时间所取代(这样上传的文件不会填满给定的目录)。例如:
1 2 3 4 5 6 |
class MyModel(models.Model): # file will be uploaded to MEDIA_ROOT/uploads upload = models.FileField(upload_to='uploads/') # or... # file will be saved to MEDIA_ROOT/uploads/2015/01/30 upload = models.FileField(upload_to='uploads/%Y/%m/%d/') |
如果您使用的是默认值FileSystemStorage,则字符串值将被添加到您的MEDIA_ROOT路径中,以形成本地文件系统中将存储上载文件的位置。如果您使用的是不同的存储,请检查存储的文档以了解其处理方式upload_to。
upload_to也可以是可调用的对象,如函数。这将被调用来获取上传路径,包括文件名。这个可调用的方法必须接受两个参数,并返回一个Unix样式的路径(带/斜杠)传递给存储系统。这两个参数是:
参数 | 描述 |
---|---|
instance |
FileField定义模型的一个实例 。更具体地说,这是当前文件被附加的特定实例。 在大多数情况下,这个对象还没有被保存到数据库中,所以如果它使用默认值AutoField,那么它的主键字段可能还没有值。 |
filename | 最初提供给该文件的文件名。在确定最终目的地路径时,这可能会也可能不会被考虑在内。 |
例如:
1 2 3 4 5 6 |
def user_directory_path(instance, filename): # file will be uploaded to MEDIA_ROOT/user_<id>/<filename> return 'user_{0}/{1}'.format(instance.user.id, filename) class MyModel(models.Model): upload = models.FileField(upload_to=user_directory_path) |
ImageField(upload_to=None, height_field=None, width_field=None, max_length=100, **options)
用来存储图片的。继承FileField所有的属性和方法,但也验证上传的对象是有效的图像。另外ImageField也具有height和width属性。为了便于查询这些属性,ImageField有两个额外的可选参数:
- height_field
每次保存模型实例时将自动填充图像高度的模型字段的名称。
- width_field
每次保存模型实例时将使用图像宽度自动填充的模型字段的名称。
需要Pillow库。
ImageField实例在数据库中创建为varchar默认最大长度为100个字符的列。与其他字段一样,您可以使用max_length参数更改最大长度。
BooleanField(**options)
用来表示布尔关系的。如果你需要接受null值,则改为使用NullBooleanField类型。当未定义default属性时BooleanField默认值是None。
ManyToManyField(to, **options)
多对一关系。
OneToOneField(to, on_delete, parent_link=False, **options)
一对一关系。
常见字段选项
每个字段都接受一组与字段有关的属性,例如,CharField需要max_length参数来指定VARCHAR数据库字段的大小。还有一些适用于所有字段的通用参数,这些参数在参考中有详细定义,这里我们只简单介绍一些最常用的:
max_length
用来设置CharField长度。
null
表示是否将数据库中的字段设置为允许将空字段存储为NULL,默认为False。
blank
用来设置此字段是否可以为空值,默认为False。
primary_key
用来设置主键,默认为False。Django会自动添加一个 IntegerField来保存主键,所以你不需要primary_key=True在任意字段上设置 ,除非你想覆盖默认的主键行为。primary_key=True暗示null=False和 unique=True,一个对象只允许有一个主键。
default
用来设置字段的默认值,可以是一个值或者可调用对象,如果可调用,每个新对象被创建它都会被调用。例如给DateTimeField字段设置默认值:
1 |
date = models.DateTimeField(default = timezone.now) |
默认不能是可变对象(模型实例,list,set等),作为该对象的相同实例的引用将被用作在所有新的模型实例的默认值。相反,将所需的默认值包装为可调用的。例如,如果要指定一个默认dict的JSONField,使用函数:
1 2 3 4 |
def contact_default(): return {"email": "to1@example.com"} contact_info = JSONField("ContactInfo", default=contact_default) |
choices
由二项元组构成的一个可迭代对象(例如,列表或元组),用来给字段提供选择项。 如果设置了choices ,默认的表单将是一个选择框而不是标准的文本框,而且这个选择框的选项就是choices中的选项。
每个元组中的第一个元素是要在模型上设置的实际值,第二个元素是人类可读的名称。给定一个模型实例,可以使用该get_FOO_display()
方法访问选择字段的显示值 。例如:
1 2 3 4 5 6 7 8 9 10 |
from django.db import models class Person(models.Model): SHIRT_SIZES = ( ('S', 'Small'), ('M', 'Medium'), ('L', 'Large'), ) name = models.CharField(max_length=60) shirt_size = models.CharField(max_length=1, choices=SHIRT_SIZES) |
1 2 3 4 5 6 |
>>> p = Person(name="Fred Flintstone", shirt_size="L") >>> p.save() >>> p.shirt_size 'L' >>> p.get_shirt_size_display() 'Large' |
unique
用来设置唯一索引约束。
db_column
用于此字段的数据库列的名称。如果没有给出,Django将使用该字段的名称。
如果您的数据库列名是SQL保留字,或者包含Python变量名中不允许使用的字符 – 特别是连字符 – 这没关系。Django在幕后引用列名和表名。
db_index
如果True,将为此字段创建数据库索引。
help_text
表单部件额外显示的帮助信息,即使字段不在表单中使用,它对生成文档也很有用。
详细字段名
每个字段类型除了ForeignKey,ManyToManyField及OneToOneField,都有一个可选的第一个位置参数 – 一个详细名称。如果没有给出详细名称,Django将使用该字段的属性名称自动创建它,并将下划线转换为空格。
在这个例子中,详细名称是:”person’s first name”
1 |
first_name = models.CharField("person's first name", max_length=30) |
在这个例子中,详细名称是:”first name”
1 |
first_name = models.CharField(max_length=30) |
ForeignKey,ManyToMany及FieldOneToOneField要求第一个参数是模型类,所以使用verbose_name关键字参数:
1 2 3 4 5 6 7 8 9 10 11 |
poll = models.ForeignKey( Poll, on_delete=models.CASCADE, verbose_name="the related poll", ) sites = models.ManyToManyField(Site, verbose_name="list of sites") place = models.OneToOneField( Place, on_delete=models.CASCADE, verbose_name="related place", ) |
元数据选项
通过使用内部类 Meta 提供模型元数据,如下所示:
1 2 3 4 5 6 7 8 |
class Choice(models.Model): question = models.ForeignKey(Question, on_delete=models.CASCADE) choice_text = models.CharField(max_length=200) votes = models.IntegerField(default=0) class Meta(object): db_table = "choice" ordering = ['id'] |
Django 模型类的 Meta 是一个内部类,它用于定义一些 Django 模型类的行为特性。而可用的选项大致包含以下几类。
ordering
这个字段是告诉 Django 模型对象返回的记录结果集是按照哪个字段排序的。这是一个字符串的元组或列表,没有一个字符串都是一个字段和用一个可选的表明降序的 ‘-‘ 构成。当字段名前面没有 ‘-‘ 时,将默认使用升序排列。使用 ‘?’ 将会随机排列
1 2 3 4 5 6 7 8 9 10 11 |
# 按订单升序排列 ordering=['order_date'] # 按订单降序排列,-表示降序 ordering=['-order_date'] # 随机排序,?表示随机 ordering=['?order_date'] # 以pub_date为降序,在以author升序排列 ordering=['-pub_date','author'] |
unique_together
这个选项用于设置多字段组合唯一索引,当你需要通过两个字段保持唯一性时使用。比如假设你希望,一个 Person 的 FirstName 和 LastName 两者的组合必须是唯一的,那么需要这样设置:
1 |
unique_together = (("first_name", "last_name"),) |
一个 ManyToManyField 不能包含在 unique_together 中。如果你需要验证关联到 ManyToManyField 字段的唯一验证,尝试使用 signal(信号) 或者明确指定 through 属性。
index_together
这个选项用于设置多字段组合索引。
更多参考官方文档:https://docs.djangoproject.com/en/1.11/topics/db/models
四、Model Migrate
编写完数据模型后需要应用到数据库,Django为了能跟踪表结构的变化,增加了migrations版本控制功能。如果没有该功能,每次表结构变化,就需要重新生成表结构,重新导入数据,非常麻烦。
通过python manage.py makemigrations
生成数据库迁移文件,如下:
1 2 3 4 5 6 |
$ python manage.py makemigrations polls Migrations for 'polls': polls/migrations/0001_initial.py - Create model Choice - Create model Question - Add field question_text to choice |
如果不带App名称,会给所有App创建迁移文件。
通过查看migrations目录,看见增加了一个SQL生成语句的文件:
1 2 |
$ ls ./polls/migrations/ 0001_initial.py __init__.py __init__.pyc |
可以使用下面命令进行查看:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
$ python manage.py sqlmigrate polls 0001_initial BEGIN; -- -- Create model Choice -- CREATE TABLE "polls_choice" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "choice_text" varchar(200) NOT NULL, "votes" integer NOT NULL); -- -- Create model Question -- CREATE TABLE "polls_question" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "question" varchar(200) NOT NULL, "pub_date" datetime NOT NULL); -- -- Add field question_text to choice -- ALTER TABLE "polls_choice" RENAME TO "polls_choice__old"; CREATE TABLE "polls_choice" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "choice_text" varchar(200) NOT NULL, "votes" integer NOT NULL, "question_id" integer NOT NULL REFERENCES "polls_question" ("id")); INSERT INTO "polls_choice" ("choice_text", "votes", "id", "question_text_id") SELECT "choice_text", "votes", "id", NULL FROM "polls_choice__old"; DROP TABLE "polls_choice__old"; CREATE INDEX "polls_choice_question_text_id_905ae7c1" ON "polls_choice" ("question_text_id"); COMMIT; |
从创建表的语句可以看出:
1. 字段默认不允许为空(NOT NULL)。
2. 默认ORM会帮我们创建一个字段名为id的自增主键。
3. 创建外键约束默认会以表的主键作为关联字段,并且会重写字段名。
4. 一个class代表一张表,多对多会产生额外一张关系表,比如用户表和组表就是多对多。
5. 表名默认为$(app_name)_$(class_name)。
五、应用数据库
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
$ python manage.py migrate Operations to perform: Apply all migrations: admin, auth, contenttypes, polls, sessions Running migrations: Applying contenttypes.0001_initial... OK Applying auth.0001_initial... OK Applying admin.0001_initial... OK Applying admin.0002_logentry_remove_auto_add... OK Applying contenttypes.0002_remove_content_type_name... OK Applying auth.0002_alter_permission_name_max_length... OK Applying auth.0003_alter_user_email_max_length... OK Applying auth.0004_alter_user_username_opts... OK Applying auth.0005_alter_user_last_login_null... OK Applying auth.0006_require_contenttypes_0002... OK Applying auth.0007_alter_validators_add_error_messages... OK Applying auth.0008_alter_user_username_max_length... OK Applying polls.0001_initial... OK Applying sessions.0001_initial... OK |
通过结果我们可以看出,当我们编写完model并应用时,会把默认要装载的admin、auth、sessions等模块的model也一同应用了。此时查看sqlite3数据库会发现创建了很多表,包括polls应用的表。
进入sqlite数据库(使用sqlite3命令跟上数据库文件即可):
1 2 3 4 5 6 7 8 |
$ sqlite3 db.sqlite3 sqlite> .tables auth_group django_admin_log auth_group_permissions django_content_type auth_permission django_migrations auth_user django_session auth_user_groups polls_choice auth_user_user_permissions polls_question |
或者使用python manage.py dbshell
命令进入数据库,以SQL的方式操作数据库。
六、操作数据库
一旦你建立好数据模型,Django会自动为你生成一套数据库抽象的API(查询集方法),可以让你创建、检索、更新和删除对象。
Django使用一种直观的方式把数据库表中的数据表示成Python对象:一个模型类代表数据库中的一个表,一个模型类的实例代表这个数据库表中的一条特定的记录。使用关键字参数实例化模型实例来创建一个对象,然后调用save()
把它保存到数据库中。
使用python manage.py shell
命令可以进入到Python交互式窗口,跟你执行Python直接进入交互式窗口是不同的,前者会加载一些环境变量。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
# 导入模块; >>> from django.utils import timezone >>> from polls.models import Question # 创建对象(插入数据); >>> q = Question(question_text='are you sure', pub_date=timezone.now()) # 保存数据到数据库; >>> q.save() # 查询属性(查询字段); >>> q.question_text 'are you sure' >>> q.pub_date datetime.datetime(2017, 10, 9, 10, 55, 7, 235098, tzinfo=<UTC>) |
django.utils提供了一些模块,比如我们这里要使用的timezone,Django默认开启了I18N(I18n是internationalization的缩写,意思指i和n之间有18个字母。是用来支持国际化信息显示。就是支持多种字符集的转换,避免出现乱码。),只能使用timezone,而不能使用python自带的datetime模块之类的,不支持timezone。另外可以看出,通过ORM取的数据都是Python类型,并且都是格式化好的,使用起来很方便。
操作对象(增删改查)
通过模型中的管理器构造一个查询集,来从你的数据库中获取对象。
查询集表示从数据库中取出来的对象的集合,它可以含有零个、一个或者多个过滤器。过滤器基于所给的参数限制查询的结果。 从SQL的角度,查询集和SELECT语句等价,过滤器是像WHERE和LIMIT一样的限制子句。你可以从模型的管理器那里取得查询集,每个模型都至少有一个管理器,它默认命名为objects,通过模型类来直接访问它。
增加
1 2 3 4 5 6 |
q = Question(**kwargs) q.save() # 或者 q = Question.objects.create(**kwargs) |
删除
1 2 3 4 5 6 7 |
q = Question.objects.get(id=1) q.delete() # 或者 Question.objects.all().delete() Question.objects.filter(id__gt=1).delete() |
修改
1 2 3 4 5 6 7 |
q = Question.objects.get(id=1) q.question_text = 'some text' q.save() # 或者 q = Question.objects.filter(id__gt=1).update(question_text='some text') |
再来说说 save() 方法,当我们执行 q.save() 时,Django 执行的是 UPDATE 或者 INSERT 。具体执行什么,遵循如下算法:
1. 如果这个对象已经有主键而且主键的值是 True 的(即不是 None 或者空字符串等),就执行 UPDATE。
2. 如果没有主键或者这条 save() 不会 update 任何字段,那么它就 INSERT。
只有在某些特定情况下,需要强制 save() 执行 INSERT 或 UPDATE 时会带上 force_update=True 或 force_insert=True 参数(比如我要求能 UPDATE 就 UPDATE ,不能我也不去 INSERT,那么我就把这个 force_update 参数设置为 True)。
另外还有一个问题,当它执行 UPDATE 时,采取的就是所有字段都会更新一遍。但很多时候我们可能只需要更新某几个字段,用来提高一下效率或别的用处时就可以用到 update_fields 参数,它可以用来指定哪些字段需要更新,默认是None,这样所有字段都会更新一遍。注意要给它的是一个可迭代对象(比如list等)。如果给它一个空的可迭代对象,那么就什么都不更新(注意和None不同,如果等于None是更新全部字段)。
1 2 |
q.question_text = 'some text' q.save(update_fields=['question_text']) |
查询
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
# 统计条数; q = Question.objects.count() # 获取第一条对象; q = Question.objects.first() # 获取最后一条对象; q = Question.objects.last() # 排序操作,'field'默认是升序,'-filed'表示降序; q = Question.objects.order_by('pub_date') # 获取所有对象; q = Question.objects.all() # 查询结果是list,所以切片操作,获取前10条,不支持负索引,切片可以节约内存; q = Question.objects.all()[:10] # 获取某个字段对象, 结果QuerySet为列表; q = Question.objects.values_list('id') # 获取某个字段对象, 结果QuerySet为字典; q = Question.objects.all().values('id') # 通过属性获取单一对象; q = Question.objects.get(id=1) # 严格匹配,并且区分大小写,等于question_text__exact; q = Question.objects.filter(question_text="are you sure") # 严格匹配,不区分大小写; q = Question.objects.filter(question_text__iexact="ARE you SURE") # 属性值包含you的都符合条件,区分大小写; q = Question.objects.filter(question_text__contains="you") # 属性值包含you的都符合条件,不区分大小写; q = Question.objects.filter(question_text__icontains="you") # 支持正则表达式,区分大小写; q = Question.objects.filter(question_text__regex="^are") # 支持正则表达式,不区分大小写; q = Question.objects.filter(question_text__iregex="^are") # 展示符合某条件但不符合某某条件的数据,其中__gt表示大于,__lt表示小于; q = Question.objects.filter(question_text__contains="some").exclude(id__gt=10) # 展示符合条件的反向结果; q = Question.objects.exclude(question_text__contains="some") |
对于查询有一点要说明,其中不管是用objects.all()
还是用objects.filter()
从数据库中查询出来的结果一般是一个列表集合,这个集合在Django中叫做QuerySet,是一个可迭代对象。如下:
1 2 |
>>> Question.objects.all() <QuerySet [<Question: Question object>, <Question: Question object>, <Question: Question object>]> |
可以看出所有对象都在一个列表中,既然是可迭代,那么就可以遍历了,如下:
1 2 3 4 5 |
>>> for i in Question.objects.all(): ... print(i.question_text) are you sure you you |
也可以使用 values_list() 方法查询某个字段:
1 |
Question.objects.values_list('question_text') |
如果使用objects.get()
方式从数据库中查询结果是一个对象,如下:
1 2 3 4 5 |
>>> q = Question.objects.get(id=1) >>> q <Question: Question object> >>> q.question_text are you sure |
主要区别在于,QuerySet必须迭代才能打印结果,而对象通过实例化之后可以直接获取到实例属性。另外用objects.filter().first()
或objects.filter().last()
也同样获取的是一个对象。
七、给带外键的表插入数据
给带外键的表插入数据时可能跟普通表插入数据不同,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
# 导入类; >>> from polls.models import Question, Choice # 获取id=6的问题; >>> q = Question.objects.get(id=6) >>> q <Question: What sport do you like?> # 给id=6的问题插入几个选项; >>> Choice.objects.create(question=q, choice_text='football') <Choice: Choice object> >>> Choice.objects.create(question=q, choice_text='basketball') <Choice: Choice object> >>> Choice.objects.create(question=q, choice_text='volleyball') <Choice: Choice object> |
我给id=6的“What sport do you like?”问题增加三个选项,现在貌似增删改查都没有问题了,但是对于一个投票系统,最常见的需求就是通过问题查询相关的选项值。这个在Django中当然也提供了相关的方式,称之为“反查询”(内部就是多表join操作),意思就是根据问题查出对象的选项。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
# 实例化一个对象; >>> q = Question.objects.get(id=6) >>> q <Question: What sport do you like?> >>> q.choice_set.all() <QuerySet [<Choice: Choice object>, <Choice: Choice object>, <Choice: Choice object>]> # 实例化一条反查数据; >>> choice = q.choice_set.first() # 查询反查记录相关信息; >>> choice.question <Question: What sport do you like?> >>> choice.id 1 >>> choice.question <Question: What sport do you like?> >>> choice.choice_text 'football' >>> choice.votes 0 |
反查的相关方法跟单表方法都差不多,比如filter、order_by等等,可以多多尝试。
另外对于choice_set是Django ORM帮我们自动生成的一个管理器,就跟objects管理器相同,它默认名称为“表名_set”,也可以自定义名称,创建外键时指定related_name参数即可。
八、给Model增加方法
在model编程中,其实跟我们基于类编程操作都是一样的,比如可以给类增加一些专有方法以及普通方法,比如给Question类增加一个__str__
专有方法,以及普通方法。
在没有__str__
之前,我们不管插入数据还是直接get数据,返回的都是一个对象,不方便调试(也是新手和老手的区别)。操作结果如下:
1 2 3 4 5 |
>>> Question.objects.create(question_text='are you sure', pub_date=timezone.now()) <Question: Question object> >>> Question.objects.get(id=1) <Question: Question object> |
增加一个__str__
专有方法:
1 2 3 4 5 6 |
class Question(models.Model): question_text = models.CharField(max_length=200) pub_date = models.DateTimeField('date published') def __str__(self): return self.question_text |
当我们给类定义了__str__
专有方法之后,再次插入或者get数据时,返回的直接就是 self.question_text 内容,这是Python解释器内置的特殊方法,类似这种特殊方法还有很多。
1 2 3 4 5 |
>>> Question.objects.create(question_text='are you sure', pub_date=timezone.now()) <Question: are you sure> >>> Question.objects.get(id=1) <Question: are you sure> |
然后也可以定义一个普通方法,比如我们定义一个查看是否有最近发布的问题,我这里定义为1天,也就是说只要这一天之类有发布的,就返回真,否则则为假。
1 2 3 4 5 6 7 8 9 10 11 12 |
from django.db import models from django.utils import timezone class Question(models.Model): question_text = models.CharField(max_length=200) pub_date = models.DateTimeField('date published') def __str__(self): return self.question_text def was_published_recently(self): return self.question_text >= timezone.now() - timezone.timedelta(days=1) |
需要注意一点,如果是Python2这里需要使用unicode格式化一下,而Python3则不需要。
1 |
return unicode(self.question_text) >= unicode(timezone.now() - timezone.timedelta(days=1)) |
然后重新执行python manage.py shell
看一下效果:
1 2 3 4 |
>>> from polls.models import Question >>> q = Question.objects.first() >>> q.was_published_recently() True |
九、在View中使用Model
简单完成了View以及Model的学习,下面在View中使用Model。简单的需求就是列出最近3个问题:
1 2 3 4 5 6 7 8 9 |
$ cat ./polls/views.py from django.shortcuts import render from django.http import HttpResponse from polls.models import Question def index(request): latest_question_list = Question.objects.order_by('-pub_date')[:3] output = ','.join([question.question_text for question in latest_question_list]) return HttpResponse(output) |
很简单,就是先从数据库中取数据,然后排个序取出最近前三个问题。然后使用join把从数据库取出的问题以','
分隔拼接成一个字符串,最后返回到页面即可。
插入三条新数据:
1 2 3 |
>>> Question.objects.create(question_text='What sport do you like?', pub_date=timezone.now()) >>> Question.objects.create(question_text='What is your hobby?', pub_date=timezone.now()) >>> Question.objects.create(question_text='are you sure?', pub_date=timezone.now()) |
使用curl访问一下,看看结果:
1 2 |
$ curl http://10.10.0.109:8000/polls/ are you sure?,What is your hobby?,What sport do you like? |
完美!!!
接下来可以多写几个简单的View,比如问题详情页面、支持投票功能、显示投票结果等:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
# ./polls/views.py from django.shortcuts import render from django.http import HttpResponse from polls.models import Question def index(request): latest_question_list = Question.objects.order_by('-pub_date')[:3] output = ','.join([question.question_text for question in latest_question_list]) return HttpResponse(output) def detail(request, question_id): return HttpResponse('Your are looking at question {}'.format(question_id)) def vote(request, question_id): return HttpResponse('Your are voting on question {}'.format(question_id)) def results(request, question_id): return HttpResponse('You are looking at results of question {}'.format(question_id)) # ./polls/urls.py from django.conf.urls import url from . import views urlpatterns = [ url(r'^$', views.index), url(r'^(?P<question_id>[0-9]+)/$', views.detail, name='detail'), url(r'^(?P<question_id>[0-9]+)/results/$', views.results, name='results'), url(r'^(?P<question_id>[0-9]+)/vote/$', views.vote, name='vote'), ] |
然后可以尝试分别访问一下这三个URL,结果如下:
1 2 3 4 5 6 |
$ curl http://10.10.0.109:8000/polls/5/ Your are looking at question 5 $ curl http://10.10.0.109:8000/polls/5/results/ You are looking at results of question 5 $ curl http://10.10.0.109:8000/polls/5/vote/ Your are voting on question 5 |
由结果可以看出,正常带参数的View跟URL简单运行起来是没有问题了。目前View中只是简单的打印一些字符,后面在此基础之上扩展,“Django表单(Form)”。
需要说一下的就是其中URL部分的r'^(?P<question_id>[0-9]+)/$
正则匹配也可以简单写成r'^([0-9]+)/$'
这个样子。前者是使用命名组方式,其到函数中的表现形式为detail(request, question_id=5),而后者没有使用命名组其在函数中的表现形式为detail(request,5),就这点区别。
另外需要说明一点的是,Django从URL中捕获到的参数永远是string类型的,不管你输入的是数值还是字符。所以如果需要其它类型,需要自己接收时转换。