データベースの移行で潜在的に危険または破壊的な操作を検出します。
パッケージは、 mix.exsの依存関係のリストへの:excellent_migrationsを追加してインストールできます。
def deps do
[
{ :excellent_migrations , "~> 0.1" , only: [ :dev , :test ] , runtime: false }
]
end ドキュメントはhexdocsで入手できます。
このツールは、移行ファイルのコード(AST)を分析します。安全性を保証するための構成コメントを時々追加する場合を除き、移行ファイルに追加のコードを編集または含める必要はありません。
優れた移行と統合する方法は複数あります。
優れた移行は、CREDOのカスタムですぐに使用できるチェックを提供します。
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を実行します。ある場合、エラーを記録して停止します。
コードで使用することもできます。そのためには、 File.read!/1およびCode.string_to_quoted/2経由で、移行ファイルのソースコードとASTを取得する必要があります。次に、それらをExcellentMigrations.DangersDetector.detect_dangers(ast)に渡します。検出された危険タイプと行を含むキーワードリストを返します。
潜在的に危険な操作:
ポストグレス固有のチェック:
ベストプラクティス:
特定のチェックを無効にすることもできます。
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
end2番目の展開:
def change do
alter table ( "recipes" ) do
remove :no_longer_needed_column
end
end既存のテーブルにデフォルト値のある列を追加すると、テーブルが書き直される場合があります。この間、読み取りと書き込みはPostgresでブロックされ、書き込みは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
end2番目の移行:
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良い✅
潜在的に長い更新操作を回避するために、特にとにかくほとんどnondefault値で列を記入する場合は、次のことをお勧めします。
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良い✅
安全な埋め戻しを実行するためのいくつかの異なる戦略があります。この記事では、詳細を説明しています。
列のタイプを変更すると、テーブルが書き直される場合があります。この間、読み取りと書き込みはPostgresでブロックされ、書き込みは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
段階的なアプローチをとる:
チェック制約ブロックを追加すると、すべての行がチェックされている間に、mysql/mariadbにブロックが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良い✅
2つの操作が発生しています。
これらのコマンドが同時に発生している場合、テーブル全体を検証し、テーブルを完全にスキャンするため、テーブルのロックを取得します。この完全なテーブルスキャンを回避するために、操作を分離できます。
1つの移行:
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を設定しないと、すべての行がチェックされている間に読み取りおよび書き込みがあります。チェック制約シナリオを追加するのと同じように、2つの操作が発生しています。
完全なテーブルスキャンを回避するために、これら2つの操作を分離できます。
悪い
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" , ""
endPostgres 12+を使用している場合、制約を検証した後、列にNOT NULLを追加できます。 Postgresから12のドキュメントから:
nullが列にのみ適用されないセットは、テーブル内のレコードには列のnull値が含まれていない場合があります。通常、これはテーブル全体をスキャンすることにより、アルターテーブル中にチェックされます。ただし、nullが存在しないことを証明する有効なチェック制約が見つかった場合、テーブルスキャンがスキップされます。
# **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
end2番目の移行で
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悪い
3列以上の非ユニークインデックスを追加すると、パフォーマンスが向上することはめったにありません。
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
endPostgresについては、必ず同時に追加してください。
安全な使用構成コメントとして移行の操作をマークする。分析中に無視されます。
利用可能な2つの構成コメントがあります。
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 ] このパッケージを追加する前に作成された移行の分析をスキップするには、configのstart_afterでの最後の移行からタイムスタンプを設定します。
config :excellent_migrations , start_after: "20191026080101" 誰もが奨励され、このプロジェクトの改善を支援することが歓迎されます。ここにあなたが助けることができるいくつかの方法があります:
Copyright(c)2021 Artur Sulej
この作業は無料です。 MITライセンスの条件に基づいて、再配布したり、変更したりできます。詳細については、license.mdファイルを参照してください。