Detecte operações potencialmente perigosas ou destrutivas em suas migrações de banco de dados.
O pacote pode ser instalado adicionando :excellent_migrations à sua lista de dependências no mix.exs :
def deps do
[
{ :excellent_migrations , "~> 0.1" , only: [ :dev , :test ] , runtime: false }
]
end A documentação está disponível em hexdocs.
Esta ferramenta analisa o código (AST) dos arquivos de migração. Você não precisa editar ou incluir nenhum código adicional em seus arquivos de migração, exceto para adicionar um comentário de configuração ocasionalmente para garantir a segurança.
Existem várias maneiras de se integrar a excelentes migrações.
Excelentes migrações fornecem verificação personalizada e pronta para uso para credo.
Adicione ExcellentMigrations.CredoCheck.MigrationsSafety Safety ao seu arquivo .credo :
% {
configs: [
% {
# …
checks: [
# …
{ ExcellentMigrations.CredoCheck.MigrationsSafety , [ ] }
]
}
]
}Exemplo de aviso 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
Esse mix analisa as migrações e registra um aviso para cada perigo detectado.
mix excellent_migrations.migrate
A execução desta tarefa analisará primeiro as migrações. Se nenhum perigo for detectado, ele continuará e executará mix ecto.migrate . Se houver algum, ele registrará erros e parará.
Você também pode usá -lo no código. Para fazer isso, você precisa obter código -fonte e AST do seu arquivo de migração, por exemplo, via File.read!/1 e Code.string_to_quoted/2 . Em seguida, passe -os para ExcellentMigrations.DangersDetector.detect_dangers(ast) . Ele retornará uma lista de palavras -chave contendo tipos de perigo e linhas onde foram detectadas.
Operações potencialmente perigosas:
Cheques específicos para o pós -gres:
Melhores práticas:
Você também pode desativar verificações específicas.
Se o ECTO ainda estiver configurado para ler uma coluna em qualquer instância em execução do aplicativo, as consultas falharão ao carregar dados em suas estruturas. Isso pode acontecer em implantações de vários nós ou se você iniciar o aplicativo antes de executar migrações.
RUIM
# Without a code change to the Ecto Schema
def change do
alter table ( "recipes" ) do
remove :no_longer_needed_column
end
endBom ✅
A segurança pode ser garantida se o código do aplicativo for atualizado pela primeira vez para remover referências à coluna, para que não seja mais carregada ou consultada. Em seguida, a coluna pode ser removida com segurança da tabela.
Primeira implantação:
# First deploy, in the Ecto schema
defmodule Cookbook.Recipe do
schema "recipes" do
- column :no_longer_needed_column, :text
end
endSegunda implantação:
def change do
alter table ( "recipes" ) do
remove :no_longer_needed_column
end
endAdicionar uma coluna com um valor padrão a uma tabela existente pode fazer com que a tabela seja reescrita. Durante esse período, leituras e gravações são bloqueadas no Postgres, e as gravações são bloqueadas em MySQL e Mariadb.
RUIM
Nota: isso se torna seguro em:
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
endBom ✅
Adicione a coluna primeiro e depois altere -a para incluir o padrão.
Primeira migração:
def change do
alter table ( "recipes" ) do
add :favourite , :boolean
# This took 0.27 milliseconds for 100 million rows with no fkeys,
end
endSegunda migração:
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
endMudança de esquema para ler a nova coluna:
schema "recipes" do
+ field :favourite, :boolean, default: false
end Se o valor padrão for volátil (por exemplo, clock_timestamp() , uuid_generate_v4() , random() ) Cada linha precisará ser atualizada com o valor calculado na ALTER TABLE alteradores de tempo.
RUIM
Adicionando padrão volátil à coluna:
def change do
alter table ( :recipes ) do
modify ( :identifier , :uuid , default: fragment ( "uuid_generate_v4()" ) )
end
endAdicionando coluna com padrão volátil:
def change do
alter table ( :recipes ) do
add ( :identifier , :uuid , default: fragment ( "uuid_generate_v4()" ) )
end
endBom ✅
Para evitar uma operação de atualização potencialmente longa, principalmente se você pretende preencher a coluna com os valores principalmente não de verdade, pode ser preferível:
UPDATETambém é seguro criar uma nova tabela com coluna com padrão volátil, porque não contém nenhum registro.
A Ecto cria uma transação em torno de cada migração e o preenchimento na mesma transação que altera uma tabela mantém a tabela bloqueada durante a duração do aterro. Além disso, executar uma única consulta para atualizar dados pode causar problemas para tabelas grandes.
RUIM
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
endBom ✅
Existem várias estratégias diferentes para realizar preenchimento seguro. Este artigo os explica em grandes detalhes.
Alterar o tipo de coluna pode fazer com que a tabela seja reescrita. Durante esse período, leituras e gravações são bloqueadas no Postgres, e as gravações são bloqueadas em MySQL e Mariadb.
RUIM
Seguro no Postgres:
Seguro em mysql/mariadb:
def change do
alter table ( "recipes" ) do
modify :my_column , :boolean , from: :text
end
endBom ✅
Aproveite uma abordagem em fases:
Pergunte a si mesmo: "Eu realmente preciso renomear uma coluna?". Provavelmente não, mas se você precisar, leia e esteja ciente de que requer tempo e esforço.
Se o ECTO estiver configurado para ler uma coluna em qualquer instância em execução do aplicativo, as consultas falharão ao carregar dados em suas estruturas. Isso pode acontecer em implantações de vários nós ou se você iniciar o aplicativo antes de executar migrações.
Há um atalho: não renomeie a coluna do banco de dados e, em vez disso, renomeie o nome do campo do esquema e configure -o para apontar para a coluna do banco de dados.
RUIM
# In your schema
schema "recipes" do
field :summary , :text
end
# In your migration
def change do
rename table ( "recipes" ) , :title , to: :summary
endO tempo entre sua migração em execução e seu aplicativo obtendo o novo código pode encontrar problemas.
Bom ✅
Estratégia 1
Renomeie apenas o campo no esquema e configure -o para apontar para a coluna do banco de dados e manter a coluna do banco de dados a mesma. Verifique se todo o código de chamada que depende do nome do campo antigo também é atualizado para fazer referência ao novo nome do 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_minutesEstratégia 2
Aproveite uma abordagem em fases:
Pergunte a si mesmo: "Eu realmente preciso renomear uma tabela?". Provavelmente não, mas se você precisar, leia e esteja ciente de que requer tempo e esforço.
Se o ECTO ainda estiver configurado para ler uma tabela em qualquer instância em execução do aplicativo, as consultas falharão ao carregar dados em suas estruturas. Isso pode acontecer em implantações de vários nós ou se você iniciar o aplicativo antes de executar migrações.
Há um atalho: renomeie apenas o esquema e não altere o nome da tabela de banco de dados subjacente.
RUIM
def change do
rename table ( "recipes" ) , to: table ( "dish_algorithms" )
endBom ✅
Estratégia 1
Renomeie apenas o esquema e todo o código de chamada e não renomeie a tabela:
- 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")Estratégia 2
Aproveite uma abordagem em fases:
Adicionando um bloqueio de restrição de verificação leituras e gravações na tabela no Postgres, e os blocos escrevem no mysql/mariadb enquanto cada linha é verificada.
RUIM
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
endBom ✅
Existem duas operações ocorrendo:
Se esses comandos estiverem acontecendo ao mesmo tempo, ele obterá uma trava na mesa, pois valida a tabela inteira e digitaliza completamente a tabela. Para evitar essa varredura completa, podemos separar as operações.
Em uma migração:
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.
endNa próxima migração:
def change do
execute "ALTER TABLE ingredients VALIDATE CONSTRAINT price_must_be_positive" , ""
# Acquires SHARE UPDATE EXCLUSIVE lock, which allows updates to continue
endEstes podem estar na mesma implantação, mas garantir que existem 2 migrações separadas.
Definir não nulo em um bloco de coluna existente leituras e gravações enquanto cada linha é verificada. Assim como a adição de um cenário de restrição de verificação, existem duas operações ocorrendo:
Para evitar a varredura completa da tabela, podemos separar essas duas operações.
RUIM
def change do
alter table ( "recipes" ) do
modify :favourite , :boolean , null: false
end
endBom ✅
Adicione uma restrição de verificação sem validá -la, preencher dados para saciar a restrição e, em seguida, validá -los. Isso será funcionalmente equivalente.
Na primeira migração:
# Deployment 1
def change do
create constraint ( "recipes" , :favourite_not_null , check: "favourite IS NOT NULL" , validate: false )
endIsso aplicará a restrição em todas as novas linhas, mas não se importa com as linhas existentes até que essa linha seja atualizada.
Você provavelmente precisará de uma migração de dados neste momento para garantir que a restrição seja satisfeita.
Então, na migração da próxima implantação, aplicaremos a restrição em todas as linhas:
# Deployment 2
def change do
execute "ALTER TABLE recipes VALIDATE CONSTRAINT favourite_not_null" , ""
endSe você estiver usando o Postgres 12+, poderá adicionar o não nulo à coluna após validar a restrição. Do Postgres 12 Docs:
O conjunto NOUN NULL só pode ser aplicado a uma coluna, desde que nenhum dos registros na tabela contenha um valor nulo para a coluna. Normalmente, isso é verificado durante a tabela de alterar, digitalizando toda a tabela; No entanto, se uma restrição de verificação válida for encontrada, o que prova que nenhum nulo pode existir, a varredura da tabela será ignorada.
# **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 )
endSe a sua restrição falhar, considere os dados de preenchimento primeiro para cobrir as lacunas na integridade desejada dos dados e revisitar a validação da restrição.
Excelentes migrações não podem garantir a segurança das instruções SQL brutas. Certifique -se de que o que você está fazendo seja seguro e depois use:
defmodule Cookbook.ExecuteRawSql do
# excellent_migrations:safety-assured-for-this-file raw_sql_executed
def change do
execute ( "..." )
end
endCriar um índice bloqueará as leituras e as gravações.
RUIM
def change do
create index ( "recipes" , [ :slug ] )
# This obtains a ShareLock on "recipes" which will block writes to the table
endBom ✅
Com o Postgres, crie o índice simultaneamente, que não bloqueia leituras. Você precisará desativar as transações do banco de dados para usar CONCURRENTLY e, como o ECTO obtém bloqueios de migração através de transações de banco de dados, isso também implica que nós concorrentes podem tentar executar a mesma migração (por exemplo, em um ambiente de Kubernetes com vários nós que executa migrações antes da startup). Portanto, alguns nós falharão com uma variedade de razões.
@ disable_ddl_transaction true
@ disable_migration_lock true
def change do
create index ( "recipes" , [ :slug ] , concurrently: true )
endA migração ainda pode demorar um pouco para correr, mas lê e atualizações para as linhas continuarão a funcionar. Por exemplo, para 100.000.000 linhas, foram necessários 165 segundos para adicionar a migração, mas as seleções e atualizações poderiam ocorrer enquanto estava em execução.
Não têm outras mudanças na mesma migração ; Crie apenas o índice simultaneamente e separe outras alterações nas migrações posteriores.
Os índices simultaneamente precisam definir @disable_ddl_transaction e @disable_migration_lock como true. Veja mais:
RUIM
defmodule Cookbook.AddIndex do
def change do
create index ( :recipes , [ :cookbook_id , :cuisine ] , concurrently: true )
end
endBom ✅
defmodule Cookbook.AddIndex do
@ disable_ddl_transaction true
@ disable_migration_lock true
def change do
create index ( :recipes , [ :cookbook_id , :cuisine ] , concurrently: true )
end
endAdicionando um bloqueio de tecla estrangeira grava em ambas as tabelas.
RUIM
def change do
alter table ( "recipes" ) do
add :cookbook_id , references ( "cookbooks" )
end
endBom ✅
Na primeira migração
def change do
alter table ( "recipes" ) do
add :cookbook_id , references ( "cookbooks" , validate: false )
end
endNa segunda migração
def change do
execute "ALTER TABLE recipes VALIDATE CONSTRAINT cookbook_id_fkey" , ""
endEssas migrações podem estar na mesma implantação, mas verifique se são migrações separadas.
jsonNo Postgres, não existe um operador de igualdade para o tipo de coluna JSON, que pode causar erros para consultas distintas selecionadas existentes em seu aplicativo.
RUIM
def change do
alter table ( "recipes" ) do
add :extra_data , :json
end
endBom ✅
Use JSONB em vez disso. Alguns dizem que é como "JSON", mas " B Etter".
def change do
alter table ( "recipes" ) do
add :extra_data , :jsonb
end
endRUIM
Adicionar um índice não único com mais de três colunas raramente melhora o desempenho.
defmodule Cookbook.AddIndexOnIngredients do
def change do
create index ( :recipes , [ :a , :b , :c , :d ] , concurrently: true )
end
endBom ✅
Em vez disso, inicie um índice com colunas que restrinjam os resultados mais.
defmodule Cookbook.AddIndexOnIngredients do
def change do
create index ( :recipes , [ :b , :d ] , concurrently: true )
end
endPara o Postgres, adicione -os simultaneamente.
Para marcar uma operação em uma migração como um comentário de configuração de uso seguro. Será ignorado durante a análise.
Existem dois comentários de configuração disponíveis:
excellent_migrations:safety-assured-for-next-line <operation_type>excellent_migrations:safety-assured-for-this-file <operation_type>Ignorando os cheques para uma determinada linha:
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 verificações para todo o arquivo:
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
endOs possíveis tipos de operação são:
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 perigos específicos para todas as verificações de migração com:
config :excellent_migrations , skip_checks: [ :raw_sql_executed , :not_null_added ] Para pular a análise das migrações criadas antes de adicionar este pacote, defina o registro de data e hora da última migração em start_after na configuração:
config :excellent_migrations , start_after: "20191026080101" Todos são encorajados e bem -vindos a ajudar a melhorar este projeto. Aqui estão algumas maneiras de ajudar:
Copyright (C) 2021 Artur Sulej
Este trabalho é gratuito. Você pode redistribuí -lo e/ou modificá -lo nos termos da licença do MIT. Consulte o arquivo License.md para obter mais detalhes.