Détecter les opérations potentiellement dangereuses ou destructrices dans vos migrations de base de données.
Le package peut être installé en ajoutant :excellent_migrations à votre liste de dépendances dans mix.exs :
def deps do
[
{ :excellent_migrations , "~> 0.1" , only: [ :dev , :test ] , runtime: false }
]
end La documentation est disponible sur Hexdocs.
Cet outil analyse le code (AST) des fichiers de migration. Vous n'avez pas à modifier ou à inclure un code supplémentaire dans vos fichiers de migration, sauf pour ajouter occasionnellement un commentaire de configuration pour assurer la sécurité.
Il existe plusieurs façons de s'intégrer à d'excellentes migrations.
Les excellentes migrations fournissent une vérification personnalisée et prêt à l'emploi pour Credo.
Ajoutez ExcellentMigrations.CredoCheck.MigrationsSafety à votre fichier .credo :
% {
configs: [
% {
# …
checks: [
# …
{ ExcellentMigrations.CredoCheck.MigrationsSafety , [ ] }
]
}
]
}Exemples d'avertissements de crédo:
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
Cette tâche de mélange analyse les migrations et enregistre un avertissement pour chaque danger détecté.
mix excellent_migrations.migrate
L'exécution de cette tâche analysera d'abord les migrations. Si aucun danger n'est détecté, il se déroulera et exécutera mix ecto.migrate . S'il y en a, il enregistrera les erreurs et s'arrêtera.
Vous pouvez également l'utiliser dans le code. Pour ce faire, vous devez obtenir du code source et AST de votre fichier de migration, par exemple via File.read!/1 et Code.string_to_quoted/2 . Ensuite, transmettez-les à ExcellentMigrations.DangersDetector.detect_dangers(ast) . Il renverra une liste de mots clés contenant des types de danger et des lignes où ils ont été détectés.
Opérations potentiellement dangereuses:
Contrôles spécifiques à Postgres:
Meilleures pratiques:
Vous pouvez également désactiver des contrôles spécifiques.
Si Ecto est toujours configuré pour lire une colonne dans les instances en cours d'exécution de l'application, les requêtes échoueront lors du chargement des données dans vos structures. Cela peut se produire dans les déploiements multi-nœuds ou si vous démarrez l'application avant d'exécuter des migrations.
MAUVAIS
# Without a code change to the Ecto Schema
def change do
alter table ( "recipes" ) do
remove :no_longer_needed_column
end
endBon ✅
La sécurité peut être assurée si le code d'application est d'abord mis à jour pour supprimer les références à la colonne afin qu'elle ne soit plus chargée ou interrogée. Ensuite, la colonne peut être retirée en toute sécurité de la table.
Premier déploiement:
# First deploy, in the Ecto schema
defmodule Cookbook.Recipe do
schema "recipes" do
- column :no_longer_needed_column, :text
end
endDeuxième déploiement:
def change do
alter table ( "recipes" ) do
remove :no_longer_needed_column
end
endL'ajout d'une colonne avec une valeur par défaut à une table existante peut entraîner la réécriture du tableau. Pendant ce temps, les lectures et les écritures sont bloquées dans Postgres, et les écritures sont bloquées dans MySQL et MariaDB.
MAUVAIS
Remarque: cela devient sûr dans:
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
endBon ✅
Ajoutez d'abord la colonne, puis modifiez-la pour inclure la valeur par défaut.
Première migration:
def change do
alter table ( "recipes" ) do
add :favourite , :boolean
# This took 0.27 milliseconds for 100 million rows with no fkeys,
end
endDeuxième 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
endChangement de schéma pour lire la nouvelle colonne:
schema "recipes" do
+ field :favourite, :boolean, default: false
end Si la valeur par défaut est volatile (par exemple, clock_timestamp() , uuid_generate_v4() , random() ) Chaque ligne devra être mise à jour avec la valeur calculée au moment où ALTER TABLE est exécutée.
MAUVAIS
Ajout d'une par défaut volatile à la colonne:
def change do
alter table ( :recipes ) do
modify ( :identifier , :uuid , default: fragment ( "uuid_generate_v4()" ) )
end
endAjout d'une colonne avec par défaut volatile:
def change do
alter table ( :recipes ) do
add ( :identifier , :uuid , default: fragment ( "uuid_generate_v4()" ) )
end
endBon ✅
Pour éviter une opération de mise à jour potentiellement longue, en particulier si vous avez l'intention de remplir la colonne avec des valeurs principalement non fines, elle peut être préférable à:
UPDATELa création d'une nouvelle table avec une colonne avec une défaut volatile est également sûre, car elle ne contient aucun enregistrement.
ECTO crée une transaction autour de chaque migration, et le remblayage dans la même transaction qui modifie un tableau permet de verrouiller le tableau pendant la durée du remblai. De plus, l'exécution d'une seule requête pour mettre à jour les données peut entraîner des problèmes pour les grandes tables.
MAUVAIS
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
endBon ✅
Il existe plusieurs stratégies différentes pour effectuer un remblayage sûr. Cet article les explique en détail.
La modification du type de colonne peut entraîner la réécriture du tableau. Pendant ce temps, les lectures et les écritures sont bloquées dans Postgres, et les écritures sont bloquées dans MySQL et MariaDB.
MAUVAIS
Sécurité dans Postgres:
Sécurité dans MySQL / MARIADB:
def change do
alter table ( "recipes" ) do
modify :my_column , :boolean , from: :text
end
endBon ✅
Adopter une approche progressive:
Demandez-vous: "Ai-je vraiment besoin de renommer une colonne?". Probablement pas, mais si vous le devez, lisez la suite et sachez que cela nécessite du temps et des efforts.
Si Ecto est configuré pour lire une colonne dans les instances en cours d'exécution de l'application, les requêtes échoueront lors du chargement des données dans vos structures. Cela peut se produire dans les déploiements multi-nœuds ou si vous démarrez l'application avant d'exécuter des migrations.
Il y a un raccourci: ne renommez pas la colonne de la base de données, et renommez plutôt le nom de champ du schéma et configurez-le pour pointer vers la colonne de la base de données.
MAUVAIS
# In your schema
schema "recipes" do
field :summary , :text
end
# In your migration
def change do
rename table ( "recipes" ) , :title , to: :summary
endLe temps entre votre migration en cours d'exécution et votre application pour obtenir le nouveau code peut rencontrer des problèmes.
Bon ✅
Stratégie 1
Renommez le champ dans le schéma uniquement, et configurez-le pour pointer vers la colonne de la base de données et gardez la colonne de la base de données identique. Assurez-vous que tout le code d'appel qui s'appuie sur l'ancien nom de champ est également mis à jour pour référencer le nouveau nom de champ.
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_minutesStratégie 2
Adopter une approche progressive:
Demandez-vous: "Ai-je vraiment besoin de renommer une table?". Probablement pas, mais si vous le devez, lisez la suite et sachez que cela nécessite du temps et des efforts.
Si Ecto est toujours configuré pour lire un tableau dans les instances en cours d'exécution de l'application, les requêtes échoueront lors du chargement des données dans vos structures. Cela peut se produire dans les déploiements multi-nœuds ou si vous démarrez l'application avant d'exécuter des migrations.
Il y a un raccourci: renommez le schéma uniquement et ne modifiez pas le nom de la table de base de données sous-jacent.
MAUVAIS
def change do
rename table ( "recipes" ) , to: table ( "dish_algorithms" )
endBon ✅
Stratégie 1
Renommez le schéma uniquement et tous les code d'appel, et ne renomment pas le tableau:
- 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")Stratégie 2
Adopter une approche progressive:
L'ajout d'une contrainte de contrôle Blocs lit et écrit à la table dans Postgres, et les blocs écrites dans MySQL / MariADB pendant que chaque ligne est vérifiée.
MAUVAIS
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
endBon ✅
Il y a deux opérations qui se produisent:
Si ces commandes se produisent en même temps, il obtient un verrou sur la table car il valide l'ensemble du tableau et scanne complètement la table. Pour éviter cette analyse complète de table, nous pouvons séparer les opérations.
Dans une 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.
endDans la prochaine migration:
def change do
execute "ALTER TABLE ingredients VALIDATE CONSTRAINT price_must_be_positive" , ""
# Acquires SHARE UPDATE EXCLUSIVE lock, which allows updates to continue
endCeux-ci peuvent être dans le même déploiement, mais assurez-vous qu'il existe 2 migrations distinctes.
Définir Not Null sur une colonne existante Blocs de lecture et écrit pendant que chaque ligne est vérifiée. Tout comme l'ajout d'un scénario de contrainte de vérification, il y a deux opérations:
Pour éviter le scan de table complet, nous pouvons séparer ces deux opérations.
MAUVAIS
def change do
alter table ( "recipes" ) do
modify :favourite , :boolean , null: false
end
endBon ✅
Ajoutez une contrainte de vérification sans la valider, le remblai de remblayage pour assurer la contrainte, puis la valider. Ce sera fonctionnellement équivalent.
Dans la première migration:
# Deployment 1
def change do
create constraint ( "recipes" , :favourite_not_null , check: "favourite IS NOT NULL" , validate: false )
endCela appliquera la contrainte dans toutes les nouvelles lignes, mais ne se souciera pas des lignes existantes tant que cette ligne ne sera pas mise à jour.
Vous aurez probablement besoin d'une migration de données à ce stade pour vous assurer que la contrainte est satisfaite.
Ensuite, dans la migration du prochain déploiement, nous appliquerons la contrainte sur toutes les lignes:
# Deployment 2
def change do
execute "ALTER TABLE recipes VALIDATE CONSTRAINT favourite_not_null" , ""
endSi vous utilisez Postgres 12+, vous pouvez ajouter la colonne non nul à la colonne après valider la contrainte. Des documents de Postgres 12:
Définir Not Null ne peut être appliqué qu'à une colonne à condition qu'aucun des enregistrements du tableau ne contienne une valeur nul pour la colonne. Habituellement, cela est vérifié pendant la table alter en balayant l'ensemble du tableau; Cependant, si une contrainte de vérification valide est trouvée qui prouve qu'aucun nul ne peut exister, la numérisation de la table est ignorée.
# **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 )
endSi votre contrainte échoue, vous devez d'abord envisager de rembourser les données pour couvrir les lacunes de l'intégrité des données souhaitée, puis révisitez la validation de la contrainte.
Les excellentes migrations ne peuvent pas garantir la sécurité des instructions SQL brutes. Assurez-vous vraiment que ce que vous faites est sûr, puis utilisez:
defmodule Cookbook.ExecuteRawSql do
# excellent_migrations:safety-assured-for-this-file raw_sql_executed
def change do
execute ( "..." )
end
endLa création d'un index bloquera les lectures et les écritures.
MAUVAIS
def change do
create index ( "recipes" , [ :slug ] )
# This obtains a ShareLock on "recipes" which will block writes to the table
endBon ✅
Avec Postgres, créez plutôt l'index simultanément qui ne bloque pas les lectures. Vous devrez désactiver les transactions de base de données à utiliser CONCURRENTLY , et comme Ecto obtient des verrous de migration via les transactions de base de données, cela implique également que les nœuds concurrents peuvent tenter d'essayer d'exécuter la même migration (par exemple, dans un environnement Kubernetes multi-nœuds qui exécute les migrations avant le démarrage). Par conséquent, certains nœuds échoueront au démarrage pour diverses raisons.
@ disable_ddl_transaction true
@ disable_migration_lock true
def change do
create index ( "recipes" , [ :slug ] , concurrently: true )
endLa migration peut encore prendre un certain temps, mais les lectures et les mises à jour des lignes continueront de fonctionner. Par exemple, pour 100 000 000 de lignes, il a fallu 165 secondes pour ajouter l'exécution de la migration, mais les sélections et les mises à jour pourraient se produire pendant son exécution.
N'ont pas d'autres changements dans la même migration ; Créez uniquement l'index simultanément et séparez d'autres modifications aux migrations ultérieures.
Les index simultanément doivent définir à la fois @disable_ddl_transaction et @disable_migration_lock sur true. Voir plus:
MAUVAIS
defmodule Cookbook.AddIndex do
def change do
create index ( :recipes , [ :cookbook_id , :cuisine ] , concurrently: true )
end
endBon ✅
defmodule Cookbook.AddIndex do
@ disable_ddl_transaction true
@ disable_migration_lock true
def change do
create index ( :recipes , [ :cookbook_id , :cuisine ] , concurrently: true )
end
endL'ajout d'un bloc de clés étranger écrit sur les deux tables.
MAUVAIS
def change do
alter table ( "recipes" ) do
add :cookbook_id , references ( "cookbooks" )
end
endBon ✅
Dans la première migration
def change do
alter table ( "recipes" ) do
add :cookbook_id , references ( "cookbooks" , validate: false )
end
endDans la deuxième migration
def change do
execute "ALTER TABLE recipes VALIDATE CONSTRAINT cookbook_id_fkey" , ""
endCes migrations peuvent être dans le même déploiement, mais assurez-vous que ce sont des migrations distinctes.
jsonDans Postgres, il n'y a pas d'opérateur d'égalité pour le type de colonne JSON, qui peut entraîner des erreurs pour les requêtes distinctes de sélection existantes dans votre application.
MAUVAIS
def change do
alter table ( "recipes" ) do
add :extra_data , :json
end
endBon ✅
Utilisez plutôt JSONB. Certains disent que c'est comme "JSON" mais " B etter".
def change do
alter table ( "recipes" ) do
add :extra_data , :jsonb
end
endMAUVAIS
L'ajout d'un indice non unique avec plus de trois colonnes améliore rarement les performances.
defmodule Cookbook.AddIndexOnIngredients do
def change do
create index ( :recipes , [ :a , :b , :c , :d ] , concurrently: true )
end
endBon ✅
Au lieu de cela, démarrez un index avec des colonnes qui rétrécissent le plus les résultats.
defmodule Cookbook.AddIndexOnIngredients do
def change do
create index ( :recipes , [ :b , :d ] , concurrently: true )
end
endPour Postgres, assurez-vous de les ajouter simultanément.
Pour marquer une opération dans une migration comme commentaire de configuration à usage sûr. Il sera ignoré pendant l'analyse.
Il y a deux commentaires de configuration disponibles:
excellent_migrations:safety-assured-for-next-line <operation_type>excellent_migrations:safety-assured-for-this-file <operation_type>Ignorer les vérifications de la ligne donnée:
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
endIgnorer les vérifications de l'ensemble du fichier:
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
endLes types de fonctionnement possibles sont:
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_renamedIgnorez les dangers spécifiques pour tous les contrôles de migration avec:
config :excellent_migrations , skip_checks: [ :raw_sql_executed , :not_null_added ] Pour ignorer l'analyse des migrations qui ont été créées avant d'ajouter ce package, définissez l'horodatage à partir de la dernière migration dans start_after dans config:
config :excellent_migrations , start_after: "20191026080101" Tout le monde est encouragé et bienvenu pour aider à améliorer ce projet. Voici quelques façons d'aider:
Copyright (C) 2021 Artur Sulej
Ce travail est gratuit. Vous pouvez le redistribuer et / ou le modifier selon les termes de la licence MIT. Voir le fichier licence.md pour plus de détails.