Mendeteksi operasi yang berpotensi berbahaya atau destruktif dalam migrasi database Anda.
Paket dapat diinstal dengan menambahkan :excellent_migrations ke daftar dependensi Anda di mix.exs :
def deps do
[
{ :excellent_migrations , "~> 0.1" , only: [ :dev , :test ] , runtime: false }
]
end Dokumentasi tersedia di hexdocs.
Alat ini menganalisis kode (AST) file migrasi. Anda tidak perlu mengedit atau memasukkan kode tambahan apa pun dalam file migrasi Anda, kecuali untuk sesekali menambahkan komentar konfigurasi untuk memastikan keamanan.
Ada banyak cara untuk berintegrasi dengan migrasi yang sangat baik.
Migrasi yang sangat baik memberikan cek kustom dan siap pakai untuk Credo.
Tambahkan ExcellentMigrations.CredoCheck.MigrationsSafety ke file .credo Anda:
% {
configs: [
% {
# …
checks: [
# …
{ ExcellentMigrations.CredoCheck.MigrationsSafety , [ ] }
]
}
]
}Contoh Peringatan 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
Tugas campuran ini menganalisis migrasi dan mencatat peringatan untuk setiap bahaya yang terdeteksi.
mix excellent_migrations.migrate
Menjalankan tugas ini pertama -tama akan menganalisis migrasi. Jika tidak ada bahaya yang terdeteksi, itu akan dilanjutkan dan menjalankan mix ecto.migrate . Jika ada, itu akan mencatat kesalahan dan berhenti.
Anda juga dapat menggunakannya dalam kode. Untuk melakukannya, Anda perlu mendapatkan kode sumber dan AST dari file migrasi Anda, misalnya melalui File.read!/1 dan Code.string_to_quoted/2 . Kemudian berikan mereka kemigrasi ExcellentMigrations.DangersDetector.detect_dangers(ast) . Ini akan mengembalikan daftar kata kunci yang berisi jenis bahaya dan baris di mana mereka terdeteksi.
Operasi yang berpotensi berbahaya:
Pemeriksaan khusus postgres:
Praktik Terbaik:
Anda juga dapat menonaktifkan cek tertentu.
Jika ECTO masih dikonfigurasi untuk membaca kolom dalam setiap contoh aplikasi, maka kueri akan gagal saat memuat data ke dalam struct Anda. Ini dapat terjadi dalam penyebaran multi-node atau jika Anda memulai aplikasi sebelum menjalankan migrasi.
BURUK
# Without a code change to the Ecto Schema
def change do
alter table ( "recipes" ) do
remove :no_longer_needed_column
end
endBagus ✅
Keselamatan dapat dijamin jika kode aplikasi pertama kali diperbarui untuk menghapus referensi ke kolom sehingga tidak lagi dimuat atau ditanya. Kemudian, kolom dapat dengan aman dilepas dari tabel.
Penerapan pertama:
# First deploy, in the Ecto schema
defmodule Cookbook.Recipe do
schema "recipes" do
- column :no_longer_needed_column, :text
end
endPenempatan kedua:
def change do
alter table ( "recipes" ) do
remove :no_longer_needed_column
end
endMenambahkan kolom dengan nilai default ke tabel yang ada dapat menyebabkan tabel ditulis ulang. Selama waktu ini, membaca dan menulis diblokir di postgres, dan menulis diblokir di MySQL dan Mariadb.
BURUK
Catatan: Ini menjadi aman di:
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
endBagus ✅
Tambahkan kolom terlebih dahulu, lalu ubah untuk memasukkan default.
Migrasi Pertama:
def change do
alter table ( "recipes" ) do
add :favourite , :boolean
# This took 0.27 milliseconds for 100 million rows with no fkeys,
end
endMigrasi Kedua:
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
endPerubahan skema untuk membaca kolom baru:
schema "recipes" do
+ field :favourite, :boolean, default: false
end Jika nilai default volatile (misalnya, clock_timestamp() , uuid_generate_v4() , random() ) Setiap baris perlu diperbarui dengan nilai yang dihitung pada ALTER TABLE saat dijalankan.
BURUK
Menambahkan Default Volatile ke Kolom:
def change do
alter table ( :recipes ) do
modify ( :identifier , :uuid , default: fragment ( "uuid_generate_v4()" ) )
end
endMenambahkan kolom dengan default yang mudah menguap:
def change do
alter table ( :recipes ) do
add ( :identifier , :uuid , default: fragment ( "uuid_generate_v4()" ) )
end
endBagus ✅
Untuk menghindari operasi pembaruan yang berpotensi panjang, terutama jika Anda bermaksud mengisi kolom dengan sebagian besar nilai tidak nondefault, mungkin lebih disukai daripada:
UPDATEJuga membuat tabel baru dengan kolom dengan default volatile aman, karena tidak berisi catatan apa pun.
ECTO menciptakan transaksi di sekitar setiap migrasi, dan mengajukan kembali dalam transaksi yang sama yang mengubah meja membuat meja terkunci selama durasi pengisian ulang. Juga, menjalankan satu kueri untuk memperbarui data dapat menyebabkan masalah untuk tabel besar.
BURUK
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
endBagus ✅
Ada beberapa strategi berbeda untuk melakukan pengisian ulang yang aman. Artikel ini menjelaskannya dengan sangat detail.
Mengubah jenis kolom dapat menyebabkan tabel ditulis ulang. Selama waktu ini, membaca dan menulis diblokir di postgres, dan menulis diblokir di MySQL dan Mariadb.
BURUK
Aman di Postgres:
Aman di mysql/mariadb:
def change do
alter table ( "recipes" ) do
modify :my_column , :boolean , from: :text
end
endBagus ✅
Ambil pendekatan bertahap:
Tanyakan pada diri sendiri: "Apakah saya benar -benar perlu mengganti nama kolom?". Mungkin tidak, tetapi jika Anda harus, membaca terus dan sadar itu membutuhkan waktu dan usaha.
Jika ECTO dikonfigurasi untuk membaca kolom dalam setiap contoh aplikasi, maka kueri akan gagal saat memuat data ke struct Anda. Ini dapat terjadi dalam penyebaran multi-node atau jika Anda memulai aplikasi sebelum menjalankan migrasi.
Ada jalan pintas: jangan ganti nama kolom basis data, dan sebaliknya ganti nama bidang skema dan konfigurasikan untuk menunjuk ke kolom basis data.
BURUK
# In your schema
schema "recipes" do
field :summary , :text
end
# In your migration
def change do
rename table ( "recipes" ) , :title , to: :summary
endWaktu antara migrasi Anda berjalan dan aplikasi Anda mendapatkan kode baru mungkin mengalami masalah.
Bagus ✅
Strategi 1
Ubah nama bidang hanya dalam skema, dan konfigurasikan untuk menunjuk ke kolom database dan jaga kolom basis data tetap sama. Pastikan semua kode panggilan yang mengandalkan nama bidang lama juga diperbarui untuk merujuk nama bidang baru.
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_minutesStrategi 2
Ambil pendekatan bertahap:
Tanyakan pada diri sendiri: "Apakah saya benar -benar perlu mengganti nama meja?". Mungkin tidak, tetapi jika Anda harus, membaca terus dan sadar itu membutuhkan waktu dan usaha.
Jika ecto masih dikonfigurasi untuk membaca tabel dalam setiap contoh aplikasi, maka kueri akan gagal saat memuat data ke struct Anda. Ini dapat terjadi dalam penyebaran multi-node atau jika Anda memulai aplikasi sebelum menjalankan migrasi.
Ada jalan pintas: ganti nama skema saja, dan jangan mengubah nama tabel basis data yang mendasarinya.
BURUK
def change do
rename table ( "recipes" ) , to: table ( "dish_algorithms" )
endBagus ✅
Strategi 1
Ubah nama skema saja dan semua kode panggilan, dan jangan ganti nama tabel:
- 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")Strategi 2
Ambil pendekatan bertahap:
Menambahkan blok kendala cek dibaca dan ditulis ke tabel di postgres, dan blok menulis di MySQL/MariAdb saat setiap baris diperiksa.
BURUK
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
endBagus ✅
Ada dua operasi yang terjadi:
Jika perintah ini terjadi pada saat yang sama, ia mendapatkan kunci di atas meja karena memvalidasi seluruh tabel dan sepenuhnya memindai tabel. Untuk menghindari pemindaian meja penuh ini, kami dapat memisahkan operasi.
Dalam satu migrasi:
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.
endDalam migrasi berikutnya:
def change do
execute "ALTER TABLE ingredients VALIDATE CONSTRAINT price_must_be_positive" , ""
# Acquires SHARE UPDATE EXCLUSIVE lock, which allows updates to continue
endIni bisa dalam penyebaran yang sama, tetapi memastikan ada 2 migrasi terpisah.
Mengatur bukan nol pada blok kolom yang ada dibaca dan ditulis saat setiap baris diperiksa. Sama seperti skenario penambahan Cek Cekatan, ada dua operasi yang terjadi:
Untuk menghindari pemindaian meja penuh, kami dapat memisahkan kedua operasi ini.
BURUK
def change do
alter table ( "recipes" ) do
modify :favourite , :boolean , null: false
end
endBagus ✅
Tambahkan kendala periksa tanpa memvalidasinya, tekan data untuk memuaskan kendala dan kemudian validasi. Ini akan setara secara fungsional.
Dalam migrasi pertama:
# Deployment 1
def change do
create constraint ( "recipes" , :favourite_not_null , check: "favourite IS NOT NULL" , validate: false )
endIni akan menegakkan kendala di semua baris baru, tetapi tidak peduli dengan baris yang ada sampai baris itu diperbarui.
Anda mungkin membutuhkan migrasi data pada titik ini untuk memastikan bahwa kendala dipenuhi.
Kemudian, dalam migrasi penempatan berikutnya, kami akan menegakkan kendala pada semua baris:
# Deployment 2
def change do
execute "ALTER TABLE recipes VALIDATE CONSTRAINT favourite_not_null" , ""
endJika Anda menggunakan Postgres 12+, Anda dapat menambahkan Not Null ke kolom setelah memvalidasi kendala. Dari Postgres 12 Docs:
Set Not Null hanya dapat diterapkan ke kolom yang asalkan tidak ada catatan dalam tabel yang berisi nilai nol untuk kolom. Biasanya ini diperiksa selama tabel alter dengan memindai seluruh tabel; Namun, jika kendala cek yang valid ditemukan yang membuktikan tidak ada nol yang bisa ada, maka pemindaian meja dilewati.
# **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 )
endJika kendala Anda gagal, maka Anda harus mempertimbangkan untuk mengajukan kembali data terlebih dahulu untuk menutupi kesenjangan dalam integritas data yang Anda inginkan, maka tinjau kembali memvalidasi kendala.
Migrasi yang sangat baik tidak dapat memastikan keamanan untuk pernyataan SQL mentah. Pastikan dengan sangat yakin bahwa apa yang Anda lakukan adalah aman, lalu gunakan:
defmodule Cookbook.ExecuteRawSql do
# excellent_migrations:safety-assured-for-this-file raw_sql_executed
def change do
execute ( "..." )
end
endMembuat indeks akan memblokir baik bacaan dan menulis.
BURUK
def change do
create index ( "recipes" , [ :slug ] )
# This obtains a ShareLock on "recipes" which will block writes to the table
endBagus ✅
Dengan Postgres, sebaliknya membuat indeks secara bersamaan yang tidak memblokir pembacaan. Anda perlu menonaktifkan transaksi basis data untuk digunakan CONCURRENTLY , dan karena ECTO memperoleh kunci migrasi melalui transaksi basis data, ini juga menyiratkan bahwa node yang bersaing dapat mencoba untuk mencoba menjalankan migrasi yang sama (misalnya, dalam lingkungan multi-node Kubernetes yang menjalankan migrasi sebelum startup). Oleh karena itu, beberapa node akan gagal startup karena berbagai alasan.
@ disable_ddl_transaction true
@ disable_migration_lock true
def change do
create index ( "recipes" , [ :slug ] , concurrently: true )
endMigrasi mungkin masih membutuhkan waktu untuk berlari, tetapi membaca dan memperbarui baris akan terus bekerja. Misalnya, untuk 100.000.000 baris butuh 165 detik untuk menambahkan menjalankan migrasi, tetapi memilih dan pembaruan dapat terjadi saat berjalan.
Tidak memiliki perubahan lain dalam migrasi yang sama ; Hanya buat indeks secara bersamaan dan pisahkan perubahan lainnya untuk migrasi selanjutnya.
Indeks secara bersamaan perlu mengatur kedua @disable_ddl_transaction dan @disable_migration_lock ke True. Lihat lebih lanjut:
BURUK
defmodule Cookbook.AddIndex do
def change do
create index ( :recipes , [ :cookbook_id , :cuisine ] , concurrently: true )
end
endBagus ✅
defmodule Cookbook.AddIndex do
@ disable_ddl_transaction true
@ disable_migration_lock true
def change do
create index ( :recipes , [ :cookbook_id , :cuisine ] , concurrently: true )
end
endMenambahkan blok kunci asing menulis di kedua tabel.
BURUK
def change do
alter table ( "recipes" ) do
add :cookbook_id , references ( "cookbooks" )
end
endBagus ✅
Dalam migrasi pertama
def change do
alter table ( "recipes" ) do
add :cookbook_id , references ( "cookbooks" , validate: false )
end
endDalam migrasi kedua
def change do
execute "ALTER TABLE recipes VALIDATE CONSTRAINT cookbook_id_fkey" , ""
endMigrasi ini dapat dalam penyebaran yang sama, tetapi pastikan mereka adalah migrasi yang terpisah.
jsonDi Postgres, tidak ada operator kesetaraan untuk jenis kolom JSON, yang dapat menyebabkan kesalahan untuk kueri terpilih yang ada dalam aplikasi Anda.
BURUK
def change do
alter table ( "recipes" ) do
add :extra_data , :json
end
endBagus ✅
Gunakan JSONB sebagai gantinya. Beberapa mengatakan itu seperti "json" tetapi " btter ."
def change do
alter table ( "recipes" ) do
add :extra_data , :jsonb
end
endBURUK
Menambahkan indeks non-unik dengan lebih dari tiga kolom jarang meningkatkan kinerja.
defmodule Cookbook.AddIndexOnIngredients do
def change do
create index ( :recipes , [ :a , :b , :c , :d ] , concurrently: true )
end
endBagus ✅
Sebaliknya, mulailah indeks dengan kolom yang paling mempersempit hasilnya.
defmodule Cookbook.AddIndexOnIngredients do
def change do
create index ( :recipes , [ :b , :d ] , concurrently: true )
end
endUntuk Postgres, pastikan untuk menambahkannya secara bersamaan.
Untuk menandai operasi dalam migrasi sebagai penggunaan konfigurasi yang aman. Itu akan diabaikan selama analisis.
Ada dua komentar konfigurasi yang tersedia:
excellent_migrations:safety-assured-for-next-line <operation_type>excellent_migrations:safety-assured-for-this-file <operation_type>Mengabaikan cek untuk baris yang diberikan:
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
endMengabaikan cek untuk seluruh file:
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
endJenis operasi yang mungkin adalah:
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_renamedAbaikan bahaya spesifik untuk semua pemeriksaan migrasi dengan:
config :excellent_migrations , skip_checks: [ :raw_sql_executed , :not_null_added ] Untuk melewatkan analisis migrasi yang dibuat sebelum menambahkan paket ini, atur stempel waktu dari migrasi terakhir di start_after di config:
config :excellent_migrations , start_after: "20191026080101" Setiap orang didorong dan disambut baik untuk membantu meningkatkan proyek ini. Berikut beberapa cara Anda dapat membantu:
Hak Cipta (C) 2021 Artur Sulej
Pekerjaan ini gratis. Anda dapat mendistribusikannya kembali dan/atau memodifikasinya berdasarkan ketentuan lisensi MIT. Lihat file lisensi.md untuk lebih jelasnya.