檢測數據庫遷移中潛在的危險或破壞性操作。
可以通過添加: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許可證的條款對其進行重新分配和/或對其進行修改。有關更多詳細信息,請參見許可證文件。