اكتشف العمليات التي يحتمل أن تكون خطيرة أو مدمرة في ترحيل قاعدة البيانات الخاصة بك.
يمكن تثبيت الحزمة عن طريق إضافة :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 , [ ] }
]
}
]
}مثال تحذيرات عقيدة:
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جيد ✅
لتجنب عملية تحديث طويلة محتملة ، لا سيما إذا كنت تنوي ملء العمود بقيم 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.
سيء
آمن في بوستجرس:
آمنة في 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يمكن أن تكون هذه في نفس النشر ، ولكن تأكد من وجود هجرة منفصلة.
قم بإعداد عدم وجود قراءات على كتل عمود موجودة يقرأ ويكتب أثناء فحص كل صف. تماما مثل إضافة سيناريو قيود الشيك ، هناك عمليتان تحدثان:
لتجنب مسح الجدول الكامل ، يمكننا فصل هاتين العمليتين.
سيء
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+ ، فيمكنك إضافة NOT NULL إلى العمود بعد التحقق من صحة القيد. من مستندات Postgres 12:
لا يجوز تطبيق Set Not 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
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" يتم تشجيع الجميع ومرحب بهم للمساعدة في تحسين هذا المشروع. فيما يلي بعض الطرق التي يمكنك من خلالها المساعدة:
حقوق الطبع والنشر (ج) 2021 أرتور سوليج
هذا العمل مجاني. يمكنك إعادة توزيعه و/أو تعديله بموجب شروط ترخيص MIT. انظر ملف الترخيص. md لمزيد من التفاصيل.