Erkennen Sie potenziell gefährliche oder zerstörerische Operationen in Ihren Datenbankmigrationen.
Das Paket kann durch Hinzufügen installiert werden :excellent_migrations in Ihre Liste der Abhängigkeiten in mix.exs :
def deps do
[
{ :excellent_migrations , "~> 0.1" , only: [ :dev , :test ] , runtime: false }
]
end Die Dokumentation ist auf Hexdocs verfügbar.
Dieses Tool analysiert den Code (AST) von Migrationsdateien. Sie müssen keinen zusätzlichen Code in Ihre Migrationsdateien bearbeiten oder aufnehmen, außer gelegentlich einen Konfigurationskommentar zur Sicherung der Sicherheit.
Es gibt mehrere Möglichkeiten, sich in hervorragende Migrationen zu integrieren.
Ausgezeichnete Migrationen bieten maßgeschneiderte und nutzende Schecks für Credo.
Fügen Sie ExcellentMigrations.CredoCheck.MigrationsSafety hinzu .credo
% {
configs: [
% {
# …
checks: [
# …
{ ExcellentMigrations.CredoCheck.MigrationsSafety , [ ] }
]
}
]
}Beispiel für Credo -Warnungen:
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
Dieser Mix -Task analysiert Migrationen und protokolliert eine Warnung für jede erkannte Gefahr.
mix excellent_migrations.migrate
Ausführen dieser Aufgabe wird zuerst Migrationen analysieren. Wenn keine Gefahren festgestellt werden, wird es fortgesetzt und mix ecto.migrate durchführen. Wenn es welche gibt, werden Fehler protokolliert und aufhören.
Sie können es auch in Code verwenden. Dazu müssen Sie Quellcode und AST Ihrer Migrationsdatei, z. B. über File.read!/1 und Code.string_to_quoted/2 , abrufen. Geben Sie sie dann an ExcellentMigrations.DangersDetector.detect_dangers(ast) . Es wird eine Schlüsselwortliste zurückgegeben, die Gefahrentypen und Zeilen enthält, in denen sie erkannt wurden.
Potenziell gefährliche Operationen:
Postgres-spezifische Schecks:
Best Practices:
Sie können auch bestimmte Schecks deaktivieren.
Wenn ECTO weiterhin so konfiguriert ist, dass eine Spalte in den laufenden Instanzen der Anwendung gelesen wird, fehlschlagen Abfragen beim Laden von Daten in Ihre Strukturen. Dies kann in Multi-Knoten-Bereitstellungen oder wenn Sie die Anwendung vor dem Ausführen von Migrationen starten.
SCHLECHT
# Without a code change to the Ecto Schema
def change do
alter table ( "recipes" ) do
remove :no_longer_needed_column
end
endGut ✅
Sicherheit kann sicher sein, wenn der Anwendungscode zuerst aktualisiert wird, um Verweise auf die Spalte zu entfernen, sodass er nicht mehr geladen oder abgefragt wird. Dann kann die Spalte sicher aus der Tabelle entfernt werden.
Erster Einsatz:
# First deploy, in the Ecto schema
defmodule Cookbook.Recipe do
schema "recipes" do
- column :no_longer_needed_column, :text
end
endZweite Bereitstellung:
def change do
alter table ( "recipes" ) do
remove :no_longer_needed_column
end
endDas Hinzufügen einer Spalte mit einem Standardwert zu einer vorhandenen Tabelle kann dazu führen, dass die Tabelle neu geschrieben wird. In dieser Zeit sind Lese- und Schreibvorgänge in Postgres blockiert und in MySQL und Mariadb blockiert.
SCHLECHT
HINWEIS: Dies wird sicher in:
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
endGut ✅
Fügen Sie zuerst die Spalte hinzu und ändern Sie sie dann so, dass die Standardeinstellung einbezieht.
Erste Migration:
def change do
alter table ( "recipes" ) do
add :favourite , :boolean
# This took 0.27 milliseconds for 100 million rows with no fkeys,
end
endZweite Migration:
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
endSchemaänderung, um die neue Spalte zu lesen:
schema "recipes" do
+ field :favourite, :boolean, default: false
end Wenn der Standardwert volatil ist (z. B. clock_timestamp() , uuid_generate_v4() , random() ) muss jede Zeile mit dem zum Zeitpunkt ALTER TABLE berechneten Wert aktualisiert werden.
SCHLECHT
Hinzufügen von volatilem Standard in die Spalte:
def change do
alter table ( :recipes ) do
modify ( :identifier , :uuid , default: fragment ( "uuid_generate_v4()" ) )
end
endHinzufügen von Spalte mit volatilem Standard:
def change do
alter table ( :recipes ) do
add ( :identifier , :uuid , default: fragment ( "uuid_generate_v4()" ) )
end
endGut ✅
Um einen potenziell langen Aktualisierungsvorgang zu vermeiden, insbesondere wenn Sie die Spalte ohnehin mit meist nicht definierten Werten füllen möchten, kann es vorzuziehen sein,:
UPDATE einDas Erstellen einer neuen Tabelle mit Spalte mit flüchtigem Standard ist sicher, da sie keine Datensätze enthalten.
ECTO erstellt eine Transaktion um jede Migration und füllt in derselben Transaktion, die eine Tabelle verändert, die Tisch für die Dauer der Rückfüllung gesperrt. Durch das Ausführen einer einzelnen Abfrage zum Aktualisieren von Daten kann Probleme für große Tabellen verursacht werden.
SCHLECHT
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
endGut ✅
Es gibt verschiedene Strategien, um eine sichere Rückfüllung durchzuführen. Dieser Artikel erklärt sie in großen Details.
Wenn Sie den Typ einer Spalte ändern, kann die Tabelle umgeschrieben werden. In dieser Zeit sind Lese- und Schreibvorgänge in Postgres blockiert und in MySQL und Mariadb blockiert.
SCHLECHT
Sicher in postgres:
Sicher in MySQL/Mariadb:
def change do
alter table ( "recipes" ) do
modify :my_column , :boolean , from: :text
end
endGut ✅
Nehmen Sie einen schrittweisen Ansatz an:
Fragen Sie sich: "Muss ich wirklich eine Kolumne umbenennen?". Wahrscheinlich nicht, aber wenn Sie müssen, lesen Sie weiter und müssen Sie sich bewusst sein, dass es Zeit und Mühe erfordert.
Wenn ECTO so konfiguriert ist, dass eine Spalte in den laufenden Instanzen der Anwendung gelesen wird, fehlschlagen Abfragen beim Laden von Daten in Ihre Strukturen. Dies kann in Multi-Knoten-Bereitstellungen oder wenn Sie die Anwendung vor dem Ausführen von Migrationen starten.
Es gibt eine Verknüpfung: Benennen Sie die Datenbankspalte nicht um und benennen Sie stattdessen den Feldnamen des Schemas um und konfigurieren Sie sie, um auf die Datenbankspalte zu verweisen.
SCHLECHT
# In your schema
schema "recipes" do
field :summary , :text
end
# In your migration
def change do
rename table ( "recipes" ) , :title , to: :summary
endDie Zeit zwischen Ihrer Migration und Ihrer Anwendung, die den neuen Code erhält, kann auf Probleme stoßen.
Gut ✅
Strategie 1
Benennen Sie das Feld nur im Schema um und konfigurieren Sie es so, dass es auf die Datenbankspalte verweist und die Datenbankspalte gleich behalten. Stellen Sie sicher, dass alle auf dem alten Feldnamen stützenden Anrufcode ebenfalls aktualisiert werden, um den neuen Feldnamen zu verweisen.
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_minutesStrategie 2
Nehmen Sie einen schrittweisen Ansatz an:
Fragen Sie sich: "Muss ich wirklich einen Tisch umbenennen?". Wahrscheinlich nicht, aber wenn Sie müssen, lesen Sie weiter und müssen Sie sich bewusst sein, dass es Zeit und Mühe erfordert.
Wenn ECTO weiterhin so konfiguriert ist, dass eine Tabelle in den laufenden Instanzen der Anwendung gelesen wird, fehlschlagen Abfragen beim Laden von Daten in Ihre Strukturen. Dies kann in Multi-Knoten-Bereitstellungen oder wenn Sie die Anwendung vor dem Ausführen von Migrationen starten.
Es gibt eine Verknüpfung: Benennen Sie nur das Schema um und ändern Sie den zugrunde liegenden Datenbank -Tabellennamen nicht.
SCHLECHT
def change do
rename table ( "recipes" ) , to: table ( "dish_algorithms" )
endGut ✅
Strategie 1
Benennen Sie nur das Schema und alle Anrufcode um und benennen Sie die Tabelle nicht um:
- 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")Strategie 2
Nehmen Sie einen schrittweisen Ansatz an:
Hinzufügen einer Scheck -Einschränkungsblöcke Lesen und Schreibvorgänge in Postgres in die Tabelle, und Blöcke schriftlich in MySQL/Mariadb, während jede Zeile überprüft wird.
SCHLECHT
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
endGut ✅
Es treten zwei Operationen auf:
Wenn diese Befehle zur gleichen Zeit stattfinden, erhält sie eine Sperre auf der Tabelle, da sie die gesamte Tabelle bestätigt und die Tabelle vollständig durchsucht. Um diesen vollständigen Tabellen -Scan zu vermeiden, können wir die Vorgänge trennen.
In einer Migration:
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.
endIn der nächsten Migration:
def change do
execute "ALTER TABLE ingredients VALIDATE CONSTRAINT price_must_be_positive" , ""
# Acquires SHARE UPDATE EXCLUSIVE lock, which allows updates to continue
endDiese können in derselben Bereitstellung vorliegen, stellen jedoch sicher, dass 2 separate Migrationen vorhanden sind.
Das Einstellen von Null auf vorhandenen Spaltenblöcken liest und schreibt, während jede Zeile überprüft wird. Genau wie beim Hinzufügen eines Scheckbeschränkungsszenario werden zwei Operationen stattfinden:
Um den vollständigen Tabellen -Scan zu vermeiden, können wir diese beiden Vorgänge trennen.
SCHLECHT
def change do
alter table ( "recipes" ) do
modify :favourite , :boolean , null: false
end
endGut ✅
Fügen Sie eine Überprüfungsbeschränkung hinzu, ohne sie zu validieren, die Daten zu füllen, um die Einschränkung zu streifen, und validieren Sie sie dann. Dies wird funktional äquivalent sein.
In der ersten Migration:
# Deployment 1
def change do
create constraint ( "recipes" , :favourite_not_null , check: "favourite IS NOT NULL" , validate: false )
endDadurch wird die Einschränkung in allen neuen Zeilen erzwingen, aber es ist sich nicht um bestehende Zeilen, bis diese Zeile aktualisiert ist.
Zu diesem Zeitpunkt benötigen Sie wahrscheinlich eine Datenmigration, um sicherzustellen, dass die Einschränkung erfüllt ist.
In der Migration der nächsten Bereitstellung werden wir dann die Einschränkungen für alle Zeilen durchsetzen:
# Deployment 2
def change do
execute "ALTER TABLE recipes VALIDATE CONSTRAINT favourite_not_null" , ""
endWenn Sie Postgres 12+ verwenden, können Sie nach der Validierung der Einschränkung die Not Null zur Spalte hinzufügen. Aus den Postgres 12 Docs:
Set No Null kann nur auf eine Spalte angewendet werden, die keine der Datensätze in der Tabelle enthält, enthält einen Nullwert für die Spalte. Normalerweise wird dies während der Änderungstabelle durch Scannen der gesamten Tabelle überprüft. Wenn jedoch eine gültige Überprüfungsbeschränkung gefunden wird, die sich beweist, dass kein Null vorhanden ist, wird der Tabellenscan übersprungen.
# **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 )
endWenn Ihre Einschränkung fehlschlägt, sollten Sie zunächst in Betracht ziehen, um die Lücken in der gewünschten Datenintegrität abzudecken, und dann die Validierung der Einschränkung erneut zu überprüfen.
Ausgezeichnete Migrationen können keine Sicherheit für RAW -SQL -Anweisungen gewährleisten. Stellen Sie wirklich sicher, dass das, was Sie tun, sicher ist, dann verwenden Sie:
defmodule Cookbook.ExecuteRawSql do
# excellent_migrations:safety-assured-for-this-file raw_sql_executed
def change do
execute ( "..." )
end
endDas Erstellen eines Index blockiert sowohl Lese- als auch Schreibvorgänge.
SCHLECHT
def change do
create index ( "recipes" , [ :slug ] )
# This obtains a ShareLock on "recipes" which will block writes to the table
endGut ✅
Erstellen Sie mit Postgres stattdessen den Index gleichzeitig, der die Lesevorgänge nicht blockiert. Sie müssen die Datenbanktransaktionen so deaktivieren, um CONCURRENTLY zu verwenden, und da ECTO Migrationsresssperrungen über Datenbanktransaktionen erhält, impliziert dies auch, dass konkurrierende Knoten versuchen, zu versuchen, dieselbe Migration auszuführen (z. B. in einer Multi-Node-Kubernetes-Umgebung, in der Migrationen ausführen). Daher scheitern einige Knoten aus verschiedenen Gründen Startup.
@ disable_ddl_transaction true
@ disable_migration_lock true
def change do
create index ( "recipes" , [ :slug ] , concurrently: true )
endDie Migration dauert möglicherweise noch eine Weile, um zu laufen, liest und aktualisiert die Zeilen weiter. Zum Beispiel dauerte für 100.000.000 Zeilen 165 Sekunden, um die Migration hinzuzufügen. Während des Laufens konnten jedoch Auswahl und Updates auftreten.
Haben Sie keine anderen Änderungen in derselben Migration ; Erstellen Sie nur den Index gleichzeitig und trennen Sie andere Änderungen an späteren Migrationen.
Gleichzeitig müssen Indizes sowohl @disable_ddl_transaction als auch @disable_migration_lock auf true festlegen. Weitere: mehr:
SCHLECHT
defmodule Cookbook.AddIndex do
def change do
create index ( :recipes , [ :cookbook_id , :cuisine ] , concurrently: true )
end
endGut ✅
defmodule Cookbook.AddIndex do
@ disable_ddl_transaction true
@ disable_migration_lock true
def change do
create index ( :recipes , [ :cookbook_id , :cuisine ] , concurrently: true )
end
endHinzufügen von Fremdschlüsselblöcken schreibt auf beiden Tabellen.
SCHLECHT
def change do
alter table ( "recipes" ) do
add :cookbook_id , references ( "cookbooks" )
end
endGut ✅
In der ersten Migration
def change do
alter table ( "recipes" ) do
add :cookbook_id , references ( "cookbooks" , validate: false )
end
endIn der zweiten Migration
def change do
execute "ALTER TABLE recipes VALIDATE CONSTRAINT cookbook_id_fkey" , ""
endDiese Migrationen können in derselben Bereitstellung vorliegen, stellen jedoch sicher, dass es sich um separate Migrationen handelt.
json -SpalteIn Postgres gibt es keinen Gleichstellungsoperator für den JSON -Spaltentyp, der Fehler für vorhandene ausgewählte Auswahlabfragen in Ihrer Anwendung verursachen kann.
SCHLECHT
def change do
alter table ( "recipes" ) do
add :extra_data , :json
end
endGut ✅
Verwenden Sie stattdessen JSONB. Einige sagen, es ist wie "JSON", aber " B Etter".
def change do
alter table ( "recipes" ) do
add :extra_data , :jsonb
end
endSCHLECHT
Das Hinzufügen eines nicht eindeutigen Index mit mehr als drei Spalten verbessert die Leistung selten.
defmodule Cookbook.AddIndexOnIngredients do
def change do
create index ( :recipes , [ :a , :b , :c , :d ] , concurrently: true )
end
endGut ✅
Starten Sie stattdessen einen Index mit Spalten, die die Ergebnisse am meisten einschränken.
defmodule Cookbook.AddIndexOnIngredients do
def change do
create index ( :recipes , [ :b , :d ] , concurrently: true )
end
endAchten Sie bei Postgres unbedingt gleichzeitig.
Markieren Sie einen Betrieb in einer Migration als sichere Konfigurationskommentar. Es wird während der Analyse ignoriert.
Es sind zwei Konfigurationskommentare verfügbar:
excellent_migrations:safety-assured-for-next-line <operation_type>excellent_migrations:safety-assured-for-this-file <operation_type>Ignorieren von Schecks für eine bestimmte Zeile:
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
endIgnorieren von Schecks für die gesamte Datei:
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
endMögliche Betriebstypen sind:
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_renamedIgnorieren Sie spezifische Gefahren für alle Migrationsprüfungen mit:
config :excellent_migrations , skip_checks: [ :raw_sql_executed , :not_null_added ] Um die Analyse von Migrationen zu überspringen, die vor dem Hinzufügen dieses Pakets erstellt wurden, stellen Sie den Zeitstempel von der letzten Migration in start_after in config fest:
config :excellent_migrations , start_after: "20191026080101" Jeder wird ermutigt und willkommen, dieses Projekt zu verbessern. Hier sind einige Möglichkeiten, wie Sie helfen können:
Copyright (C) 2021 Artur Sulej
Diese Arbeit ist kostenlos. Sie können es umverteilen und/oder unter den Bedingungen der MIT -Lizenz ändern. Weitere Informationen finden Sie in der Datei Lizenz.md -Datei.