Django 迁移是我们处理 Sentry 中数据库更改的方式。

Django 迁移官方文档:https://docs.djangoproject.com/en/2.2/topics/migrations/

这些将涵盖了解迁移正在执行的操作所需的大部分内容。

命令

请注意,对于所有这些命令,如果在 getsentry 存储库中,您可以将 getsentry 替换为 sentry

将您的数据库升级到最新

sentry upgrade 会自动更新你的迁移。您也可以运行 sentry django migrate 来直接访问迁移命令。

将您的数据库移动到特定的迁移

当您要测试迁移时,这会很有帮助。

sentry django migrate <app_name> <migration_name> - 请注意,migration_name 可以是部分匹配,通常数字就是你所需要的。

例如:sentry django migrate sentry 0005

这也可用于回滚迁移。如果你犯了错误,在开发中很有用。

为迁移生成 SQL

这对审查您的代码的人很有帮助,因为并不总是清楚 Django 迁移实际要做什么。

sentry django sqlmigrate <app_name> <migration_name>

例如 sentry django sqlmigrate sentry 0003

生成迁移

这会根据您对模型所做的更改自动为您生成迁移。

sentry django makemigrations

或者

sentry django makemigrations <app_name> 用于一个指定的 app

例如 sentry django makemigrations sentry

当您在 pr 中包含迁移时,还要为迁移生成 sql 并将其作为注释包含在内,以便您的审阅者可以更轻松地了解 Django 正在做什么。

您还可以使用 sentry django makemigrations <app_name> --empty 生成空迁移。这对于数据迁移和其他自定义工作很有用。

将迁移合并到 master

合并到 master 时,您可能会注意到与 migrations_lockfile.txt 的冲突。

这个文件是为了帮助我们避免将具有相同迁移编号的两个迁移合并到 master,如果您与它发生冲突,那么很可能有人在您之前提交了迁移。

指南

在运行迁移时,我们需要注意一些事项。

过滤器

如果(数据)迁移涉及大表或未索引的列,最好迭代整个表而不是使用 filter。 例如:

EnvironmentProject.objects.filter(environment__name="none")

因为 EnvironmentProject 行太多,这会一次将太多行带入内存。

相反,我们应该使用 RangeQuerySetWrapperWithProgressBar 遍历所有 EnvironmentProject 行,因为它会分块进行。

例如:

for env in RangeQuerySetWrapperWithProgressBar(EnvironmentProject.objects.all()):
if env.name == 'none':
# Do what you need

我们通常更喜欢避免将 .filterRangeQuerySetWrapperWithProgressBar 一起使用。

由于它已经通过 id 对表进行排序,因此我们无法利用字段上的任何索引,并且可能会为每个块扫描大量行。

这会运行得更慢,但我们通常更喜欢这样,因为它在更长的时间内平均负载,并使每个查询获取每个块的成本相当低。

索引

我们更喜欢使用 CREATE INDEX CONCURRENTLY 在现有的大型表上创建索引。当我们这样做时,我们无法在事务中运行迁移,因此使用 atomic = False 来运行这些很重要。

删除列/表

由于我们的部署过程,这很复杂。

当我们部署时,我们运行迁移,然后推出应用程序代码,这需要一段时间。

这意味着如果我们只是删除一个列或模型,那么 sentry 中的代码将查找这些列/表并在部署完成之前出错。

在某些情况下,这可能意味着 Sentry 在部署完成之前很难停机。

为避免这种情况,请执行以下步骤:

  • 如果列不是空的,则将其标记为空,并创建一个迁移。
  • 部署。
  • 从模型中删除列,但在迁移中确保我们只将状态标记为已删除(removed)。
  • 部署。
  • 最后,创建一个删除列的迁移。

这是删除已经可以为空的列的示例。首先我们从模型中删除列,然后修改迁移以仅更新状态而不进行数据库操作。

operations = [
migrations.SeparateDatabaseAndState(
database_operations=[],
state_operations=[
migrations.RemoveField(model_name="alertrule", name="alert_threshold"),
migrations.RemoveField(model_name="alertrule", name="resolve_threshold"),
migrations.RemoveField(model_name="alertrule", name="threshold_type"),
],
)
]

一旦部署完成,我们就可以部署实际的列删除。这个 pr 只会有一个迁移,因为 Django 不再知道这些字段。请注意,反向 SQL 仅适用于开发人员,因此可以不分配默认值或进行任何类型的回填:

operations = [
migrations.SeparateDatabaseAndState(
database_operations=[
migrations.RunSQL(
"""
ALTER TABLE "sentry_alertrule" DROP COLUMN "alert_threshold";
ALTER TABLE "sentry_alertrule" DROP COLUMN "resolve_threshold";
ALTER TABLE "sentry_alertrule" DROP COLUMN "threshold_type";
""",
reverse_sql="""
ALTER TABLE "sentry_alertrule" ADD COLUMN "alert_threshold" smallint NULL;
ALTER TABLE "sentry_alertrule" ADD COLUMN "resolve_threshold" int NULL;
ALTER TABLE "sentry_alertrule" ADD COLUMN "threshold_type" int NULL; """,
)
],
state_operations=[],
)
]

如果该表在其他表中被引用为外键,则需要格外小心。在这种情况下,首先删除其他表中的外键列,然后返回到此步骤。

  • 通过在列上设置 db_constraint=False,删除此表到其他表的任何数据库级外键约束。
  • 部署
  • sentry 代码库中删除模型和所有引用。确保迁移仅将状态标记为已删除。
  • 部署。
  • 创建一个删除表的迁移。
  • 部署

这是删除此模型的示例:

class AlertRuleTriggerAction(Model):
alert_rule_trigger = FlexibleForeignKey("sentry.AlertRuleTrigger")
integration = FlexibleForeignKey("sentry.Integration", null=True)
type = models.SmallIntegerField()
target_type = models.SmallIntegerField()
# Identifier used to perform the action on a given target
target_identifier = models.TextField(null=True)
# Human readable name to display in the UI
target_display = models.TextField(null=True)
date_added = models.DateTimeField(default=timezone.now) class Meta:
app_label = "sentry"
db_table = "sentry_alertruletriggeraction"

首先,我们检查了它没有被任何其他模型引用,它没有。接下来,我们需要删除和 db 级外键约束。为此,我们改变这两列并生成一个迁移:

alert_rule_trigger = FlexibleForeignKey("sentry.AlertRuleTrigger", db_constraint=False)
integration = FlexibleForeignKey("sentry.Integration", null=True, db_constraint=False)

迁移中的操作看起来像

    operations = [
migrations.AlterField(
model_name='alertruletriggeraction',
name='alert_rule_trigger',
field=sentry.db.models.fields.foreignkey.FlexibleForeignKey(db_constraint=False, on_delete=django.db.models.deletion.CASCADE, to='sentry.AlertRuleTrigger'),
),
migrations.AlterField(
model_name='alertruletriggeraction',
name='integration',
field=sentry.db.models.fields.foreignkey.FlexibleForeignKey(db_constraint=False, null=True, on_delete=django.db.models.deletion.CASCADE, to='sentry.Integration'),
),
]

我们可以看到它生成的 sql 只是删除了 FK 约束

BEGIN;
SET CONSTRAINTS "a875987ae7debe6be88869cb2eebcdc5" IMMEDIATE; ALTER TABLE "sentry_alertruletriggeraction" DROP CONSTRAINT "a875987ae7debe6be88869cb2eebcdc5";
SET CONSTRAINTS "sentry_integration_id_14286d876e86361c_fk_sentry_integration_id" IMMEDIATE; ALTER TABLE "sentry_alertruletriggeraction" DROP CONSTRAINT "sentry_integration_id_14286d876e86361c_fk_sentry_integration_id";
COMMIT;

所以现在我们部署它并进入下一阶段。

下一阶段涉及从代码库中删除对模型的所有引用。所以我们这样做,然后我们生成一个迁移,从迁移状态中删除模型,而不是数据库。此迁移中的操作如下所示

operations = [
migrations.SeparateDatabaseAndState(
state_operations=[migrations.DeleteModel(name="AlertRuleTriggerAction")],
database_operations=[],
)
]

并且生成的 SQL 显示没有发生数据库更改。所以现在我们部署它并进入最后一步。

在这最后一步中,我们只想手动编写 DDL 来删除表。 所以我们使用 sentry django makemigrations --empty 来产生一个空的迁移,然后修改操作如下:

operations = [
migrations.RunSQL(
"""
DROP TABLE "sentry_alertruletriggeraction";
""",
reverse_sql="CREATE TABLE sentry_alertruletriggeraction (fake_col int)", # We just create a fake table here so that the DROP will work if we roll back the migration.
)
]

然后我们部署它,我们就完成了。

外键

创建外键大多没问题,但是对于像 ProjectGroup 这样的大/繁忙的表,由于获取锁的困难,它可能会导致问题。

您仍然可以创建 Django 级别的外键,而无需创建数据库约束。为此,请在定义键时设置 db_constraint=False

重命名表

重命名表很危险,会导致停机。发生这种情况的原因是在部署期间将运行旧/新代码的混合。

因此,一旦我们在 Postgres 中重命名该表,如果旧代码尝试访问它,它就会立即开始出错。有两种方法可以处理重命名表:

  • 不要在 Postgres 中重命名表。相反,只需在 Django 中重命名模型,并确保将 Meta.db_table 设置为当前表名,这样不会有任何中断。这是首选方法。
  • 如果你真的想重命名表,那么步骤将是:
  • 使用新名称创建一个表
  • 开始对旧表和新表进行双重写入,最好是在事务中。
  • 将旧行回填到新表中。
  • model 更改为从新表开始读取。
  • 停止写入旧表并从代码中删除引用。
  • 丢弃旧表。
  • 一般来说,这是不值得做的,与回报相比,这需要冒很多风险/付出很多努力。

添加列

创建新列时,它们应始终创建为可为空的。这是出于两个原因:

  • 如果存在现有行,添加非空列需要设置默认值,添加默认值需要完全重写表。这是危险的,很可能会导致停机
  • 在部署期间,新旧代码混合运行。如果旧代码尝试向表中插入一行,则插入将失败,因为旧代码不知道新列存在,因此无法为该列提供值。

向列添加 NOT NULL

not null 添加到列可能很危险,即使该列的表的每一行都有数据。

这是因为 Postgres 仍然需要对所有行执行非空检查,然后才能添加约束。

在小表上这可能没问题,因为检查会很快,但在大表上这可能会导致停机。

这里有几个选项可以确保安全:

  • ALTER TABLE tbl ADD CONSTRAINT cnstr CHECK (col IS NOT NULL) NOT VALID; ALTER TABLE tbl VALIDATE CONSTRAINT cnstr;. 首先,我们将约束创建为无效。然后我们之后验证它。我们仍然需要扫描整个表来验证,但我们只需要持有一个 SHARE UPDATE EXCLUSIVE 锁,它只会阻止其他 ALTER TABLE 命令,但允许读/写继续。这很有效,但会有 0.5-1% 的轻微性能损失。在 Postgres 12 之后,我们可以扩展这个方法来添加一个真正的 NOT NULL 约束。
  • 如果表足够小并且体积足够小,那么创建一个普通的 NOT NULL 约束应该是安全的。小是几百万行或更少。

添加具有默认值的列

向现有表添加具有默认值的列是危险的。这需要 Postgres 锁定表并重写它。相反,更好的选择是:

  • Postgres 中添加没有默认值的列,但在 Django 中添加默认值。这使我们能够确保所有新行都具有默认值。这是通过修改迁移文件以包含 migrations.SeperateDatabaseAndState 来完成的
operations = [
migrations.SeparateDatabaseAndState(
database_operations=[
migrations.AddField(
model_name="mymodel",
name="new_field",
# Don't use a default in Postgres, a data migration can be used afterward to backfill
field=models.PositiveSmallIntegerField(null=True),
),
],
state_operations=[
migrations.AddField(
model_name="mymodel",
name="new_field",
# Use the default in Django, new rows will use the specified default
field=models.PositiveSmallIntegerField(null=True, default=1),
),
],
)
]
  • 通过数据迁移使用默认值回填预先存在的行。

改变列类型

改变列的类型通常是危险的,因为它需要重写整个表。有一些例外:

  • varchar(<size>) 更改为更大尺寸的 varchar
  • 将任何 varchar 更改为 text
  • numeric 更改为 numeric,其中 precision 更高但 scale 相同。

对于任何其他类型,最好的前进路径通常是:

  • 创建具有新类型的列。
  • 开始对新旧列进行双重写入。
  • 回填并将旧列值转换为新列。
  • 更改代码以使用新字段。
  • 停止写入旧列并从代码中删除引用。
  • 从数据库中删除旧列。

通常,这值得在 #discuss-backend 中讨论。

重命名列

重命名列是危险的,会导致停机。

发生这种情况的原因是在部署期间将运行旧/新代码的混合。

因此,一旦我们在 Postgres 中重命名该列,如果旧代码尝试访问它,它就会立即开始出错。有两种方法可以处理重命名列:

  • 不要重命名 Postgres 中的列。相反,只需在 Django 中重命名字段,并在定义中使用 db_column 将其设置为现有的列名,这样就不会中断。这是首选方法。
  • 如果你真的想重命名列,那么步骤将是:
    • 创建具有新名称的列
    • 开始对新旧列进行双重写入。
    • 将旧列值回填到新列中。
    • 将字段更改为从新列开始读取。
    • 停止写入旧列并从代码中删除引用。
    • 从数据库中删除旧列。
    • 一般来说,这是不值得做的,与回报相比,这需要冒很多风险/付出很多努力。

更多

Sentry 开发者贡献指南 - 数据库迁移的更多相关文章

  1. Sentry 开发者贡献指南 - 后端服务(Python/Go/Rust/NodeJS)

    内容整理自官方开发文档 系列 1 分钟快速使用 Docker 上手最新版 Sentry-CLI - 创建版本 快速使用 Docker 上手 Sentry-CLI - 30 秒上手 Source Map ...

  2. Sentry 开发者贡献指南 - SDK 开发(性能监控)

    内容整理于官方开发文档 系列 Docker Compose 部署与故障排除详解 K8S + Helm 一键微服务部署 Sentry 开发者贡献指南 - 前端(ReactJS生态) Sentry 开发者 ...

  3. Sentry 开发者贡献指南 - Django Rest Framework(Serializers)

    Serializer 用于获取复杂的 python 模型并将它们转换为 json.序列化程序还可用于在验证传入数据后将 json 反序列化回 Python 模型. 在 Sentry,我们有两种不同类型 ...

  4. Sentry 开发者贡献指南 - 前端 React Hooks 与虫洞状态管理模式

    系列 Sentry 开发者贡献指南 - 前端(ReactJS生态) Sentry 开发者贡献指南 - 后端服务(Python/Go/Rust/NodeJS) 什么是虫洞状态管理模式? 您可以逃脱的最小 ...

  5. Sentry 开发者贡献指南 - SDK 开发(事件负载)

    内容整理自官方开发文档 系列 Docker Compose 部署与故障排除详解 1 分钟快速使用 Docker 上手最新版 Sentry-CLI - 创建版本 快速使用 Docker 上手 Sentr ...

  6. Sentry 开发者贡献指南 - SDK 开发(性能监控:Sentry SDK API 演进)

    内容整理自官方开发文档 本文档的目标是将 Sentry SDK 中性能监控功能的演变置于上下文中. 我们首先总结了如何将性能监控添加到 Sentry 和 SDK, 然后我们讨论 identified ...

  7. Sentry 开发者贡献指南 - Feature Flag

    功能 flag 在 Sentry 的代码库中声明. 对于自托管用户,这些标志然后通过 sentry.conf.py 进行配置. 对于 Sentry 的 SaaS 部署,Flagr 用于在生产中配置标志 ...

  8. Sentry 开发者贡献指南 - 配置 PyCharm

    概述 如果您使用 PyCharm 进行开发,则需要配置一些内容才能运行和调试. 本文档描述了一些对 sentry 开发有用的配置 配置 Python 解释器:(确保它是 venv 解释器)例如 ~/v ...

  9. Sentry 开发者贡献指南 - 前端(ReactJS生态)

    内容整理自官方开发文档 系列 1 分钟快速使用 Docker 上手最新版 Sentry-CLI - 创建版本 快速使用 Docker 上手 Sentry-CLI - 30 秒上手 Source Map ...

随机推荐

  1. Angular动画(ng-class)

    ng-class 同 触发的是 addClass//当给元素添加一个class时触发, removeClass //把元素的class移除时触发 <ul ng-style="ulWid ...

  2. Java程序员的日常——存储过程知识普及

    存储过程是保存可以接受或返回用户提供参数的SQL语句集合.在日常的使用中,经常会遇到复杂的业务逻辑和对数据库的操作,使用存储过程可以进行封装.可以在数据库中定义子程序,然后把子程序存储在数据库服务器, ...

  3. 设计模式学习之模板方法模式(TemplateMethod,行为型模式)(9)

    一.什么是模板方法模式 Template Method模式也叫模板方法模式,是行为模式之一,它把具有特定步骤算法中的某些必要的处理委让给抽象方法,通过子类继承对抽象方法的不同实现改变整个算法的行为. ...

  4. Android自定义UI模板

    第一步:自定义xml属性 新建一个android项目,在values文件夹中新建一个atts.xml的文件,在这个xml文件中声明我们一会在使用自定义控件时候需要指明的属性.atts.xml < ...

  5. Java应用的优秀管理工具Maven的下载安装及配置

    1.进入Maven的官方下载地址:http://maven.apache.org/download.cgi 2.向下滚动页面,点击这个zip包进行下载: 3.将压缩包解压后剪切到Mac的某个目录下就完 ...

  6. 微信公众号批量爬取java版

    最近需要爬取微信公众号的文章信息.在网上找了找发现微信公众号爬取的难点在于公众号文章链接在pc端是打不开的,要用微信的自带浏览器(拿到微信客户端补充的参数,才可以在其它平台打开),这就给爬虫程序造成很 ...

  7. GetLastError获取到错误代码的含义

    在写win32的时候我们会用到GetLastError()函数来获取程序错误信息,那我们如何从返回的数字得到错误信息. 这里推荐一个博客,总结了所以返回数字的错误信息: http://blog.csd ...

  8. virtualbox谨记:win7上只有4.3.x的版本支持ubuntu14.04.3虚拟机安装Oracle Rac,其他的版本3.x和5.0.2(至2015-08-30)均不可以

    virtualbox谨记:win7上只有4.3.x的版本支持ubuntu14.04.3虚拟机安装Oracle Rac,其他的版本3.x和5.0.2(至2015-08-30)均不可以

  9. 微信小程序中的 web-view 组件

    web-view 是一个可以承载 web 网页的容器,当 WXML 文件中存在 web-view 组件时,其他组件会自动全部失效,而且 web-view 承载的组件会自动铺满小程序的整个页面.其他组件 ...

  10. C# 关闭显示器(显示)

    1.先引入DllImport所在的名称空间 using System.Runtime.InteropServices; 2.引入方法 [DllImport("user32.dll" ...