ตรวจจับการดำเนินการที่อาจเป็นอันตรายหรือทำลายล้างในการย้ายฐานข้อมูลของคุณ
แพ็คเกจสามารถติดตั้งได้โดยการเพิ่ม :excellent_migrations ไปยังรายการการอ้างอิงของคุณใน mix.exs :
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 หากมีใด ๆ มันจะบันทึกข้อผิดพลาดและหยุด
คุณยังสามารถใช้ในรหัส ในการทำเช่นนั้นคุณจะต้องได้รับซอร์สโค้ดและ 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การเพิ่มคอลัมน์ที่มีค่าเริ่มต้นไปยังตารางที่มีอยู่อาจทำให้ตารางถูกเขียนใหม่ ในช่วงเวลานี้การอ่านและการเขียนจะถูกบล็อกใน 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
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ดี✅
มีกลยุทธ์ที่แตกต่างกันหลายประการในการดำเนินการ backfilling ที่ปลอดภัย บทความนี้อธิบายรายละเอียดที่ดี
การเปลี่ยนประเภทของคอลัมน์อาจทำให้ตารางถูกเขียนใหม่ ในช่วงเวลานี้การอ่านและการเขียนจะถูกบล็อกใน 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
ใช้วิธีการค่อยๆ:
การเพิ่มบล็อกข้อ จำกัด การตรวจสอบอ่านและเขียนลงในตารางใน 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 แยก
การตั้งค่าไม่เป็นโมฆะบนบล็อกคอลัมน์ที่มีอยู่อ่านและเขียนในขณะที่ตรวจสอบทุกแถว เช่นเดียวกับสถานการณ์การเพิ่มข้อ จำกัด การตรวจสอบมีการดำเนินการสองครั้งที่เกิดขึ้น:
เพื่อหลีกเลี่ยงการสแกนตารางเต็มเราสามารถแยกการดำเนินการทั้งสองนี้
แย่
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+ คุณสามารถเพิ่ม NON NULL ลงในคอลัมน์หลังจากตรวจสอบข้อ จำกัด จากเอกสาร Postgres 12:
ตั้งค่าไม่เป็นโมฆะอาจถูกนำไปใช้กับคอลัมน์ที่ไม่มีการบันทึกในตารางที่มีค่า 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หากข้อ จำกัด ของคุณล้มเหลวคุณควรพิจารณาข้อมูล backfilling ก่อนเพื่อครอบคลุมช่องว่างในความสมบูรณ์ของข้อมูลที่คุณต้องการแล้วกลับมาตรวจสอบข้อ จำกัด อีกครั้ง
การอพยพที่ยอดเยี่ยมไม่สามารถมั่นใจได้ถึงความปลอดภัยสำหรับคำสั่ง 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 เป็นจริง ดูเพิ่มเติม:
แย่
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 อย่าลืมเพิ่มพวกเขาพร้อมกัน
เพื่อทำเครื่องหมายการดำเนินการในการโยกย้ายเป็นการใช้ความคิดเห็นการกำหนดค่าที่ปลอดภัย มันจะถูกละเว้นในระหว่างการวิเคราะห์
มีสองความคิดเห็น config:
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:
config :excellent_migrations , start_after: "20191026080101" ทุกคนได้รับการสนับสนุนและยินดีต้อนรับเพื่อช่วยปรับปรุงโครงการนี้ นี่คือสองสามวิธีที่คุณสามารถช่วยได้:
ลิขสิทธิ์ (c) 2021 Artur Sulej
งานนี้ฟรี คุณสามารถแจกจ่ายซ้ำและ/หรือแก้ไขภายใต้ข้อกำหนดของใบอนุญาต MIT ดูไฟล์ License.md สำหรับรายละเอียดเพิ่มเติม