The Django Book

第五章:与数据库的交互:数据建模

在第三章,我们讲述了用 Django 建造网站的基本途径:建立视图和 URLConf 。正如我们所阐述的,视图负责处理 一些任意逻辑 ,然后返回响应结果。在范例中,我们的任意逻辑就是计算当前的日期和时间。

在当代 Web 应用中,任意逻辑经常牵涉到与数据库的交互。 数据库驱动网站 在后台连接数据库服务器,从中取出一些数据,然后在 Web 页面用漂亮的格式展示这些数据。或者,站点也提供让访问者自行填充数据库的功能。

许多复杂的网站都提供了以上两个功能的某种结合。例如 Amazon.com 就是一个数据库驱动站点的良好范例。本质上,每个产品页都是从数据库中取出的数据被格式化为 HTML,而当你发表客户评论时,该评论被插入评论数据库中。

由于先天具备 Python 简单而强大的数据库查询执行方法,Django 非常适合开发数据库驱动网站。本章深入介绍了该功能:Django 数据库层。

(注意:尽管对 Django 数据库层的使用中并不特别强调,我们还是强烈建议掌握一些数据库和 SQL 原理。对这些概念的介绍超越了本书的范围,但就算你是数据库方面的菜鸟,我们也建议你继续阅读。你也许能够跟上进度,并在上下文学习过程中掌握一些概念。)

在视图中进行数据库查询的笨方法

正如第三章详细介绍的那个在视图中输出 HTML 的笨方法(通过在视图里对文本直接硬编码HTML),在视图中也有笨方法可以从数据库中获取数据。很简单:用现有的任何 Python 类库执行一条 SQL 查询并对结果进行一些处理。

在本例的视图中,我们使用了 MySQLdb 类库(可以从 http://www.djangoproject.com/r/python-mysql/ 获得)来连接 MySQL 数据库,取回一些记录,将它们提供给模板以显示一个网页:

from django.shortcuts import render_to_response
import MySQLdb

def book_list(request):
    db = MySQLdb.connect(user='me', db='mydb', passwd='secret', host='localhost')
    cursor = db.cursor()
    cursor.execute('SELECT name FROM books ORDER BY name')
    names = [row[0] for row in cursor.fetchall()]
    db.close()
    return render_to_response('book_list.html', {'names': names})

这个方法可用,但很快一些问题将出现在你面前:

  • 我们将数据库连接参数硬行编码于代码之中。理想情况下,这些参数应当保存在 Django 配置中。

  • 我们不得不重复同样的代码:创建数据库连接、创建数据库游标、执行某个语句、然后关闭数据库。理想情况下,我们所需要应该只是指定所需的结果。

  • 它把我们栓死在 MySQL 之上。如果过段时间,我们要从 MySQL 换到 PostgreSQL,就不得不使用不同的数据库适配器(例如 psycopg 而不是 MySQLdb ),改变连接参数,根据 SQL 语句的类型可能还要修改SQL 。理想情况下,应对所使用的数据库服务器进行抽象,这样一来只在一处修改即可变换数据库服务器。

正如你所期待的,Django数据库层正是致力于解决这些问题。以下提前揭示了如何使用 Django 数据库 API 重写之前那个视图。

from django.shortcuts import render_to_response
from mysite.books.models import Book

def book_list(request):
    books = Book.objects.order_by('name')
    return render_to_response('book_list.html', {'books': books})

我们将在本章稍后的地方解释这段代码。目前而言,仅需对它有个大致的认识。

MTV 开发模式

在钻研更多代码之前,让我们先花点时间考虑下 Django 数据驱动 Web 应用的总体设计。

我们在前面章节提到过,Django 的设计鼓励松耦合及对应用程序中不同部分的严格分割。遵循这个理念的话,要想修改应用的某部分而不影响其它部分就比较容易了。在视图函数中,我们已经讨论了通过模板系统把业务逻辑和表现逻辑分隔开的重要性。在数据库层中,我们对数据访问逻辑也应用了同样的理念。

把数据存取逻辑、业务逻辑和表现逻辑组合在一起的概念有时被称为软件架构的 Model-View-Controller (MVC)模式。在这个模式中, Model 代表数据存取层,View 代表的是系统中选择显示什么和怎么显示的部分,Controller 指的是系统中根据用户输入并视需要访问模型,以决定使用哪个视图的那部分。

为什么用缩写?

像 MVC 这样的明确定义模式的主要用于改善开发人员之间的沟通。与其告诉同事:“让我们对数据存取进行抽象,用单独一层负责数据显示,然后在中间放置一层来进行控制”,还不如利用通用的词汇告诉他们:“让我们在这里使用 MVC 模式吧”。

Django 紧紧地遵循这种 MVC 模式,可以称得上是一种 MVC 框架。以下是 Django 中 M、V 和 C 各自的含义:

  • M ,数据存取部分,由django数据库层处理,本章要讲述的内容。

  • V ,选择显示哪些数据要及怎样显示的部分,由视图和模板处理。

  • C ,根据用户输入委派视图的部分,由 Django 框架通过按照 URLconf 设置,对给定 URL 调用合适的 python 函数来自行处理。

由于 C 由框架自行处理,而 Django 里更关注的是模型(Model)、模板(Template)和视图(Views),Django 也被称为 MTV 框架 。在 MTV 开发模式中:

  • M 代表模型(Model),即数据存取层。该层处理与数据相关的所有事务:如何存取、如何确认有效性、包含哪些行为以及数据之间的关系等。

  • T 代表模板(Template),即表现层。该层处理与表现相关的决定:如何在页面或其他类型文档中进行显示。

  • V 代表视图(View),即业务逻辑层。该层包含存取模型及调取恰当模板的相关逻辑。你可以把它看作模型与模板之间的桥梁。

如果你熟悉其它的 MVC Web开发框架,比方说 Ruby on Rails,你可能会认为 Django 视图是控制器,而 Django 模板是视图。很不幸,这是对 MVC 不同诠释所引起的错误认识。在 Django 对 MVC 的诠释中,视图用来描述要展现给用户的数据;不是数据看起来 怎么样 ,而是要呈现 哪些 数据。相比之下,Ruby on Rails 及一些同类框架提倡控制器负责决定向用户展现哪些数据,而视图则仅决定 如何 展现数据,而不是展现 哪些 数据。

两种诠释中没有哪个更加正确一些。重要的是要理解底层概念。

数据库配置

记住这些理念之后,让我们来开始 Django 数据库层的探索。首先,我们需要搞定一些初始化设置:我们必须告诉 Django 要用哪个数据库服务器及如何连接上它。

我们将假定你已经完成了数据库服务器的安装和激活,并且已经在其中创建了数据库(例如,用 CREATE DATABASE 语句)。SQLite 数据库有点特别,用它的话不需要创建数据库,因为 SQLite 使用文件系统中的单个文件来保存数据。

象前面章节提到的 TEMPLATE_DIRS 一样,数据库配置也是在Django的配置文件里,缺省 是 settings.py 。编辑打开这个文件并查找数据库配置:

DATABASE_ENGINE = ''
DATABASE_NAME = ''
DATABASE_USER = ''
DATABASE_PASSWORD = ''
DATABASE_HOST = ''
DATABASE_PORT = ''

配置纲要如下。

DATABASE_ENGINE 告诉Django使用哪个数据库引擎。如果你在 Django 中使用数据库, DATABASE_ENGINE 必须是 Table 5-1 中所列出的值。

表 5-1. 数据库引擎设置
设置 数据库 适配器
postgresql PostgreSQL psycopg 版本 1.x, http://www.djangoproject.com/r/python-pgsql/1/.
postgresql_psycopg2 PostgreSQL psycopg 版本 2.x, http://www.djangoproject.com/r/python-pgsql/.
mysql MySQL MySQLdb , http://www.djangoproject.com/r/python-mysql/.
sqlite3 SQLite Python 2.5+ 内建。 其他, pysqlite , http://www.djangoproject.com/r/python-sqlite/.
ado_mssql Microsoft SQL Server adodbapi 版本 2.0.1+, http://www.djangoproject.com/r/python-ado/.
oracle Oracle cx_Oracle , http://www.djangoproject.com/r/python-oracle/.

要注意的是无论选择使用哪个数据库服务器,都必须下载和安装对应的数据库适配器。访问表 5-1 中“所需适配器”一栏中的链接,可通过互联网免费获取这些适配器。

DATABASE_NAME 将数据库名称告知 Django 。如果使用 SQLite,请对数据库文件指定完整的文件系统路径。(例如 '/home/django/mydata.db' )。

DATABASE_USER 告诉 Django 用哪个用户连接数据库。如果用SQLite,空白即可。

DATABASE_PASSWORD 告诉Django连接用户的密码。SQLite 用空密码即可。

DATABASE_HOST 告诉 Django 连接哪一台主机的数据库服务器。如果数据库与 Django 安装于同一台计算机(即本机),可将此项保留空白。使用 SQLite ,也可保留空白。

此处的 MySQL 是一个特例。如果使用的是 MySQL 且该项设置值由斜杠( '/' )开头,MySQL 将通过 Unix socket 来连接指定的套接字,例如:

DATABASE_HOST = '/var/run/mysql'

如果用 MySQL 而该项设置的值 不是 以正斜线开始的,系统将假定该项值是主机名。

DATABASE_PORT 告诉 Django 连接数据库时使用哪个端口。如果用SQLite,空白即可。其他情况下,如果将该项设置保留空白,底层数据库适配器将会连接所给定数据库服务器的缺省端口。在多数情况下,使用缺省端口就可以了,因此你可以将该项设置保留空白。

输入完设置后,测试一下配置情况。首先,转到在第二章创建的 mysite 项目目录,运行 python manage.py shell 命令。

你会看到该命令启动了一个 Python 交互界面。运行命令 python manage.py shell 启动的交互界面和 标准的 python 交互界面有很大的区别。看起来都是基本的python外壳(shell), 但是前者告诉Django使用哪个配置文件启动。这对数据库操作来说很关键:Django需要 知道使用哪个配置文件来获得数据库连接信息。

python manage.py shell 假定你的配置文件就在和 manage.py 一样的目录中。 以后将会讲到使用其他的方式来告诉Django使用其他的配置文件。

输入下面这些命令来测试你的数据库配置:

>>> from django.db import connection
>>> cursor = connection.cursor()

如果没有显示什么错误信息,那么你的数据库配置是正确的。否则,你就得 查看错误信息来纠正错误。表 5-2 是一些常见错误。

表 5-2. 数据库配置错误信息
错误信息 解决方案
You havent set the DATABASE_ENGINE setting yet. 设置正确的 DATABASE_ENGINE 配置
Environment variable DJANGO_SETTINGS_MODULE is undefined. 运行命令行 python manage.py shell 而不是 python .
Error loading _____ module: No module named _____. 你没有安装相关的数据库适配器 (例如, psycopgMySQLdb ).
_____ isnt an available database backend. 设置正确的 DATABASE_ENGINE 配置 也许是拼写错误?
database _____ does not exist 设置 DATABASE_NAME 配置到一个已有的数据库, 或者使用 CREATE DATABASE 语句创建数据库。
role _____ does not exist 修改 DATABASE_USER 配置到一个有效用户
could not connect to server 确认 DATABASE_HOSTDATABASE_PORT 设置是正确的,并 确认服务器是在运行的。

你的第一个应用程序

你现在已经确认数据库连接正常工作了,让我们来创建一个 Django app ,开始编码模型和视图。这些文件放置在同一个包中并且形成为一个完整的Django应用程序。

在这里要先解释一些术语,初学者可能会混淆它们。在第二章我们已经创建了 project , 那么 projectapp 之间到底有什么不同呢? 它们的区别就是一个是配置另一个是代码:

一个project包含很多个Django app以及对它们的配置。

技术上,project的作用是提供配置文件,比方说哪里定义数据库连接信息, 安装的app列表, TEMPLATE_DIRS ,等等。

一个app是一套Django功能的集合,通常包括模型和视图,按Python的包结构的方式存在。

例如,Django本身内建有一些app,例如注释系统和自动管理界面。 app的一个关键点是它们是很容易移植到其他project和被多个project重用。

如果你只是建造一个简单的web站点,那么可能你只需要一个app就可以了。如果是复杂的象 电子商务之类的Web站点,你可能需要把这些功能划分成不同的app,以便以后重用。

确实,你还可以不用创建app,例如以前写的视图,只是简单的放在 views.py ,不需要app。

当然,系统对app有一个约定:如果你使用了Django的数据库层(模型),你 必须创建一个django app。模型必须在这个app中存在。因此,为了开始建造 我们的模型,我们必须创建一个新的app。

转到 mysite 项目目录,执行下面的命令来创建一个新app叫做books:

python manage.py startapp books

这个命令没有输出什么,它在 mysite 的目录里创建了一个 books 目录。 让我们来看看这个目录的内容:

books/
    __init__.py
    models.py
    views.py

这些文件里面就包含了这个app的模型和视图。

看一下 models.pyviews.py 文件。它们都是空的,除了 models.py 里有一个 import。

在Python代码里定义模型

我们早些时候谈到。MTV里的M代表模型。Django模型是用Python代码形式表述的数据在数据库 中的定义。对数据层来说它等同于 CREATE TABLE 语句,只不过执行的是Python代码而不是 SQL,而且还包含了比数据库字段定义更多的含义。Django用模型在后台执行SQL代码并把结果 用Python的数据结构来描述,这样你可以很方便的使用这些数据。Django还用模型来描述SQL不能 处理的高级概念。

如果你对数据库很熟悉,你可能马上就会想到,用Python SQL来定义数据模型是不是有点多余? Django这样做是有下面几个原因的:

自省(运行时自动识别数据库)会导致过载和有数据完整性问题。为了提供方便的数据访问API, Django需要以 某种方式 知道数据库层内部信息,有两种实现方式。 第一种方式是用Python明确的定义数据模型,第二种方式是通过运行时扫描数据库来自动侦测识别数据模型。

第二种方式看起来更清晰,因为数据表信息只存放在一个地方-数据库里,但是会带来一些问题。 首先,运行时扫描数据库会带来严重的系统过载。如果每个请求都要扫描数据库的表结构,或者即便是 服务启动时做一次都是会带来不能接受的系统过载。(Django尽力避免过载,而且成功做到了这一点) 其次,有些数据库,例如老版本的MySQL,没有提供足够的元数据来完整地重构数据表。

编写Python代码是非常有趣的,保持用Python的方式思考会避免你的大脑在不同领域来回切换。 这可以帮助你提高生产率。不得不去重复写SQL,再写Python代码,再写SQL,…,会让你头都要裂了。

把数据模型用代码的方式表述来让你可以容易对它们进行版本控制。这样,你可以很容易了解数据层 的变动情况。

SQL只能描述特定类型的数据字段。例如,大多数数据库都没有数据字段类型描述Email地址、URL。 而用Django的模型可以做到这一点。好处就是高级的数据类型带来高生产力和更好的代码重用。

SQL还有在不同数据库平台的兼容性问题。你必须为不同的数据库编写不同的SQL脚本, 而Python的模块就不会有这个问题。

当然,这个方法也有一个缺点,就是Python代码和数据库表的同步问题。如果你修改了一个Django模型, 你要自己做工作来保证数据库和模型同步。我们将在稍后讲解解决这个问题的几种策略。

最后,我们要提醒你Django提供了实用工具来从现有的数据库表中自动扫描生成模型。 这对已有的数据库来说是非常快捷有用的。

你的第一个模型

在本章和后续章节里,我们将集中到一个基本的 书籍/作者/出版商 数据层上。我们这样做是因为 这是一个众所周知的例子,很多SQL有关的书籍也常用这个举例。你现在看的这本书也是由作者 创作再由出版商出版的哦!

我们来假定下面的这些概念、字段和关系:

  • 作者有尊称(例如,先生或者女士),姓,名,还有Email地址,头像。

  • 出版商有名称,地址,所在城市、省,国家,网站。

  • 书籍有书名和出版日期。它有一个或多个作者(和作者是多对多的关联关系[many-to-many]), 只有一个出版商(和出版商是一对多的关联关系[one-to-many],也被称作外键[foreign key])

第一步是用Python代码来描述它们。打开 models.py 并输入下面的内容:

from django.db import models

class Publisher(models.Model):
    name = models.CharField(maxlength=30)
    address = models.CharField(maxlength=50)
    city = models.CharField(maxlength=60)
    state_province = models.CharField(maxlength=30)
    country = models.CharField(maxlength=50)
    website = models.URLField()

class Author(models.Model):
    salutation = models.CharField(maxlength=10)
    first_name = models.CharField(maxlength=30)
    last_name = models.CharField(maxlength=40)
    email = models.EmailField()
    headshot = models.ImageField(upload_to='/tmp')

class Book(models.Model):
    title = models.CharField(maxlength=100)
    authors = models.ManyToManyField(Author)
    publisher = models.ForeignKey(Publisher)
    publication_date = models.DateField()

让我们来快速讲解一下这些代码的含义。首先要注意的事是每个数据模型都是 django.db.models.Model 的子类。它的父类 Model 包含了所有和数据库 打交道的方法,并提供了一个简洁漂亮的定义语法。不管你相信还是不相信, 这就是我们用Django写的数据基本存取功能的全部代码。

每个模型相当于单个数据库表,每个属性也是这个表中的一个字段。 属性名就是字段名,它的类型(例如 CharField )相当于数据库的字段类型 (例如 varchar )。例如, Publisher 模块等同于下面这张表(用Postgresql 的 CREATE TABLE 语法描述):

CREATE TABLE "books_publisher" (
    "id" serial NOT NULL PRIMARY KEY,
    "name" varchar(30) NOT NULL,
    "address" varchar(50) NOT NULL,
    "city" varchar(60) NOT NULL,
    "state_province" varchar(30) NOT NULL,
    "country" varchar(50) NOT NULL,
    "website" varchar(200) NOT NULL
);

事实上,正如过一会儿我们所要展示的,Django 可以自动生成这些 CREATE TABLE 语句。

“每个数据库表对应一个类”这条规则的例外情况是多对多关系。在我们的范例模型中, Book 有一个 多对多字段 叫做 authors 。 该字段表明一本书籍有一个或多个作者,但 Book 数据库表却并没有 authors 字段。相反,Django创建了一个额外的表(多对多连接表)来处理书籍和作者之间的映射关系。

请查看附录 B 了解所有的字段类型和模型语法选项。

最后需要注意的是:我们并没有显式地为这些模型定义任何主键。除非你指定,否则 Django 会自动为每个模型创建一个叫做 id 的主键。每个 Django 模型必须要有一个单列主键。

模型安装

完成这些代码之后,现在让我们来在数据库中创建这些表。要完成该项工作,第一步是在 Django 项目中 激活 这些模型。将 books app 添加到配置文件的已 installed apps 列表中即可完成此步骤。

再次编辑 settings.py 文件, 找到 INSTALLED_APPS 设置。 INSTALLED_APPS 告诉 Django 项目哪些 app 处于激活状态。缺省情况下如下所示:

INSTALLED_APPS = (
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.sites',
)

把这四个设置前面加#临时注释起来。(它们是一些缺省的公用设置,现在先不管 它们,以后再来讨论)同样的,修改缺省的 MIDDLEWARE_CLASSESTEMPLATE_CONTEXT_PROCESSORS 设置,都注释起来。 然后添加 'mysite.books'INSTALLED_APPS 列表,现在看起来是这样:

MIDDLEWARE_CLASSES = (
#    'django.middleware.common.CommonMiddleware',
#    'django.contrib.sessions.middleware.SessionMiddleware',
#    'django.contrib.auth.middleware.AuthenticationMiddleware',
#    'django.middleware.doc.XViewMiddleware',
)

TEMPLATE_CONTEXT_PROCESSORS = ()
#...

INSTALLED_APPS = (
    #'django.contrib.auth',
    #'django.contrib.contenttypes',
    #'django.contrib.sessions',
    #'django.contrib.sites',
    'mysite.books',
)

(尽管这是单个tuple元素,我们也不要忘了结尾的逗号[,]。 另外,本书的作者喜欢在 每一个 tuple元素后面加一个逗号,不管它是不是 只有一个元素。这是为了避免忘了加逗号)

'mysite.books' 标识 books app。 INSTALLED_APPS 中的每个app都用 Python的路径描述,包的路径,用小数点(.)区分。

现在我们可以创建数据库表了。首先,用下面的命令对校验模型的有效性:

python manage.py validate

validate 命令检查你的模型的语法和逻辑是否正确。如果一切正常,你会看到 0 errors found 消息。如果有问题,它会给出非常有用的错误信息来帮助你 修正你的模型。

一旦你觉得你的模型可能有问题,运行 python manage.py validate 。 它可以帮助你捕获一些常见的模型定义错误。

模型确认没问题了,运行下面的命令来生成 CREATE TABLE 语句:

python manage.py sqlall books

在这个命令行中, books 是app的名称。和你运行 manage.py startapp 中的一样。 运行命令的结果是这样的:

BEGIN;
CREATE TABLE "books_publisher" (
    "id" serial NOT NULL PRIMARY KEY,
    "name" varchar(30) NOT NULL,
    "address" varchar(50) NOT NULL,
    "city" varchar(60) NOT NULL,
    "state_province" varchar(30) NOT NULL,
    "country" varchar(50) NOT NULL,
    "website" varchar(200) NOT NULL
);
CREATE TABLE "books_book" (
    "id" serial NOT NULL PRIMARY KEY,
    "title" varchar(100) NOT NULL,
    "publisher_id" integer NOT NULL REFERENCES "books_publisher" ("id"),
    "publication_date" date NOT NULL
);
CREATE TABLE "books_author" (
    "id" serial NOT NULL PRIMARY KEY,
    "salutation" varchar(10) NOT NULL,
    "first_name" varchar(30) NOT NULL,
    "last_name" varchar(40) NOT NULL,
    "email" varchar(75) NOT NULL,
    "headshot" varchar(100) NOT NULL
);
CREATE TABLE "books_book_authors" (
    "id" serial NOT NULL PRIMARY KEY,
    "book_id" integer NOT NULL REFERENCES "books_book" ("id"),
    "author_id" integer NOT NULL REFERENCES "books_author" ("id"),
    UNIQUE ("book_id", "author_id")
);
CREATE INDEX books_book_publisher_id ON "books_book" ("publisher_id");
COMMIT;

注意:

  • 自动生成的表名是app名称( books )和模型的小写名称 ( publisher , book , author )的组合。 你可以指定不同的表名,详情请看附录 B。

  • 我们前面已经提到,Django为自动加了一个 id 主键,你一样可以修改它。

  • 按约定,Django添加 "_id" 后缀到外键字段名。这个同样也是可自定义的。

  • 外键是用 REFERENCES 语句明确定义的。

  • 这些 CREATE TABLE 语句会根据你的数据库而作调整,这样象数据库特定的一些字段例如: auto_increment (MySQL), serial (PostgreSQL), integer primary key (SQLite) 可以自动处理。 同样的,字段名称的引号也是自动处理(例如单引号还是双引号)。 这个给出的例子是Postgresql的语法。

sqlall 命令并没有在数据库中真正创建数据表,只是把SQL语句段打印出来。 你可以把这些语句段拷贝到你的SQL客户端去执行它。当然,Django提供了更简单的 方法来执行这些SQL语句。运行 syncdb 命令:

python manage.py syncdb

你将会看到这样的内容:

Creating table books_publisher
Creating table books_book
Creating table books_author
Installing index for books.Book model

syncdb 命令是同步你的模型到数据库的一个简单方法。它会根据 INSTALLED_APPS 里设置的app来检查数据库, 如果表不存在,它就会创建它。 需要注意的是, syncdb不能 同步模型的修改到数据库。如果你修改了模型,然后你想更新 数据库, syncdb 是帮不了你的。(稍后我们再讲这些。)

如果你再次运行 python manage.py syncdb ,什么也没发生,因为你没有添加新的模型或者 添加新的app。所以,运行 python manage.py syncdb 总是安全的,它不会把事情搞砸。

如果你有兴趣,花点时间用你的SQL客户端登录进数据库服务器看看刚才Django创建的数据表。 Django带有一个命令行工具, python manage.py dbshell

基本数据访问

一旦你创建了模型,Django自动为这些模型提供了高级的Pyhton API。 运行 python manage.py shell 并输入下面的内容试试看:

>>> from books.models import Publisher
>>> p1 = Publisher(name='Addison-Wesley', address='75 Arlington Street',
...     city='Boston', state_province='MA', country='U.S.A.',
...     website='http://www.apress.com/')
>>> p1.save()
>>> p2 = Publisher(name="O'Reilly", address='10 Fawcett St.',
...     city='Cambridge', state_province='MA', country='U.S.A.',
...     website='http://www.oreilly.com/')
>>> p2.save()
>>> publisher_list = Publisher.objects.all()
>>> publisher_list
[<Publisher: Publisher object>, <Publisher: Publisher object>]

这短短几行代码干了不少的事。这里简单的说一下:

  • 要创建对象,只需 import 相应模型类,并传入每个字段值将其实例化。

  • 调用该对象的 save() 方法,将对象保存到数据库中。Django 会在后台执行一条 INSERT 语句。

  • 使用属性 Publisher.objects 从数据库中获取对象。调用 Publisher.objects.all() 获取数据库中所有的 Publisher 对象。此时,Django 在后台执行一条 SELECT SQL语句。

自然,你肯定想执行更多的Django数据库API试试看,不过,还是让我们先解决一点烦人的小问题。

添加模块的字符串表现

当我们打印整个publisher列表时,我们没有得到想要的有用的信息:

[<Publisher: Publisher object>, <Publisher: Publisher object>]

我们可以简单解决这个问题,只需要添加一个方法 __str__()Publisher 对象。 __str__() 方法告诉Python要怎样把对象当作字符串来使用。 请看下面:

from django.db import models

class Publisher(models.Model):
    name = models.CharField(maxlength=30)
    address = models.CharField(maxlength=50)
    city = models.CharField(maxlength=60)
    state_province = models.CharField(maxlength=30)
    country = models.CharField(maxlength=50)
    website = models.URLField()

    **def __str__(self):**
        **return self.name**

class Author(models.Model):
    salutation = models.CharField(maxlength=10)
    first_name = models.CharField(maxlength=30)
    last_name = models.CharField(maxlength=40)
    email = models.EmailField()
    headshot = models.ImageField(upload_to='/tmp')

    **def __str__(self):**
        **return '%s %s' % (self.first_name, self.last_name)**

class Book(models.Model):
    title = models.CharField(maxlength=100)
    authors = models.ManyToManyField(Author)
    publisher = models.ForeignKey(Publisher)
    publication_date = models.DateField()

    **def __str__(self):**
        **return self.title**

就象你看到的一样, __str__() 方法返回一个字符串。 __str__() 必须返回字符串, 如果是其他类型,Python将会抛出 TypeError 错误消息 "__str__ returned non-string" 出来。

为了让我们的修改生效,先退出Python Shell,然后再次运行 python manage.py shell 进入。 现在列出 Publisher 对象就很容易理解了:

>>> from books.models import Publisher
>>> publisher_list = Publisher.objects.all()
>>> publisher_list
[<Publisher: Addison-Wesley>, <Publisher: O'Reilly>]

请确保你的每一个模型里都包含 __str__() 方法,这不只是为了交互时方便,也是因为 Django会在其他一些地方用 __str__() 来显示对象。

最后, __str()__ 也是一个很好的例子来演示我们怎么添加 行为 到模型里。 Django的模型不只是为对象定义了数据库表的结构,还定义了对象的行为。 __str__() 就是一个例子来演示模型知道怎么显示它们自己。

插入和更新数据

你已经知道怎么做了:先使用一些关键参数创建对象实例,如下:

>>> p = Publisher(name='Apress',
...         address='2855 Telegraph Ave.',
...         city='Berkeley',
...         state_province='CA',
...         country='U.S.A.',
...         website='http://www.apress.com/')

这个对象实例并 没有 对数据库做修改。

要保存这个记录到数据库里(也就是执行 INSERT SQL 语句),调用对象的 save() 方法:

>>> p.save()

在SQL里,这大致可以转换成这样:

INSERT INTO book_publisher
    (name, address, city, state_province, country, website)
VALUES
    ('Apress', '2855 Telegraph Ave.', 'Berkeley', 'CA',
     'U.S.A.', 'http://www.apress.com/');

因为 Publisher 模型有一个自动增加的主键 id ,所以第一次调用 save() 还多做了一件事: 计算这个主键的值并把它赋值给这个对象实例:

>>> p.id
52    # this will differ based on your own data

接下来再调用 save() 将不会创建新的记录,而只是修改记录内容(也就是 执行 UPDATE SQL语句,而不是 INSERT 语句):

>>> p.name = 'Apress Publishing'
>>> p.save()

前面执行的 save() 相当于下面的SQL语句:

UPDATE book_publisher SET
    name = 'Apress Publishing',
    address = '2855 Telegraph Ave.',
    city = 'Berkeley',
    state_province = 'CA',
    country = 'U.S.A.',
    website = 'http://www.apress.com'
WHERE id = 52;

选择对象

我们已经知道查找所有数据的方法了:

>>> Publisher.objects.all()
[<Publisher: Addison-Wesley>, <Publisher: O'Reilly>, <Publisher: Apress Publishing>]

这相当于这个SQL语句:

SELECT
    id, name, address, city, state_province, country, website
FROM book_publisher;

注意

注意到Django在选择所有数据时并没有使用 SELECT* ,而是显式列出了所有字段。 就是这样设计的: SELECT* 会更慢,而且最重要的是列出所有字段遵循了Python 界的一个信条:明确比不明确好。

有关Python之禅(戒律) :-),在Python提示行输入 import this 试试看。

让我们来仔细看看 Publisher.objects.all() 这行的每个部分:

首先,我们有一个已定义的模型 Publisher 。没什么好奇怪的:你想要查找数据, 你就用模型来获得数据。

其次, objects 是干什么的?技术上,它是一个 管理器(manager) 。 管理器 将在附录B详细描述,在这里你只要知道它处理有关数据表的操作,特别是数据查找。

所有的模型都自动拥有一个 objects 管理器;你可以在想要查找数据时是使用它。

最后,还有 all() 方法。这是 objects 管理器返回所有记录的一个方法。 尽管这个对象 看起来 象一个列表(list),它实际是一个 QuerySet 对象, 这个对象是数据库中一些记录的集合。附录C将详细描述QuerySet,现在,我们 就先当它是一个仿真列表对象好了。

所有的数据库查找都遵循一个通用模式:调用模型的管理器来查找数据。

数据过滤

如果想要获得数据的一个子集,我们可以使用 filter() 方法:

>>> Publisher.objects.filter(name="Apress Publishing")
[<Publisher: Apress Publishing>]

filter() 根据关键字参数来转换成 WHERE SQL语句。前面这个例子 相当于这样:

SELECT
    id, name, address, city, state_province, country, website
FROM book_publisher
WHERE name = 'Apress Publishing';

你可以传递多个参数到 filter() 来缩小选取范围:

>>> Publisher.objects.filter(country="U.S.A.", state_province="CA")
[<Publisher: Apress Publishing>]

多个参数会被转换成 AND SQL语句,例如象下面这样:

SELECT
    id, name, address, city, state_province, country, website
FROM book_publisher
WHERE country = 'U.S.A.' AND state_province = 'CA';

注意,SQL缺省的 = 操作符是精确匹配的,其他的查找类型如下:

>>> Publisher.objects.filter(name__contains="press")
[<Publisher: Apress Publishing>]

namecontains 之间有双下划线。象Python自己一样,Django也使用 双下划线来做一些小魔法,这个 __contains 部分会被Django转换成 LIKE SQL语句:

SELECT
    id, name, address, city, state_province, country, website
FROM book_publisher
WHERE name LIKE '%press%';

其他的一些查找类型有: icontains (大小写无关的 LIKE ), startswithendswith , 还有 range (SQL BETWEEN 查询)。 附录C详细列出了这些类型的详细资料。

获取单个对象

有时你只想获取单个对象,这个时候使用 get() 方法:

>>> Publisher.objects.get(name="Apress Publishing")
<Publisher: Apress Publishing>

这样,就返回了单个对象,而不是列表(更准确的说,QuerySet)。 所以,如果结果是多个对象,会导致抛出异常:

>>> Publisher.objects.get(country="U.S.A.")
Traceback (most recent call last):
    ...
AssertionError: get() returned more than one Publisher -- it returned 2!

如果查询没有返回结果也会抛出异常:

>>> Publisher.objects.get(name="Penguin")
Traceback (most recent call last):
    ...
DoesNotExist: Publisher matching query does not exist.

数据排序

在运行前面的例子中,你可能已经注意到返回的结果是无序的。我们还没有告诉数据库 怎样对结果进行排序,所以我们返回的结果是无序的。

当然,我们不希望在页面上列出的出版商的列表是杂乱无章的。我们用 order_by() 来 排列返回的数据:

>>> Publisher.objects.order_by("name")
[<Publisher: Apress Publishing>, <Publisher: Addison-Wesley>, <Publisher: O'Reilly>]

跟以前的 all() 例子差不多,SQL语句里多了指定排序的部分:

SELECT
    id, name, address, city, state_province, country, website
FROM book_publisher
ORDER BY name;

我们可以对任意字段进行排序:

>>> Publisher.objects.order_by("address")
[<Publisher: O'Reilly>, <Publisher: Apress Publishing>, <Publisher: Addison-Wesley>]

>>> Publisher.objects.order_by("state_province")
[<Publisher: Apress Publishing>, <Publisher: Addison-Wesley>, <Publisher: O'Reilly>]

多个字段也没问题:

>>> Publisher.objects.order_by("state_provice", "address")
 [<Publisher: Apress Publishing>, <Publisher: O'Reilly>, <Publisher: Addison-Wesley>]

我们还可以指定逆向排序,在前面加一个减号 - 前缀:

>>> Publisher.objects.order_by("-name")
[<Publisher: O'Reilly>, <Publisher: Apress Publishing>, <Publisher: Addison-Wesley>]

每次都要用 order_by() 显得有点啰嗦。 大多数时间你通常只会对某些 字段进行排序。在这种情况下,Django让你可以指定模型的缺省排序方式:

class Publisher(models.Model):
    name = models.CharField(maxlength=30)
    address = models.CharField(maxlength=50)
    city = models.CharField(maxlength=60)
    state_province = models.CharField(maxlength=30)
    country = models.CharField(maxlength=50)
    website = models.URLField()

    def __str__(self):
        return self.name

    **class Meta:**
        **ordering = ["name"]**

这个 ordering = ["name"] 告诉Django如果没有显示提供 order_by() , 就缺省按名称排序。

Meta是什么?

Django使用内部类Meta存放用于附加描述该模型的元数据。 这个类完全可以不实现,不过他能做很多非常有用的事情。查看附录B,在Meta项下面,获得更多选项信息,

排序

你已经知道怎么过滤数据了,现在让我们来排序它们。你可以同时做这 过滤和排序,很简单,就象这样:

>>> Publisher.objects.filter(country="U.S.A.").order_by("-name")
[<Publisher: O'Reilly>, <Publisher: Apress Publishing>, <Publisher: Addison-Wesley>]

你应该没猜错,转换成SQL查询就是 WHEREORDER BY 的组合:

SELECT
    id, name, address, city, state_province, country, website
FROM book_publisher
WHERE country = 'U.S.A'
ORDER BY name DESC;

你可以任意把它们串起来,多长都可以,这里没有限制。

限制返回的数据

另一个常用的需求就是取出固定数目的记录。想象一下你有成千上万的出版商在你的数据库里, 但是你只想显示第一个。你可以这样做:

>>> Publisher.objects.all()[0]
<Publisher: Addison-Wesley>

这相当于:

SELECT
    id, name, address, city, state_province, country, website
FROM book_publisher
ORDER BY name
LIMIT 1;

还有更多

我们只是刚接触到模型的皮毛,你还必须了解更多的内容以便理解以后的范例。 具体请看附录C。

删除对象

要删除对象,只需简单的调用对象的 delete() 方法:

>>> p = Publisher.objects.get(name="Addison-Wesley")
>>> p.delete()
>>> Publisher.objects.all()
[<Publisher: Apress Publishing>, <Publisher: O'Reilly>]

你还可以批量删除对象,通过对查询的结果调用 delete() 方法:

>>> publishers = Publisher.objects.all()
>>> publishers.delete()
>>> Publisher.objects.all()
[]

注意

删除是 不可恢复 的,所以要小心操作!事实上,应该尽量避免删除对象,除非你 确实需要删除它。数据库的数据恢复的功能通常不太好,而从备份数据恢复是很痛苦的。

通常更好的方法是给你的数据模型添加激活标志。你可以只在激活的对象中查找, 对于不需要的对象,将激活字段值设为 False , 而不是删除对象。这样, 如果一旦你认为做错了的话,只需把标志重设回来就可以了。

修改数据库表结构

当我们在这一章的前面介绍 syncdb 命令的时候,我们强调 syncdb 仅仅创建数据库中不存在的表,而不会同步模型的修改或者删除到数据库。如果你添加或者修改了模型的一个字段,或者删除一个模型,你必须手动改变你的数据库。下面我们看看怎么来做。

当我们处理表结构的修改时,要时刻想着 Django 的数据库层是如何工作的:

  • 如果模型中包含一个在数据库中并不存在的字段,Django会大声抱怨的。这样当你第一次调用Django的数据库API来查询给定的表时就会出错(也就是说,它会在执行的时候出错,而不是编译的时候)

  • Django并不关心数据库表中是否存在没有在模型中定义的列

  • Django并不关心数据库中是否包含没有被模型描述的表

修改表结构也就是按照正确的顺序修改各种Python代码和数据库本身

添加字段

当按照产品需求向一个表/模型添加字段时,Django不关心一个表的列是否在模型中定义,我们可以利用这个小技巧,先在数据库中添加列,然后再改变模型中对应的字段。

然而,这里总是存在先有鸡还是先有蛋的问题,为了弄清新的数据列怎么用SQL描述,你需要查看 manage.py sqlall 的执行结果,它列出了模型中已经存在的字段。(注意:你不需要像Django中的SQL一模一样的创建你的列,但是这确实是一个好主意,从而保证所有都是同步的)

解决鸡和蛋的问题的方法就是先在开发环境而不是发布服务器上修改。(你现在用的就是测试/开发环境,不是吗?)下面是详细的步骤。

首先,在开发环境中执行下面的步骤(也就是说,不是在发布服务器上):

  1. 把这个字段添加到你的模型中.

  1. 运行 manage.py sqlall [yourapp] 会看到模型的新的 CREATE TABLE 语句。 注意新的字段的列定义。

  1. 启动您的数据库交互shell(也就是 psqlmysql , 或者您也可以使用 manage.py dbshell )。 执行一个 ALTER TABLE 语句,添加您的新列。

4. (可选)用 manage.py shell 启动Python交互式shell,并通过引入模型并选择表 验证新的字段已被正确添加(比如, MyModel.objects.all()[:5] )。

然后在发布服务器上执行下面的步骤:

  1. 启动你的数据库的交互式命令行;

  1. 执行 ALTER TABLE 语句,也就是在开发环境中第3步执行的语句;

  1. 添加字段到你的模型中。如果你在开发时使用了版本控制系统并checkin了你的修改,现在可以更新 代码到发布服务器上了(例如,使用Subverison的话就是 svn update )。

  1. 重启Web服务器以使代码修改生效。

例如,让我们通过给 Book 模型添加一个 num_pages 字段来演示一下。 首先,我们在开发环境中这样修改模型:

class Book(models.Model):
    title = models.CharField(maxlength=100)
    authors = models.ManyToManyField(Author)
    publisher = models.ForeignKey(Publisher)
    publication_date = models.DateField()
    **num_pages = models.IntegerField(blank=True, null=True)**

    def __str__(self):
        return self.title

(注意:我们这里为什么写 blank=Truenull=True 呢?阅读题为“添加非空字段”的侧边栏获取更多信息。)

然后我们运行命令 manage.py sqlall books 来得到 CREATE TABLE 语句。它们看起来 是这样的:

CREATE TABLE "books_book" (
    "id" serial NOT NULL PRIMARY KEY,
    "title" varchar(100) NOT NULL,
    "publisher_id" integer NOT NULL REFERENCES "books_publisher" ("id"),
    "publication_date" date NOT NULL,
    "num_pages" integer NULL
);

新加的字段SQL描述是这样的:

"num_pages" integer NULL

接下来,我们启动数据库交互命令界面,例如Postgresql是执行 psql , 并执行下面的语句:

ALTER TABLE books_book ADD COLUMN num_pages integer;

添加非空字段(NOT NULL)

这里有一个要注意的地方。在添加 num_pages 字段时我们使用了 blank=Truenull=True 可选项。 我们之所以这么做是因为在数据库创建时我们想允许字段值为NULL。

当然,也可以在添加字段时设置值不能为NULL。要实现这个,你不得不先创建一个 NULL 字段, 使用缺省值,再修改字段到 NOT NULL 。例如:

BEGIN;
ALTER TABLE books_book ADD COLUMN num_pages integer;
UPDATE books_book SET num_pages=0;
ALTER TABLE books_book ALTER COLUMN num_pages SET NOT NULL;
COMMIT;

如果你这样做了, 记得要把 blank=Truenull=True 从你的模型中拿掉。

执行完 ALTER TABLE 语句, 我们确认一下修改是否正确,启动Python交互界面并执行下面语句:

>>> from mysite.books.models import Book
>>> Book.objects.all()[:5]

如果没有错误,我们就可以转到发布服务器来在数据库上执行 ALTER TABLE 语句了。然后, 再更新模型并重启WEB服务器。

删除字段

从模型里删除一个字段可要比增加它简单多了。删除一个字段仅需要做如下操作:

从你的模型里删除这个字段,并重启Web服务器。

使用如下面所示的命令,从你的数据库中删掉该列:

ALTER TABLE books_book DROP COLUMN num_pages;

删除 Many-to-Many 字段

因为many-to-many字段同普通字段有些不同,它的删除过程也不一样:

删除掉你的模型里的 ManyToManyField ,并且重启Web服务器。

使用如下面所示的命令,删除掉你数据库里的many-to-many表:

DROP TABLE books_books_publishers;

删除模型

完全删除一个模型就像删除一个字段一样简单。删除模型仅需要做如下步骤:

将此模型从你的 models.py 文件里删除,并且重启Web服务器。

使用如下的命令,将此表从你的数据库中删除:

DROP TABLE books_book;

下一步?

一旦你定义了你的模型,接下来就是要把数据导入数据库里了。你可能已经有现成的数据了,请看第十六章,如何集成现有的数据库。也可能数据是用户提供的,第七章中还会教你怎么处理用户提交的数据。

有时候,你和你的团队成员也需要手工输入数据,这时候如果能有一个基于Web的数据输入和管理的界面 就很有帮助。下一章将讲述Django的管理界面,它就是专门干这个活的。

Copyright 2006 Adrian Holovaty and Jacob Kaplan-Moss.
This work is licensed under the GNU Free Document License.
Hosting graciously provided by media temple
Chinese translate hosting by py3k.cn.