检测数据库迁移中潜在的危险或破坏性操作。
可以通过添加:excellent_migrations mix.exs安装该软件包。
def deps do
[
{ :excellent_migrations , "~> 0.1" , only: [ :dev , :test ] , runtime: false }
]
end 可以在Hexdocs上获得文档。
该工具分析迁移文件的代码(AST)。您无需编辑或在迁移文件中包含任何其他代码,除了偶尔添加配置注释以确保安全性。
有多种与出色的迁移相结合的方法。
出色的迁移提供了自定义,可用来的信用。
添加ExcellentMigrations.CredoCheck.MigrationsSafety到您的.credo文件:
% {
configs: [
% {
# …
checks: [
# …
{ ExcellentMigrations.CredoCheck.MigrationsSafety , [ ] }
]
}
]
}示例Credo警告:
Warnings - please take a look
┃
┃ [W] ↗ Raw SQL used
┃ apps/cookbook/priv/repo/migrations/20211024133700_create_recipes.exs:13 #(Cookbook.Repo.Migrations.CreateRecipes.up)
┃ [W] ↗ Index added not concurrently
┃ apps/cookbook/priv/repo/migrations/20211024133705_create_index_on_veggies.exs:37 #(Cookbook.Repo.Migrations.CreateIndexOnVeggies.up)
mix excellent_migrations.check_safety
此混合任务分析迁移并记录检测到的每种危险的警告。
mix excellent_migrations.migrate
执行此任务将首先分析迁移。如果未发现危险,它将进行并进行mix ecto.migrate 。如果有的话,它将记录错误并停止。
您也可以在代码中使用它。为此,您需要获取迁移文件的源代码和AST,例如通过File.read!/1和Code.string_to_quoted/2 。然后将它们传递到ExcellentMigrations.DangersDetector.detect_dangers(ast) 。它将返回一个关键字列表,其中包含检测到的危险类型和线条。
潜在危险的行动:
特定于Postgres的检查:
最佳实践:
您也可以禁用特定的检查。
如果仍在将ECTO配置为在应用程序的任何运行实例中读取列,则在将数据加载到您的结构中时,查询将失败。这可以在多节点部署中发生,或者如果您在运行迁移之前启动应用程序。
坏的
# Without a code change to the Ecto Schema
def change do
alter table ( "recipes" ) do
remove :no_longer_needed_column
end
end好✅
如果首先更新应用程序代码以删除对列的引用,则可以确保安全性,以免加载或查询。然后,可以安全地从表格中删除列。
第一次部署:
# First deploy, in the Ecto schema
defmodule Cookbook.Recipe do
schema "recipes" do
- column :no_longer_needed_column, :text
end
end第二部署:
def change do
alter table ( "recipes" ) do
remove :no_longer_needed_column
end
end在现有表中添加带有默认值的列可能会导致表重写。在此期间,读取和写入被封锁,并在MySQL和Mariadb中被阻止。
坏的
注意:这变得安全:
def change do
alter table ( "recipes" ) do
add :favourite , :boolean , default: false
# This took 10 minutes for 100 million rows with no fkeys,
# Obtained an AccessExclusiveLock on the table, which blocks reads and
# writes.
end
end好✅
首先添加列,然后将其更改以包含默认值。
第一次迁移:
def change do
alter table ( "recipes" ) do
add :favourite , :boolean
# This took 0.27 milliseconds for 100 million rows with no fkeys,
end
end第二迁移:
def change do
alter table ( "recipes" ) do
modify :favourite , :boolean , default: false
# This took 0.28 milliseconds for 100 million rows with no fkeys,
end
end模式更改以读取新列:
schema "recipes" do
+ field :favourite, :boolean, default: false
end如果默认值是挥发性的(例如, clock_timestamp() , uuid_generate_v4() , random() )每行都需要在执行ALTER TABLE中计算的值时更新。
坏的
将波动性默认值添加到列:
def change do
alter table ( :recipes ) do
modify ( :identifier , :uuid , default: fragment ( "uuid_generate_v4()" ) )
end
end添加具有挥发性默认值的列:
def change do
alter table ( :recipes ) do
add ( :identifier , :uuid , default: fragment ( "uuid_generate_v4()" ) )
end
end好✅
为了避免潜在的冗长更新操作,尤其是如果您打算用大多数非默认值填充列,则可能比:
UPDATE查询插入正确的值同样,用挥发性默认值创建一个带有列列的新表也是安全的,因为它不包含任何记录。
ECTO围绕每个迁移创建一项交易,并在相同的交易中进行回填,该交易改变了表格在回填充期间将表锁定的锁定。此外,运行单个查询以更新数据可能会引起大表格问题。
坏的
defmodule Cookbook.BackfillRecipes do
use Ecto.Migration
import Ecto.Query
def change do
alter table ( "recipes" ) do
add :new_data , :text
end
flush ( )
Cookbook.Recipe
|> where ( new_data: nil )
|> Cookbook.Repo . update_all ( set: [ new_data: "some data" ] )
end
end好✅
有几种不同的策略来执行安全的回填。本文在很好的细节上解释了它们。
更改列的类型可能会导致表重写。在此期间,读取和写入被封锁,并在MySQL和Mariadb中被阻止。
坏的
在Postgres中安全:
MySQL/Mariadb的安全:
def change do
alter table ( "recipes" ) do
modify :my_column , :boolean , from: :text
end
end好✅
采用分阶段的方法:
问问自己:“我真的需要重命名专栏吗?”。可能不是,但是如果您必须,请继续阅读并意识到它需要时间和精力。
如果将ECTO配置为在应用程序的任何运行实例中读取列,则将数据加载到您的结构中时,查询将失败。这可以在多节点部署中发生,或者如果您在运行迁移之前启动应用程序。
有一个快捷方式:不要重命名数据库列,而是重命名架构的字段名称并将其配置为指向数据库列。
坏的
# In your schema
schema "recipes" do
field :summary , :text
end
# In your migration
def change do
rename table ( "recipes" ) , :title , to: :summary
end您的迁移和应用程序获得新代码之间的时间可能会遇到麻烦。
好✅
策略1
仅在模式中重命名字段,然后将其配置为指向数据库列,并保持数据库列相同。确保还更新了所有依赖旧字段名称的调用代码以引用新字段名称。
defmodule Cookbook.Recipe do
use Ecto.Schema
schema "recipes" do
field :author , :string
field :preparation_minutes , :integer , source: :prep_min
end
end # # Update references in other parts of the codebase:
recipe = Repo.get(Recipe, "my_id")
- recipe.prep_min
+ recipe.preparation_minutes策略2
采用分阶段的方法:
问问自己:“我真的需要重命名桌子吗?”。可能不是,但是如果您必须,请继续阅读并意识到它需要时间和精力。
如果仍将ECTO配置为在应用程序的任何运行实例中读取表,则在将数据加载到您的结构中时,查询将失败。这可以在多节点部署中发生,或者如果您在运行迁移之前启动应用程序。
有一个快捷方式:仅重命名架构,并且不要更改基础数据库表名称。
坏的
def change do
rename table ( "recipes" ) , to: table ( "dish_algorithms" )
end好✅
策略1
仅重命名架构和所有调用代码,并且不要重命名表:
- defmodule Cookbook.Recipe do
+ defmodule Cookbook.DishAlgorithm do
use Ecto.Schema
schema "dish_algorithms" do
field :author, :string
field :preparation_minutes, :integer
end
end
# and in calling code:
- recipe = Cookbook.Repo.get(Cookbook.Recipe, "my_id")
+ dish_algorithm = Cookbook.Repo.get(Cookbook.DishAlgorithm, "my_id")策略2
采用分阶段的方法:
添加检查约束块会读取并写入Postgres的表格,并在检查每行时在MySQL/Mariadb中写入。
坏的
def change do
create constraint ( "ingredients" , :price_must_be_positive , check: "price > 0" )
# Creating the constraint with validate: true (the default when unspecified)
# will perform a full table scan and acquires a lock preventing updates
end好✅
发生了两项操作:
如果这些命令同时发生,则在验证整个表并对表进行充分扫描时,将在表上获得锁定。为了避免此完整的桌子扫描,我们可以将操作分开。
一个迁移:
def change do
create constraint ( "ingredients" , :price_must_be_positive , check: "price > 0" , validate: false )
# Setting validate: false will prevent a full table scan, and therefore
# commits immediately.
end在下一个迁移中:
def change do
execute "ALTER TABLE ingredients VALIDATE CONSTRAINT price_must_be_positive" , ""
# Acquires SHARE UPDATE EXCLUSIVE lock, which allows updates to continue
end这些可以在同一部署中,但请确保有2个单独的迁移。
在检查每一行时,在现有列块上设置不为null会读取和写入。就像添加支票约束方案一样,发生了两种操作:
为了避免全表扫描,我们可以将这两个操作分开。
坏的
def change do
alter table ( "recipes" ) do
modify :favourite , :boolean , null: false
end
end好✅
添加检查约束而无需验证它,回填数据以满足约束并验证它。这将在功能上等效。
在第一次迁移中:
# Deployment 1
def change do
create constraint ( "recipes" , :favourite_not_null , check: "favourite IS NOT NULL" , validate: false )
end这将在所有新行中执行约束,但在更新该行之前不关心现有行。
此时可能需要数据迁移,以确保满足约束。
然后,在下一个部署的迁移中,我们将对所有行实施约束:
# Deployment 2
def change do
execute "ALTER TABLE recipes VALIDATE CONSTRAINT favourite_not_null" , ""
end如果您使用的是Postgres 12+,则可以在验证约束后将其添加到列中。从Postgres 12个文档中:
如果表中的任何记录都不包含该列的零值,则只能将NOT NULL应用于列。通常,通过扫描整个表格,可以在Alter表格中检查一下。但是,如果找到有效的检查约束,证明没有空的存在,则表跳过表扫描。
# **Postgres 12+ only**
def change do
execute "ALTER TABLE recipes VALIDATE CONSTRAINT favourite_not_null" , ""
alter table ( "recipes" ) do
modify :favourite , :boolean , null: false
end
drop constraint ( "recipes" , :favourite_not_null )
end如果您的约束失败,则应首先考虑回填数据,以涵盖所需数据完整性中的空白,然后重新访问验证约束。
出色的迁移无法确保原始SQL语句的安全性。真正确定您的工作是安全的,然后使用:
defmodule Cookbook.ExecuteRawSql do
# excellent_migrations:safety-assured-for-this-file raw_sql_executed
def change do
execute ( "..." )
end
end创建索引将阻止读取和写入。
坏的
def change do
create index ( "recipes" , [ :slug ] )
# This obtains a ShareLock on "recipes" which will block writes to the table
end好✅
使用Postgres,而是同时创建索引,该索引不会阻止读取。您将需要禁用数据库交易以CONCURRENTLY使用,并且由于ECTO通过数据库交易获得迁移锁,这也意味着竞争节点可能试图尝试运行相同的迁移(例如,在启动之前运行迁移的多节点Kubernetes环境中)。因此,由于各种原因,有些节点会失败。
@ disable_ddl_transaction true
@ disable_migration_lock true
def change do
create index ( "recipes" , [ :slug ] , concurrently: true )
end迁移可能仍需一段时间才能运行,但是对行的阅读和更新将继续起作用。例如,对于100,000,000行,添加运行迁移的时间为165秒,但是在运行时可能会进行选择和更新。
在同一迁移中没有其他变化;仅同时创建索引,并将其他更改与以后的迁移分开。
同时索引需要将@disable_ddl_transaction和@disable_migration_lock设置为true。查看更多:
坏的
defmodule Cookbook.AddIndex do
def change do
create index ( :recipes , [ :cookbook_id , :cuisine ] , concurrently: true )
end
end好✅
defmodule Cookbook.AddIndex do
@ disable_ddl_transaction true
@ disable_migration_lock true
def change do
create index ( :recipes , [ :cookbook_id , :cuisine ] , concurrently: true )
end
end在两个表上添加外键块写入。
坏的
def change do
alter table ( "recipes" ) do
add :cookbook_id , references ( "cookbooks" )
end
end好✅
在第一次迁移中
def change do
alter table ( "recipes" ) do
add :cookbook_id , references ( "cookbooks" , validate: false )
end
end在第二次迁移中
def change do
execute "ALTER TABLE recipes VALIDATE CONSTRAINT cookbook_id_fkey" , ""
end这些迁移可以处于同一部署,但请确保它们是单独的迁移。
json列在Postgres中,没有针对JSON列类型的平等操作员,这可能会在应用程序中为现有选择的不同查询造成错误。
坏的
def change do
alter table ( "recipes" ) do
add :extra_data , :json
end
end好✅
改用JSONB。有人说这就像“ json”,但“ b etter”。
def change do
alter table ( "recipes" ) do
add :extra_data , :jsonb
end
end坏的
添加一个超过三列的非独特索引很少会提高性能。
defmodule Cookbook.AddIndexOnIngredients do
def change do
create index ( :recipes , [ :a , :b , :c , :d ] , concurrently: true )
end
end好✅
取而代之的是,启动一个索引,其列最缩小结果的缩小。
defmodule Cookbook.AddIndexOnIngredients do
def change do
create index ( :recipes , [ :b , :d ] , concurrently: true )
end
end对于Postgres,请确保同时添加它们。
将迁移中的操作标记为安全使用配置注释。在分析过程中将被忽略。
有两个配置注释可用:
excellent_migrations:safety-assured-for-next-line <operation_type>excellent_migrations:safety-assured-for-this-file <operation_type>忽略给定行的支票:
defmodule Cookbook.AddTypeToRecipesWithDefault do
def change do
alter table ( :recipes ) do
# excellent_migrations:safety-assured-for-next-line column_added_with_default
add ( :type , :string , default: "dessert" )
end
end
end忽略整个文件的支票:
defmodule Cookbook.AddTypeToRecipesWithDefault do
# excellent_migrations:safety-assured-for-this-file column_added_with_default
def change do
alter table ( :recipes ) do
add ( :type , :string , default: "dessert" )
end
end
end可能的操作类型是:
check_constraint_addedcolumn_added_with_defaultcolumn_reference_addedcolumn_removedcolumn_renamedcolumn_type_changedcolumn_volatile_defaultindex_concurrently_without_disable_ddl_transactionindex_concurrently_without_disable_migration_lockindex_not_concurrentlyjson_column_addedmany_columns_indexnot_null_addedoperation_deleteoperation_insertoperation_updateraw_sql_executedtable_droppedtable_renamed忽略所有迁移检查的特定危险:
config :excellent_migrations , skip_checks: [ :raw_sql_executed , :not_null_added ] 为了跳过分析在添加此软件包之前创建的迁移,请在配置中的start_after中的最后一个迁移设置时间戳:
config :excellent_migrations , start_after: "20191026080101" 鼓励和欢迎每个人,以帮助改进该项目。以下是您可以提供帮助的几种方法:
版权(C)2021 Artur Sulej
这项工作是免费的。您可以根据MIT许可证的条款对其进行重新分配和/或对其进行修改。有关更多详细信息,请参见许可证文件。