Detectar operaciones potencialmente peligrosas o destructivas en las migraciones de su base de datos.
El paquete se puede instalar agregando :excellent_migrations a su lista de dependencias en mix.exs :
def deps do
[
{ :excellent_migrations , "~> 0.1" , only: [ :dev , :test ] , runtime: false }
]
end La documentación está disponible en hexdocs.
Esta herramienta analiza el código (AST) de los archivos de migración. No tiene que editar ni incluir ningún código adicional en sus archivos de migración, excepto para agregar ocasionalmente un comentario de configuración para garantizar la seguridad.
Hay múltiples formas de integrarse con excelentes migraciones.
Las excelentes migraciones proporcionan una verificación personalizada y lista para usar para Credo.
Agregue ExcellentMigrations.CredoCheck.MigrationsSafety a su archivo .credo :
% {
configs: [
% {
# …
checks: [
# …
{ ExcellentMigrations.CredoCheck.MigrationsSafety , [ ] }
]
}
]
}Ejemplo de advertencias de 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
Esta tarea de mezcla analiza las migraciones y registra una advertencia para cada peligro detectado.
mix excellent_migrations.migrate
Ejecutar esta tarea analizará primero las migraciones. Si no se detectan peligros, continuará y ejecutará mix ecto.migrate . Si hay alguno, registrará errores y se detendrá.
También puede usarlo en código. Para hacerlo, debe obtener el código fuente y el AST de su archivo de migración, por ejemplo, a través de File.read!/1 y Code.string_to_quoted/2 . Luego pase a ExcellentMigrations.DangersDetector.detect_dangers(ast) . Devolverá una lista de palabras clave que contiene tipos de peligro y líneas donde fueron detectados.
Operaciones potencialmente peligrosas:
Verificaciones específicas de Postgres:
Las mejores prácticas:
También puede deshabilitar cheques específicos.
Si el ecto todavía está configurado para leer una columna en cualquier instancia en ejecución de la aplicación, las consultas fallarán al cargar datos en sus estructuras. Esto puede suceder en implementaciones de múltiples nodos o si inicia la aplicación antes de ejecutar migraciones.
MALO
# Without a code change to the Ecto Schema
def change do
alter table ( "recipes" ) do
remove :no_longer_needed_column
end
endBueno ✅
La seguridad se puede asegurar si el código de aplicación se actualiza primero para eliminar las referencias a la columna, por lo que ya no está cargado o consultado. Luego, la columna se puede eliminar de manera segura de la tabla.
Primer implementación:
# First deploy, in the Ecto schema
defmodule Cookbook.Recipe do
schema "recipes" do
- column :no_longer_needed_column, :text
end
endSegunda implementación:
def change do
alter table ( "recipes" ) do
remove :no_longer_needed_column
end
endAgregar una columna con un valor predeterminado a una tabla existente puede hacer que la tabla se reescriba. Durante este tiempo, las lecturas y las escrituras están bloqueadas en Postgres, y las escrituras están bloqueadas en MySQL y MariadB.
MALO
Nota: Esto se vuelve seguro en:
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
endBueno ✅
Agregue la columna primero, luego altera para incluir el valor predeterminado.
Primera migración:
def change do
alter table ( "recipes" ) do
add :favourite , :boolean
# This took 0.27 milliseconds for 100 million rows with no fkeys,
end
endSegunda migración:
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
endCambio de esquema para leer la nueva columna:
schema "recipes" do
+ field :favourite, :boolean, default: false
end Si el valor predeterminado es volátil (por ejemplo, clock_timestamp() , uuid_generate_v4() , random() ) Cada fila deberá actualizarse con el valor calculado en la ALTER TABLE momento.
MALO
Agregar Volátil predeterminado a la columna:
def change do
alter table ( :recipes ) do
modify ( :identifier , :uuid , default: fragment ( "uuid_generate_v4()" ) )
end
endAgregar columna con valor predeterminado volátil:
def change do
alter table ( :recipes ) do
add ( :identifier , :uuid , default: fragment ( "uuid_generate_v4()" ) )
end
endBueno ✅
Para evitar una operación de actualización potencialmente larga, particularmente si tiene la intención de llenar la columna con valores en su mayoría no defectos de todos modos, puede ser preferible:
UPDATETambién es seguro crear una nueva tabla con columna con Volátil predeterminado, ya que no contiene ningún registro.
ECTO crea una transacción alrededor de cada migración y rellena en la misma transacción que altera una tabla mantiene la tabla bloqueada durante la duración del relleno. Además, ejecutar una sola consulta para actualizar los datos puede causar problemas para tablas grandes.
MALO
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
endBueno ✅
Hay varias estrategias diferentes para realizar un relleno seguro. Este artículo los explica en excelentes detalles.
Cambiar el tipo de columna puede hacer que la tabla sea reescribida. Durante este tiempo, las lecturas y las escrituras están bloqueadas en Postgres, y las escrituras están bloqueadas en MySQL y MariadB.
MALO
Safe en Postgres:
Seguro en mysql/mariadb:
def change do
alter table ( "recipes" ) do
modify :my_column , :boolean , from: :text
end
endBueno ✅
Tome un enfoque gradual:
Pregúntese: "¿ Realmente necesito cambiar el nombre de una columna?". Probablemente no, pero si es necesario, siga leyendo y tenga en cuenta que requiere tiempo y esfuerzo.
Si el ecto está configurado para leer una columna en cualquier instancia en ejecución de la aplicación, las consultas fallarán al cargar datos en sus estructuras. Esto puede suceder en implementaciones de múltiples nodos o si inicia la aplicación antes de ejecutar migraciones.
Hay un atajo: no cambie el nombre de la columna de la base de datos y, en su lugar, cambie el nombre del nombre del campo del esquema y lo configure para apuntar a la columna de la base de datos.
MALO
# In your schema
schema "recipes" do
field :summary , :text
end
# In your migration
def change do
rename table ( "recipes" ) , :title , to: :summary
endEl tiempo entre la ejecución de su migración y su aplicación que obtiene el nuevo código puede encontrar problemas.
Bueno ✅
Estrategia 1
Cambie el nombre del campo solo en el esquema y configúrelo para apuntar a la columna de la base de datos y mantenga la columna de la base de datos igual. Asegúrese de que todo el código de llamadas que confíe en el nombre de campo anterior también se actualiza para hacer referencia al nuevo nombre de campo.
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_minutesEstrategia 2
Tome un enfoque gradual:
Pregúntese: "¿ Realmente necesito cambiar el nombre de una mesa?". Probablemente no, pero si es necesario, siga leyendo y tenga en cuenta que requiere tiempo y esfuerzo.
Si el ecto todavía está configurado para leer una tabla en cualquier instancia en ejecución de la aplicación, las consultas fallarán al cargar datos en sus estructuras. Esto puede suceder en implementaciones de múltiples nodos o si inicia la aplicación antes de ejecutar migraciones.
Hay un atajo: cambiar el nombre del esquema solamente y no cambiar el nombre de la tabla de la base de datos subyacente.
MALO
def change do
rename table ( "recipes" ) , to: table ( "dish_algorithms" )
endBueno ✅
Estrategia 1
Cambie el nombre del esquema solo y todo el código de llamadas, y no cambie el nombre de la tabla:
- 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")Estrategia 2
Tome un enfoque gradual:
Agregar una restricción de verificación Bloques de lectura y escritura en la tabla en Postgres, y Blocks escribe en MySQL/MariadB mientras se verifica cada fila.
MALO
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
endBueno ✅
Hay dos operaciones que ocurren:
Si estos comandos están ocurriendo al mismo tiempo, obtiene un bloqueo en la tabla, ya que valida toda la tabla y escanea completamente la tabla. Para evitar este escaneo completo de la tabla, podemos separar las operaciones.
En una migración:
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.
endEn la próxima migración:
def change do
execute "ALTER TABLE ingredients VALIDATE CONSTRAINT price_must_be_positive" , ""
# Acquires SHARE UPDATE EXCLUSIVE lock, which allows updates to continue
endEstos pueden estar en la misma implementación, pero asegúrese de que haya 2 migraciones separadas.
Establecer no nulo en una columna existente bloquea y escribe mientras se verifica cada fila. Al igual que el escenario de restricción Agregar una verificación, ocurren dos operaciones:
Para evitar el escaneo completo de la tabla, podemos separar estas dos operaciones.
MALO
def change do
alter table ( "recipes" ) do
modify :favourite , :boolean , null: false
end
endBueno ✅
Agregue una restricción de verificación sin validarla, rellenar datos para saciar la restricción y luego validarla. Esto será funcionalmente equivalente.
En la primera migración:
# Deployment 1
def change do
create constraint ( "recipes" , :favourite_not_null , check: "favourite IS NOT NULL" , validate: false )
endEsto hará cumplir la restricción en todas las filas nuevas, pero no le importará las filas existentes hasta que se actualice esa fila.
Es probable que necesite una migración de datos en este momento para garantizar que la restricción esté satisfecha.
Luego, en la migración de la próxima implementación, haremos cumplir la restricción en todas las filas:
# Deployment 2
def change do
execute "ALTER TABLE recipes VALIDATE CONSTRAINT favourite_not_null" , ""
endSi está utilizando Postgres 12+, puede agregar el no nulo a la columna después de validar la restricción. Desde el Postgres 12 Docs:
El conjunto no nulo solo se puede aplicar a una columna siempre que ninguno de los registros en la tabla contenga un valor nulo para la columna. Por lo general, esto se verifica durante la tabla ALTER escaneando toda la tabla; Sin embargo, si se encuentra una restricción de verificación válida que demuestra que no puede existir un nulo, entonces se omite el escaneo de la tabla.
# **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 su restricción falla, entonces debe considerar los datos de relleno primero para cubrir las brechas en la integridad de los datos deseada, luego revise la validación de la restricción.
Excelentes migraciones no pueden garantizar la seguridad de las declaraciones SQL en bruto. Asegúrese de que lo que está haciendo es seguro, luego use:
defmodule Cookbook.ExecuteRawSql do
# excellent_migrations:safety-assured-for-this-file raw_sql_executed
def change do
execute ( "..." )
end
endLa creación de un índice bloqueará las lecturas y las escrituras.
MALO
def change do
create index ( "recipes" , [ :slug ] )
# This obtains a ShareLock on "recipes" which will block writes to the table
endBueno ✅
Con Postgres, en su lugar, cree el índice simultáneamente que no bloquea las lecturas. Deberá deshabilitar las transacciones de la base de datos para usar CONCURRENTLY , y dado que ecto obtiene bloqueos de migración a través de transacciones de bases de datos, esto también implica que los nodos competitivos pueden intentar ejecutar la misma migración (por ejemplo, en un entorno de Kubernetes múltiples que ejecuta migraciones antes de la inicio). Por lo tanto, algunos nodos fallarán el inicio por una variedad de razones.
@ disable_ddl_transaction true
@ disable_migration_lock true
def change do
create index ( "recipes" , [ :slug ] , concurrently: true )
endLa migración aún puede tardar un tiempo en ejecutarse, pero las lecturas y las actualizaciones de las filas continuarán funcionando. Por ejemplo, para 100,000,000 de filas tardó 165 segundos en agregar Ejecutar la migración, pero las selecciones y actualizaciones podrían ocurrir mientras se ejecutaba.
No tienen otros cambios en la misma migración ; Solo cree el índice simultáneamente y separe otros cambios a las migraciones posteriores.
Los índices simultáneos deben establecer tanto @disable_ddl_transaction como @disable_migration_lock en True. Ver más:
MALO
defmodule Cookbook.AddIndex do
def change do
create index ( :recipes , [ :cookbook_id , :cuisine ] , concurrently: true )
end
endBueno ✅
defmodule Cookbook.AddIndex do
@ disable_ddl_transaction true
@ disable_migration_lock true
def change do
create index ( :recipes , [ :cookbook_id , :cuisine ] , concurrently: true )
end
endAgregar un bloqueo externo escribe en ambas tablas.
MALO
def change do
alter table ( "recipes" ) do
add :cookbook_id , references ( "cookbooks" )
end
endBueno ✅
En la primera migración
def change do
alter table ( "recipes" ) do
add :cookbook_id , references ( "cookbooks" , validate: false )
end
endEn la segunda migración
def change do
execute "ALTER TABLE recipes VALIDATE CONSTRAINT cookbook_id_fkey" , ""
endEstas migraciones pueden estar en la misma implementación, pero asegúrese de que sean migraciones separadas.
jsonEn Postgres, no hay operador de igualdad para el tipo de columna JSON, lo que puede causar errores para consultas distintas selectas existentes en su aplicación.
MALO
def change do
alter table ( "recipes" ) do
add :extra_data , :json
end
endBueno ✅
Use JSONB en su lugar. Algunos dicen que es como "json" pero " b etter".
def change do
alter table ( "recipes" ) do
add :extra_data , :jsonb
end
endMALO
Agregar un índice no único con más de tres columnas rara vez mejora el rendimiento.
defmodule Cookbook.AddIndexOnIngredients do
def change do
create index ( :recipes , [ :a , :b , :c , :d ] , concurrently: true )
end
endBueno ✅
En su lugar, comience un índice con columnas que reduzcan más los resultados.
defmodule Cookbook.AddIndexOnIngredients do
def change do
create index ( :recipes , [ :b , :d ] , concurrently: true )
end
endPara Postgres, asegúrese de agregarlos simultáneamente.
Para marcar una operación en una migración como un comentario de configuración de uso seguro. Será ignorado durante el análisis.
Hay dos comentarios de configuración disponibles:
excellent_migrations:safety-assured-for-next-line <operation_type>excellent_migrations:safety-assured-for-this-file <operation_type>Ignorando los cheques para la línea dada:
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
endIgnorando los cheques para todo el archivo:
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
endLos posibles tipos de operación son:
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_renamedIgnore los peligros específicos para todas las verificaciones de migración con:
config :excellent_migrations , skip_checks: [ :raw_sql_executed , :not_null_added ] Para omitir el análisis de las migraciones que se crearon antes de agregar este paquete, establezca la marca de tiempo de la última migración en start_after en config:
config :excellent_migrations , start_after: "20191026080101" Se alienta a todos y bienvenidos para ayudar a mejorar este proyecto. Aquí hay algunas maneras en que puede ayudar:
Copyright (c) 2021 Artur Sulej
Este trabajo es gratis. Puede redistribuirlo y/o modificarlo bajo los términos de la licencia MIT. Consulte el archivo License.md para obtener más detalles.