有多少人曾为应用程序数据库的扩展和优化而头疼?如果你还没有阅读过《如何用Django搞定SQL的“脏活累活”》,强烈建议先看看那篇文章。简单来说,SQL是专为SQL数据库优化的,Python则不是,而Django可以充当一个中介,帮助我们构建更有效的应用程序,同时在配合使用这两种语言时减少摩擦,降低复杂性和代码量。
然而,尽管Django承担了创建数据木应用的重任,我们依然需要承担数据库的日常管理和监控工作。好在只要使用诸如Linode Managed Databases这样的托管数据库服务,很多管理任务都可以委托给云提供商,但我们可能依然会在应用扩展的过程中遇到新问题,例如:
- 数据库迁移。将现有数据库切换为新的所需状态,并以可控的方式更改数据库Schema。
- 多数据库部署。为了优化性能,开发者可以设计应用程序中的不同功能使用独立的数据库。例如,一个主要的读/写数据库和一个为通用查询提供服务的可读副本数据库。
如果其中一个数据库使用了SQL,我们可以按照《如何用Django搞定SQL的“脏活累活”》一文的介绍,大幅简化海量数据的处理工作。
本文将介绍数据库管理工作中的两个重要概念,并通过循序渐进的指南告诉大家该如何构建面向生产环境的Django应用程序。
数据库迁移
在刚上手时,为数据库中不同列的内容确定适合的数据类型,这可能是个有点棘手的任务,如果有关数据的需求会随着时间推移不可避免产生变化,感觉就更棘手了。如果希望标题字段只能包含80个字符该怎么办?如果需要添加一个时间戳字段以便准确追踪每个内容是在什么时候加入数据库的又该怎么办?
在创建好一个表之后再更改其内容,可能会产生一些非常混乱的后果,原因主要在于:
- 该如何处理已经存在的值?
- 如果原有的行缺少新增列/字段对应的数据该怎么办?
- 如果删除了某个列/字段该怎么办?数据会发生什么变化?
- 如果添加了一个之前不存在的关系(例如外键)又该如何处理?
好在对于Django开发者来说,可以使用makemigrations和migrate。一起看看它们是如何生效的。
以如下的Django数据模型为例:
class BlogArticle(models.Model):
user = models.ForeignKey(User, default=1, on_delete=models.SET_DEFAULT)
title = models.CharField(max_length=120)
slug = models.SlugField(blank=True, null=True)
content = models.TextField(blank=True, null=True)
publish_timestamp = models.DateTimeField(
auto_now_add=False,
auto_now=False,
blank=True,
null=True,
)
先来添加一个字段:
updated_by = models.ForeignKey(
User, related_name="editor", null=True, blank=True, on_delete=models.SET_NULL
)
该字段可供我们追踪修改了模型的最后一个用户。
接着更新一下这个模型:
class BlogArticle(models.Model):
user = models.ForeignKey(User, default=1, on_delete=models.SET_DEFAULT)
title = models.CharField(max_length=120)
slug = models.SlugField(blank=True, null=True)
content = models.TextField(blank=True, null=True)
publish_timestamp = models.DateTimeField(
auto_now_add=False,
auto_now=False,
blank=True,
null=True,
)
# our new field
updated_by = models.ForeignKey(
User, related_name="editor", null=True, blank=True, on_delete=models.SET_NULL
)
当我们保存了BlogArticle类所声明的文件(models.py)后,如何让数据库知道发生了这个改动?有两种方法:
- python manage.py makemigrations
- python manage.py migrate
一起看看这两个命令的作用吧。
1.python manage.py makemigrations
python manage.py makemigrations可以在我们的Django项目中寻找对models.py文件进行的所有改动。如果找到改动,将创建一个全新的Python文件,其中包含SQL数据库需要进行的提议变更(Proposed change)。这种提议变更看起来类似这样:
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('articles', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='article',
name='updated_by',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='editor', to=settings.AUTH_USER_MODEL),
),
]
这当然也是另一个Python文件。开发者可以通过该文件了解数据库中应该发生的事情。为了维持一致并利用Django ORM的内置功能,该文件会使用Python编写(而非SQL)。
但为什么这个文件中记录了应该发生的事情?主要原因在于:
- 如果我们希望在实际发生前检查应该发生的事情,可以直接通过该文件了解。
- Makemigrations命令并不会通过检查数据库来判断这些变化是否可以真正发生。
- 为了使用这些要求,数据库可能已经发生了改变(取决于谁在管理数据库等一系列因素)。
- 如果希望在更改生产数据库前进行测试,那么此时就是最佳时机。
假设这些变更是有效的(只要我们可以正确判断),随后就可以通过下列方法提交这些变更了。
2.python manage.py migrate
python manage.py migrate将尝试着为我们更改数据库(所有字段、列、表、外键等),Django会代替我们做这些工作,以确保数据库能按照我们希望的方式更新。
需要注意的是,Django可能会因为一些原因而无法执行这些变更。对于新接触Django的开发者,最主要的原因往往是添加或删除了字段/列,但未能正确进行迁移。
如果操作正确无误,python manage.py migrate可以保证产生稳定的系统,并且各方面均与我们的Python代码和SQL表保持匹配,从而确保可以正确发挥出Django和SQL数据库提供的强大功能。
这种做法如何为我们带来更高灵活性?
Python有着广泛的应用,但SQL就未必了。顾名思义,结构化查询语言(Structured Query Language)存在一些固有的局限。谁能只使用SQL就创建出精彩的动画电影呢?
这样说的意图在于,Python的简洁性有助于开发者发挥出SQL的真正威力,甚至可能是在不知不觉中发挥出来的。
为何使用托管数据库和Django是合理的做法
在创建包括Django在内的Web应用程序时,我们需要决定一些事情,例如:
- 需要怎样的一种或多种数据存储解决方案?MySQL、Postgres、MongoDB、Redis、Object Storage……
- 如何运行以及与数据存储解决方案进行集成?
- 如何从中断或停机中恢复?
- 如何维护存储解决方案?
- 如何保护存储解决方案?
- 如何备份存储解决方案?
上述这些问题的答案可能会随着项目复杂性的提高而改变,但所有答案都始于同一个地方:决定到底是使用自管理的还是第三方托管的存储解决方案。
自管理数据库:
- 优势:控制能力和更低的成本
- (巨大的)不足:一切都由我们自行负责
托管服务往往从一开始就需要付出更多成本,而自行管理意味着我们可以使用自己喜欢,并且以某种方式(或在某种程度上)针对我们的需求进行了优化的Linux发行版。例如我们可以用自己团队创建并修改的MySQL分支版本。自行运行的服务往往更省钱,但需要付出更多的时间和精力来维护。
第三方托管数据库服务:
没错,这类服务往往成本更高一些,但可以大幅减少我们需要投入的维护时间。这种方式以及托管式的数据存储解决方案已经成为很多Web应用程序的首选。本例中,我们已经在使用Django来管理数据库事务,SQLAlchemy也具备类似优势,因为它可以配合FastAPI、Flask等框架一起使用。如果你已经将SQL的相关开发工作通过Python包来实现,那么为何不把SQL服务器的运行也照此处理?
考虑到Python ORM(例如Django ORM和SQLAlchemy)的效果,建议尽可能使用托管数据库和/或托管的数据存储服务,从而可以获得下列这些收益:
- 缩短开发时间
- 缩短管理时间
- 缩短恢复时间
- 减少服务中断
- 降低部署和开发工作的复杂度
- 减少(从其他服务)迁移数据库的复杂度
- 减少SQL开发者的重复/无效/低效活动
- 降低DevOps/Ops复杂度
- 提高non-SQL开发者的效率
- 加快部署和开发速度
- 提高可靠性(托管服务通常有服务级别协议作为保证)
- 增强安全性
- 增强可维护性
- 增加备份和冗余
- 成本略微增加
实现上述好处的前提是:在Linode平台上使用托管的MySQL数据库集群,并使用Linode的Object Storage(存储CSS、JavaScript、图像、视频等文件)。实际上,使用这些服务还有助于我们将更多精力专注于通过Django、 FastAPI、Flask、Node.js等技术构建卓越的Web应用程序。换句话说,我们可以将工作的重心放在构建用户实际需要的工具和软件上,毕竟对用户而言,这些才是真正的价值所在。
MySQL、PostgreSQL、Redis和Django
很长一段时间以来,配合Django使用最广泛的数据库都是PostgreSQL,这在很大程度上可能因为只能在Postgres中使用JSONField。然而随着Django 3.2+和MySQL 5.7.8+的推出,JSONField也已经可用于MySQL了。
这一点为何重要?
在处理用户生成的内容或存储来自其他API服务的数据时,通常需要存储非结构化的数据,例如JSON。一起来看看具体是怎么做的:
from django.db import models
class Pet(models.Model):
name = models.CharField(max_length=200)
data = models.JSONField(null=True)
def __str__(self):
return self.name
我们可能想要存储与“Pet”有关的如下数据:
pet1 = {
"name": "Bruno",
"type": "Rat",
"nickname": "We don't talk about it",
"age": 2,
"age_interval": "months"
}
pet2 = {
"name": "Tom",
"type": "Cat",
"breed": "Mixed"
"age": 4,
"age_interval: "years",
"favorite_food": [{"brand": "Acme", "flavor": "Tuna" }]
}
pet3 = {
"name": "Stewey",
"type": "Dog",
"breed": "unknown"
"age": 34,
"age_interval: "dog years",
"nickname": "Football"
}
从上述数据中我们可以知道什么时候可能需要一个JSONField。我们可以存储所有宠物的名字(使用name键)并将其他信息存储在JSONField中。JSONField最酷的地方在于,可以像其他标准Django字段那样查询,哪怕它们使用了不同的Schema。
Django开发者之间一直在争论要使用哪种数据库:MySQL或是PostgreSQL。以前,很多人会坚持选择PostgreSQL,因为JSONField只能在PostgreSQL中使用,但现在情况不同了。因此我们完全可以任选一个并一直使用,直到所选数据库已经无法满足自己的需求。
但为何又要使用Redis?
Redis是一种速度惊人的内存中数据存储技术,因此通常被用作临时数据库(稍后将详细介绍这一点)、缓存服务以及/或消息队列。之所以将其称之为“临时数据库”是因为该技术将数据保存在内存中,内存通常比磁盘存储更贵,因此将数据长期存储在内存中往往并不是可行的做法。
本例中的Redis和Django主要用于缓存和队列。
缓存:假设用户需要频繁访问某几个网页,我们希望能尽可能快速地向用户展示这些页面的数据。如果配合Django使用Redis作为缓存系统,就可以轻松实现这一点。这些页面上的数据可能是从一个SQL数据库中渲染出来的,但Redis依然可以将渲染出的数据存储在缓存中。换句话说,将Redis与SQL配合使用通常可以加快响应速度,同时减少对SQL数据库发出的查询数量。
队列:Redis的另一个流行用例是将长时间运行的任务分载(Offload)到另一个进程(这通常可借助一个名为Celery的Python包实现)。如果需要这样做,即可使用Redis作为队列,保存需要在另一个时间完成的任务。
举例来说,如果有用户需要一份有关过去五年来所有交易的报表,软件可能需要花费几小时才能生成这样的报表。很明显,没人愿意盯着计算机等待数小时之久。因此我们可以把这种请求从用户那里分载到一个Redis队列中。一旦放入Redis,就可以运行工作进程(例如将Celery与Django配合使用)来生成报表。报表创建完成后,无论该过程花费了多长时间,用户都能收到通知。和其他通知一样,这种通知也可以通过Redis队列与Celery/Django工作进程配合实现。
上述内容都是为了说明:Redis和MySQL可以实现非常美妙的互补。我们可以通过Linode Marketplace部署自行管理的Redis数据库服务器。
对象存储
我们推荐的最后一个与数据有关的托管服务是Linode Object Storage。Object Storage负责保存我们需要存储的所有其他类型的数据,毕竟肯定没人会希望将一个视频文件的所有数据都存储在MySQL中。数据库可以只用来存储与视频有关的元数据,而视频文件本身,可以存储在Object Storage中。
我们可以使用对象存储服务来存储类似下面这些数据:
- 层叠样式表(CSS)
- JavaScript(例如React.js、Vue.js、Vanilla.js等)
- 视频
- 图像(原始图像和压缩后的图像)
- CSV、XLSX
- 数据库备份
- Docker容器镜像层(如果自行管理)
- 训练后的机器学习算法和后续迭代
- Terraform State File
- PDF(无论大小)
- 任何需要频繁下载(或上传)的演示文稿
总结
按照本文推荐的思路,任何开发者都可以在自己的Web应用程序项目中充分发挥托管服务的力量。Django是一种在SQL数据库基础上构建Web应用的出色解决方案,但它并非唯一。如果希望深入探寻SQL和SQL服务器的内部原理,我们认为有必要通过不断的研究来了解到底有多少成功的应用程序在使用Django来处理各种工作。
将Django与Linode的托管MySQL配合使用,可以获得包括但不限于下面列举的这些好处:
- Django代替用户执行繁重的SQL任务(适用于Flask/FastAPI的SQLAlchemy等工具也是如此)
- Django可以实现原始SQL命令(SQLAlchemy之类的工具也可以支持)
- Django可以帮助新手学习SQL命令
- Django内建了对MySQL和PostgreSQL的支持(还提供了一个针对db的Python客户端)
- 可提高生产环境的部署速度
- 可提高可靠性与可恢复性
- 有助于为开发和生产环境提供几乎完全一致的数据库技术
- 让基于容器的Django更易用、更可靠
- 实现了从单节点部署到多节点的扩展,甚至可全面过渡至Kubernetes
- 帮助Django/Python新手更容易地使用生产级系统
- 更易于跨多个Python应用共享数据库,并且更安全(例如FastAPI应用读/写基于Django的MySQL数据库)
- Django的JSONField现已支持使用MySQL(以前只支持PostgreSQL)
- 更易于测试(在CI/CD期间,或在本地开发环境中)
- 规模完全可满足Django的需求
- 支持在一个Django项目中使用多个数据库,例如使用MySQL作为主要的读/写数据库,并用另一个MySQL为常用查询提供可读副本数据库
- 严格的访问控制(Linode Private IP、本地开发)
- 要求通过SSL证书进行连接(增加了部署复杂度,但也增强了安全性)
- 实现私有连接(同一区域内部,降低连接成本)