如果您最近阅读过Model.Meta.index_together的Django文档,则可能已经注意到以下说明:
请改用索引选项。 较新的索引选项比index_together提供更多功能。 index_together将来可能会被弃用。
Django历史上为Field(db_index = True)的单个字段以及Meta.index_together中的多个字段提供了索引控制。 这些选项非常适合为一个或多个字段指定索引,但是它们不能让您访问数据库索引的全部功能。
Django 1.11(2017)中添加了Meta.indexes选项,以允许通过Index()类使用更多索引功能。 最初,Index()添加了对降序索引的支持。 现在,它支持db_tablespace来控制存储,支持opclass以将PostgreSQL的各种运算符类用于索引,并支持创建不包含每一行的部分索引的条件。
“升级”
那么,如何将Field(db_index = True)或Meta.index_together一起“升级”到Meta.indexes?
首先,这不是必需的。 这两个功能实际上均已弃用,也不太可能。 如果您有一个使用Field(db_index = True)或Meta.index_together的旧项目,则最好将其保留在原处,并使用索引作为新索引。
但是,此更改是如何以低风险进行“零停机时间”迁移的一个很好的例子。 学习更多有关Django迁移的信息可能是一个很好的练习。
让我们采用以下模型:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
from django.db import models class Status(models.TextChoices): UNPUBLISHED = 'UN', 'Unpublished' PUBLISHED = 'PB', 'Published' class Book(models.Model): status = models.CharField( max_length=2, choices=Status.choices, default=Status.UNPUBLISHED, ) title = models.CharField(max_length=200) class Meta: index_together = [["status", "title"]] |
(注:Status类正在使用Django 3.0的新枚举类型。)
我们的模型使用index_together,我们将其更改为使用索引。 该过程应类似于将Field(db_index = True)更改为使用索引。
我们将研究两种方法。 第一个使用索引的重建,这可能需要一些时间才能在大型表上运行。 第二个保留了“零停机时间”的现有索引。
请注意,我们根本不会更改索引的定义。 如果要升级索引以使用Index()的任何其他功能(例如条件),则数据库通常无法就地更改索引。 您需要在一次迁移中添加新索引,然后在第二次迁移中删除原始索引。
重建方法
要重建,我们只需要删除index_together并添加定义了等效Index()的索引:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
from django.db import models class Book(models.Model): status = models.CharField( max_length=2, choices=Status.choices, default=Status.UNPUBLISHED, ) title = models.CharField(max_length=200) class Meta: indexes = [ models.Index( name="core_book_status_title_idx", fields=["status", "title"], ) ] |
当我们运行makemigrations时,最终将得到一个这样的迁移文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ ("core", "0001_initial"), ] operations = [ migrations.AlterIndexTogether(name="book", index_together=set()), migrations.AddIndex( model_name="book", index=models.Index( fields=["status", "title"], name="core_book_status_title_idx" ), ), ] |
这是功能。 但是,如果运行sqlmigrate,我们将看到它执行DROP INDEX,然后执行CREATE INDEX:
1 2 3 4 5 6 7 8 9 10 11 |
$ python manage.py sqlmigrate core 0002 BEGIN; -- -- Alter index_together for book (0 constraint(s)) -- DROP INDEX "core_book_status_title_6099efdb_idx"; -- -- Create index core_book_status_title_idx on field(s) status, title of model book -- CREATE INDEX "core_book_status_title_idx" ON "core_book" ("status", "title"); COMMIT; |
这不适用于大型表,在大型表中创建索引可能需要几个小时。另外,PostgreSQL和SQLite在创建新索引时将锁定该表以进行写入。在PostgreSQL上,我们可以将AddIndex替换为AddIndexConcurrently(Django 3.0+)以防止锁定。
让我们看一下避免重新创建索引的工作的第二种方法。
零停机时间方法
为了实现零下降,我们需要使用现有的索引名称添加新的Index()定义,然后编写一个迁移,告诉Django数据库中无需进行任何更改。
我们需要的第一件事是Django自动为索引生成的名称。这将合并表名称,包含的字段名称和哈希。在Django的历史记录版本中,哈希算法已更改了两次,因此,为了安全起见,我们将从数据库中检索索引名称。我们可以使用dbshell中的一些SQL来做到这一点。
例如,在SQLite上,我们可以运行.indexes命令在模型表上列出索引,然后从列表中选择索引:
1 2 3 4 5 6 7 8 |
python manage.py dbshell SQLite version 3.24.0 2018-06-04 14:10:15 Enter ".help" for usage hints. sqlite> .indexes core_book ... core_book_status_title_6099efdb_idx ... sqlite> |
在MariaDB / MySQL上,要运行的查询为:
1 |
SHOW INDEXES FROM core_book; |
On PostgreSQL, the query to run is:
1 2 3 4 |
SELECT tablename, indexname, indexdef FROM pg_indexes WHERE schemaname = 'public' AND tablename = 'core_book' ORDER BY tablename, indexname; |
值得检查的索引名称在您的所有环境(开发,登台,生产)中是否相同。 如果最初使用不同的Django版本以及不同的哈希算法创建数据库,则环境之间的名称可能会有所不同。 如果名称确实不同,我们可能要在所有环境中重命名索引以匹配生产。
其次,我们想使用找到的名称将其移动到Meta.indexes内的Index()定义中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
from django.db import models class Book(models.Model): status = models.CharField( max_length=2, choices=Status.choices, default=Status.UNPUBLISHED, ) title = models.CharField(max_length=200) class Meta: indexes = [ models.Index( name="core_book_status_title_6099efdb_idx", fields=["status", "title"], ) ] |
如果此时运行check命令,则会看到错误消息:
1 2 3 4 5 6 7 |
$ python manage.py check SystemCheckError: System check identified some issues: ERRORS: core.Book: (models.E034) The index name 'core_book_status_title_6099efdb_idx' cannot be longer than 30 characters. System check identified 1 issue (0 silenced). |
新的Index()将其名称限制为30个字符,以便与Oracle兼容。 这足够公平,尤其适用于应该与所有数据库后端兼容的Django核心和第三方软件包。 如果您使用的是Oracle,则旧的index_together名称应小于30个字符。
对于其他后端,我们可以使用更多字符:
SQLite-1,000,000
PostgreSQL-63
MariaDB / MySQL-64
在这种情况下,我们可以安全地禁用该检查。 通过将检查ID添加到SILENCED_SYSTEM_CHECKS设置中来执行此操作:
1 2 3 4 |
SILENCED_SYSTEM_CHECKS = [ # Allow index names >30 characters, because we aren’t using Oracle "models.E034", ] |
这有点危险,因为它删除了每个索引的检查。 但是,测试应该发现将来的索引是否具有过长的索引名称,因为数据库在迁移期间会引发错误。
(N.B.有一张开放的票证,可以使系统检查更安静些。)
正在运行的检查将显示它现在已被静音:
1 2 |
$ python manage.py check System check identified no issues (1 silenced). |
然后,我们应该运行带有标志的makemigrations进行新的迁移:
1 2 3 4 5 |
$ python manage.py makemigrations core --name book_indexes Migrations for 'core': index_change/core/migrations/0002_book_indexes.py - Alter index_together for book (0 constraint(s)) - Create index core_book_status_title_6099efdb_idx on field(s) status, title of model book |
(我们传递了–name以避免自动迁移名称。)
我们的新迁移与先前导致停机的迁移相同。我们需要对其进行修改,以允许Django的迁移在不运行任何SQL的情况下考虑所应用的这些更改。
输入SeparateDatabaseAndState。
该操作类采用两个迁移操作列表。 database_operations编译为SQL并在数据库上运行。 state_operations适用于模型的内存版本。这种分离允许我们在数据库中执行一些操作,并告诉Django实际模型类发生了另一件事。这对于无法正确自动检测到的更改很有用,例如,将ManyToManyField更改为使用直通模型。
在我们的例子中,我们不希望对数据库做任何事情,因此我们提供了database_operations = []。在状态层中,我们想告诉Django我们已经“删除”了index_together并“添加”了数据库中的新索引。为此,我们可以将自动生成的AlterIndexTogether和AddIndex操作移到我们的state_operations列表中。
我们的迁移最终像这样:
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 |
from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ ("core", "0001_initial"), ] operations = [ migrations.SeparateDatabaseAndState( database_operations=[], state_operations=[ migrations.AlterIndexTogether( name="book", index_together=set(), ), migrations.AddIndex( model_name="book", index=models.Index( fields=["status", "title"], name="core_book_status_title_6099efdb_idx", ), ), ], ), ] |
我们可以在运行迁移之前验证两组操作。
首先,我们可以检查sqlmigrate确实对database_operations无效:
1 2 3 4 5 6 |
$ python manage.py sqlmigrate core 0002 BEGIN; -- -- Custom state/database change combination -- COMMIT; |
很好-除了正常的BEGIN和COMMIT之外,没有SQL语句。
其次,我们可以检查state_operations是否确实告诉Django我们的迁移符合我们模型的最新定义。 我们通过运行makemigrations –dry-run来做到这一点,以确保自动检测器没有发现任何要更改的内容:
1 2 |
$ python manage.py makemigrations core --dry-run No changes detected in app 'core' |
在我们正常的测试套件通过后,现在应该可以部署了:)
原文:https://adamj.eu/tech/2020/07/27/how-to-modernize-your-django-index-definitions/