At the Forge - 使用 Django 进行数据库建模
在过去的几个月里,本专栏一直在探讨 Django,这是一个流行的开源 Web 开发框架,使用 Python 编写。Django 有时被描述为 Ruby on Rails 的竞争对手,或 Python 版本的 Rails,但正如 Python 和 Ruby 是不同的语言,各有其优点和缺点一样,Django 和 Rails 也是不同的框架,各有其权衡取舍。
如果您一直在关注关于 Django 的系列专栏,您已经了解了如何下载和安装 Django 软件,如何创建和配置站点和应用程序,甚至是如何创建视图(处理业务逻辑的 Python 方法)和模板(带有特殊规则的 HTML 文件,用于插入变量和动态内容)。凭借我们目前所了解的一切,您大概可以创建一个有趣的动态 Web 应用程序。
然而,大多数现代 Web 应用程序还有另一个组件,即关系数据库,它们依赖该数据库进行数据存储和检索。当然,您可以将所有内容存储在文件系统甚至内存中,但对于我们大多数人来说,关系数据库是阻力最小的路径,既能确保数据的安全性,又能在检索数据方面提供极大的灵活性。
那么,本月的专栏将着眼于 Django 程序员如何在数据库中存储和检索信息。如果您只从 PHP 或 CGI 程序中使用过数据库,您会对 Django 提供的自动化程度感到惊讶和印象深刻。如果您使用过 Ruby on Rails,您可能会认为 Django 程序员工作得太辛苦了——对此,Django 黑客会说,他们希望完全控制自己的应用程序,而不是依赖幕后魔法。
Django 世界中的术语“模型”描述了一个 Python 对象,该对象具有持久状态,大概存储在关系数据库中。我们不需要使用模型将数据库集成到 Django 中,但如果我们只是将 SQL 查询粘贴到模板中,那将是困难的(更不用说不美观了)。因此,我们转而使用 Django 内置的对象关系映射器,仅使用视图和模板中的对象。映射器的任务是将我们的方法调用转换为 SQL,然后将生成的数据库响应转换为 Python 对象。
但是,在我们甚至可以创建模型对象之前,我们首先必须有一个数据库表,该对象将连接到该表。Django 要求我们使用 Python 代码定义模型,描述表的名称、字段,甚至是一些默认值。
如果我们对保留上个月开始的博客应用程序感兴趣,我们可能会在 PostgreSQL 中定义我们的表如下
CREATE TABLE Posting ( id SERIAL NOT NULL, title TEXT NOT NULL, body TEXT NOT NULL, posted_at TIMESTAMP NOT NULL DEFAULT NOW(), PRIMARY KEY(id) );
但在 Django 中,我们不直接创建上述 SQL。相反,我们使用 Python 为我们创建它。例如,我们可以使用 Django 将上述表定义如下
from django.db import models class Posting(models.Model): title = models.CharField(maxlength=30) body = models.TextField() publication_date = models.DateTimeField()
我们的模型是一个 Python 类,它继承自 django.db.models.Model。我们使用从 django.db.models 导入的对象,为每个字段定义特定的类型。如上所示,其中一些数据类型可以通过传递参数来限制或修改其默认值。有些类型的定义是专门因为它们具有内置限制,例如 EmailField,它必须是有效的电子邮件地址。通过使用 ManyToManyField 和 ForeignKey 对象定义列,可以定义表之间的各种关系。
上面的代码应该放在 models.py 中,这是一个 Python 文件,位于我们应用程序的目录(在本例中为 blog)中,而该目录本身位于我们的 Django 站点目录(在本例中为 mysite)内。因此,我的博客应用程序的模型位于 mysite/blog/models.py 中,而投票应用程序的模型将位于 mysite/poll/models.py 中。
请注意,我们不必定义主键,传统上主键称为 id,是一个非重复整数。(在 PostgreSQL 中,我们将其设置为具有 SERIAL 数据类型,这为列提供了一个从新创建的序列对象中获取的默认值。在 MySQL 中,您会将列设置为 AUTO_INCREMENT,它具有与序列相同的一些功能。)Django 会自动为我们创建 id 列。Django 通过在表名称前加上应用程序名称来处理潜在的命名空间冲突。因此,博客应用程序中的 posting 表变为 blog_posting 表。
现在,我们如何将 Python 代码转换为 SQL?首先,我们必须确保 Django 知道要使用哪个数据库。如果您从我在 2007 年 7 月刊上的第一篇 Django 文章开始就一直在关注,您已经将适当的行添加到 settings.py 中,这是一个站点范围的配置文件,我们在其中定义数据库类型、名称、用户和密码。以下是我安装的值
DATABASE_ENGINE = 'postgresql' DATABASE_NAME = 'atf' DATABASE_USER = 'reuven' DATABASE_PASSWORD = '' DATABASE_HOST = '' DATABASE_PORT = '5433'
检查应用程序是否在 INSTALLED_APPS 中定义也很重要,INSTALLED_APPS 是一个字符串元组。在我的系统中,INSTALLED_APPS 看起来像这样
INSTALLED_APPS = ( 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.sites', 'django.contrib.admin', 'mysite.blog' )
请注意我的应用程序 (mysite.blog) 和 Django 附带的应用程序 (django.contrib.*) 之间清晰的命名空间区分。
在我们将 Python 代码转换为 SQL 之前,我们首先应该检查以确保它通过了一些基本的健全性和验证检查。为此,我们转到站点的根目录,然后键入
python manage.py validate
如果一切顺利,Django 将报告没有任何错误。现在我们的模型已经过验证,我们可以使用它来创建 SQL。最简单的方法是使用 sqlall 命令来管理 manage.py
python manage.py sqlall blog
这将为我们的数据库驱动程序(在本例中为 PostgreSQL)生成 SQL 输出。例如,这是我在我的系统上看到的输出
BEGIN; CREATE TABLE "blog_posting" ( "id" serial NOT NULL PRIMARY KEY, "title" varchar(30) NOT NULL, "body" text NOT NULL, "publication_date" timestamp with time zone NOT NULL ); COMMIT;
值得称赞的是,Django 开发人员将 CREATE TABLE 语句包装在 BEGIN 和 COMMIT 之间,确保表创建将在事务中进行,并且如果出现问题将回滚。当仅创建一个表时,这不是问题,但如果我们有多个模型,最好始终将数据库保持一致状态。
我们使用 sqlall 的输出创建表的一种方法是从终端窗口复制它,然后将其粘贴到文件或 psql 客户端程序中。但是,Django 提供了 syncdb 实用程序来为我们执行此操作
python manage.py syncdb
此输出向我们保证一切正常
Creating table blog_posting Loading 'initial_data' fixtures... No fixtures found.
而且,果然,现在我们可以看到我们的表已添加
atf=# \d blog_posting id | integer | not null default nextval('blog_posting_id_seq'::regclass) title | character varying(30) | not null body | text | not null publication_date | timestamp with time zone | not null Indexes: "blog_posting_pkey" PRIMARY KEY, btree (id)
瞧! 我们现在有了一个可以通过 Python 方法访问的模型,但该模型存在于我们的关系数据库中。
现在我们的数据模型已经到位,让我们看看如何使用它。鉴于我们的模型是全新的,并且当前没有数据存储在其中,让我们首先向其中添加一些数据。
在上个月的专栏中,我们看到了 Django 中的每个 URL 请求如何导致方法的调用。调用哪个方法取决于 urls.py 的设置,urls.py 是一个站点范围的配置文件,它告诉 Django 哪个应用程序和方法应与哪个 URL 关联。
将数据添加到我们的博客数据库的一种方法,以及练习使用 Django 的各种组件的一种方法,是通过视图和模板来完成。通常,我会演示如何使用 HTML 表单来完成此操作,但出于空间原因,我使用一种更简单(也更人为)的方法,将虚拟数据插入到数据库中。
第一步是在 urls.py 中定义的 urlpatterns 变量的定义中添加新行
('^blog/add_dummy_data', 'mysite.blog.add_dummy_data')
现在,我们可以转到 URL /blog/add_dummy_data,Django 将调用 blog.add_dummy_data 方法。此方法的开头非常简单,即
def add_dummy_data(request):
方法的名称从配置文件中显而易见。参数的数量由 urlpatterns 中的带括号的组的数量决定。
现在我们该怎么办?如果我们处理的是原始 SQL,我会建议以下内容
INSERT INTO Posting (title, body, posted_at) VALUES ('Dummy 1 headline', 'This is my first blog post', NOW - interval '1 hour'); INSERT INTO Posting (title, body, posted_at) VALUES ('Dummy 2 headline', 'This is my second blog post', NOW());
这些会将两行插入到 Posting 文件中:第一行带有来自一小时前的的时间戳,第二行带有当前时间戳。
但是,我们不想使用 SQL。我们想使用 Python,创建自动映射到这些 INSERT 语句的对象。因此,我们所要做的就是创建 Posting 对象的新实例,并向其传递适当的参数,这似乎是合理的。而且,果然,我们可以做到这一点
p = Posting(title='Dummy 1 headline', body='This is my first blog post', posted_at=(datetime.now() - timedelta(0, 0, 0, 0, 1))) p.save() p = Posting(title='Dummy 2 headline', body='This is my second blog post', posted_at=datetime.now()) p.save()
如果您是一位经验丰富的 Python 程序员,上面的代码根本不应该令人惊讶。我们只是创建了 Posting 的两个新实例,传递将设置对象属性的参数。然后,我们在每个 posting 上调用 save() 方法,这大概会将 posting 保存到磁盘。
最后,我们用以下内容完成我们的方法
return HttpResponse("Created blog posts.")
定义了该方法(如列表 1 所示)后,启动服务器
python manage.py runserver 69.55.232.87:8000
然后,将 Web 浏览器指向 urls.py 中定义的 URL,并获得消息
Created blog posts.
接下来,检查数据库,以确保
atf=# \x Expanded display is on. atf=# select * from blog_posting; -[ RECORD 1 ]----+------------------------------ id | 1 title | Dummy 1 headline body | This is my first blog post publication_date | 2007-06-15 16:13:34.609396-05 -[ RECORD 2 ]----+------------------------------ id | 2 title | Dummy 2 headline body | This is my second blog post publication_date | 2007-06-15 16:14:34.675235-05
如您所见,我们能够成功创建这些新对象并将它们存储在数据库中。
列表 1. models.py,用于创建新的虚拟帖子
from django.template import Context, loader from django.http import HttpResponse from blog.models import Posting from datetime import * def add_dummy_data(request): p = Posting(title='Dummy 1 headline', body='This is my first blog post', publication_date=(datetime.now() - timedelta(0, 0, 0, 0,1))) p.save() p = Posting(title='Dummy 2 headline', body='This is my second blog post', publication_date=datetime.now()) p.save() return HttpResponse("Created blog posts.")
现在我们已经创建了这些对象,让我们看看是否可以检索和显示它们——如果您在 Django 中编写应用程序,这是一件非常典型的事情。因为您可能想对博客做的最常见的事情是以反向时间顺序显示所有帖子,所以我们编写了 index 方法来执行此操作。如果您在 urls.py 中仍然没有 index 的条目,请确保 urlpatterns 的定义中有一行如下所示
(r'^blog/$', 'mysite.blog.views.index'),
现在,我们打开 views.py 来创建我们的 index 方法。该方法中的首要任务是获取所有帖子。Django 使这变得非常容易
postings = Posting.objects.all()
这会检索 Posting 的所有实例(这些实例恰好存储为数据库中的行)并将它们分配给变量 postings。此变量不是列表,而是 QuerySet 对象的实例。我们很可能想要迭代 QuerySet,但我们也可以对其执行其他操作,例如对其重新排序或检索选定的元素。
我们还可以从数据库中选择特定的项目。这是通过两种方法完成的:一种称为 filter(返回与限制性函数匹配的对象),另一种称为 except(执行相反操作,返回对函数为 false 的对象)。filter 和 except 都接受大量参数,这些参数通过将列名与各种函数连接起来动态构建。列名和函数名用双下划线 (__) 连接。
例如,我们可以只获取今年发布的帖子
this_year_postings = Posting.objects.filter( publication_date__gte=datetime(2007, 1, 1))
果然,这返回了我们的两个帖子。因为 filter 和 except 返回 QuerySet 对象,所以我们可以将它们链接在一起,在 Python 代码中创建我们想要的查询。
但是,如果我们只想要最新的帖子怎么办?如果您认为会有“limit”功能,那么您在 SQL 级别(或 Rails 中)工作的时间太长了。因为 QuerySets 使用惰性求值,所以您可以简单地说
this_year_postings = Posting.objects.filter( publication_date__gte=datetime(2007, 1, 1))[0]
我们也可以使用 order_by 方法对我们的对象进行排序,该方法可以与 filter 和 exclude 链接在一起
latest_posting = Posting.objects.filter( publication_date__gte=datetime(2007, 1, 1)).order_by('-publication_date')[0]
请注意,我们在单词 publication_date 之前放置了一个减号 (-)。这告诉 Django 我们希望以相反的顺序对结果进行排序。
Django 拥有大量此类方法,既为构建查询提供了极大的灵活性,又提供了丰富的 Python API,使您几乎可以完全忽略低级 SQL 调用。
最后,我们可以像从任何 Python 对象中检索信息一样从我们的对象中获取信息
output += "<h1>%s</h1>\n" % posting.title output += "<h2>%s</h2>\n" % posting.publication_date.isoformat() output += "<p>%s</p>\n\n\n" % posting.body
如果我们将所有这些放在一起,如列表 2 所示,我们将有一个视图方法(尽管没有适当的 Django 模板),该方法显示每个博客帖子。
列表 2. views.py,带有 Index 方法
from django.template import Context, loader from django.http import HttpResponse from blog.models import Posting from datetime import * def index(request): postings = Posting.objects.all().order_by("-publication_date") output = "" for posting in postings: output += "<h1>%s</h1>\n" % posting.title output += "<h2>%s</h2>\n" % posting.publication_date.isoformat() output += "<p>%s</p>\n\n\n" % posting.body return HttpResponse(output)
您可以使用 Django 的 shell 自己尝试所有这些数据库查询
python manage.py shell
使用 Django shell 而不是直接的交互式 Python 界面,可以确保 Django 相关的类和路径已预加载,从而可以从 Python 内部交互式地查询和修改数据库。这是试验您正在考虑添加到视图方法中的新代码的好方法,而无需将其放置在文件中。
Django 提供了一个高级接口,用于使用 Python 而不是 SQL 定义数据库模型。这种高级 API 渗透到框架中,使得完全在 Python 中工作成为可能。此外,API 包括许多便捷功能和数据类型,使得以这种方式工作相对自然。使用 Django 创建数据库支持的 Web 应用程序比我使用过的大多数框架都容易得多且更好,尽管它在风格上类似于 Ruby on Rails。您应该使用 Django 还是 Rails 取决于个人品味,也取决于您组织中的其他人正在使用什么,但毫无疑问,如果您是 Python Web/数据库黑客,那么 Django 非常值得认真考虑。
资源
Django 文档: www.djangoproject.com/documentation
Django 模型 API: www.djangoproject.com/documentation/model-api
Django 数据库 API: www.djangoproject.com/documentation/db-api
Reuven M. Lerner,一位长期的 Web/数据库顾问,是伊利诺伊州埃文斯顿西北大学学习科学博士候选人。他目前与妻子和三个孩子住在伊利诺伊州斯科基。您可以在他的博客上阅读:altneuland.lerner.co.il。