Обнаружение потенциально опасных или разрушительных операций в миграции базы данных.
Пакет может быть установлен, добавив :excellent_migrations в ваш список зависимостей в mix.exs :
def deps do
[
{ :excellent_migrations , "~> 0.1" , only: [ :dev , :test ] , runtime: false }
]
end Документация доступна на Hexdocs.
Этот инструмент анализирует код (AST) миграционных файлов. Вам не нужно редактировать или включать какой -либо дополнительный код в ваши миграционные файлы, за исключением случаев, когда иногда добавляете комментарий конфигурации для обеспечения безопасности.
Есть несколько способов интеграции с отличными миграциями.
Отличные миграции обеспечивают пользовательский, готовый к использованию чек для Credo.
Добавить ExcellentMigrations.CredoCheck.MigrationsSafety .credo
% {
configs: [
% {
# …
checks: [
# …
{ ExcellentMigrations.CredoCheck.MigrationsSafety , [ ] }
]
}
]
}Пример предупреждений 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
Эта задача с миксом анализирует миграции и регистрирует предупреждение для каждой обнаруженной опасности.
mix excellent_migrations.migrate
Запуск этой задачи сначала проанализирует миграции. Если не обнаружено опасности, это будет продолжаться и запустить mix ecto.migrate . Если есть, это будет регистрировать ошибки и остановиться.
Вы также можете использовать его в коде. Для этого необходимо получить исходный код и AST вашего файла миграции, например, через File.read!/1 и Code.string_to_quoted/2 . Затем передайте их в ExcellentMigrations.DangersDetector.detect_dangers(ast) . Он вернет список ключевых слов, содержащий типы опасности и строки, где они были обнаружены.
Потенциально опасные операции:
Postgres-специфические проверки:
Лучшие практики:
Вы также можете отключить конкретные проверки.
Если ECTO все еще настроен для чтения столбца в любых запущенных экземплярах приложения, то запросы не будут выполняться при загрузке данных в ваши структуры. Это может произойти в мультизлеточных развертываниях или, если вы запускаете приложение перед запусками миграции.
ПЛОХОЙ
# Without a code change to the Ecto Schema
def change do
alter table ( "recipes" ) do
remove :no_longer_needed_column
end
endХорошо ✅
Безопасность может быть гарантирована, если код приложения сначала обновлен для удаления ссылок на столбец, чтобы он больше не загружался или не запрашивается. Затем колонна может быть безопасно удален из таблицы.
Первое развертывание:
# First deploy, in the Ecto schema
defmodule Cookbook.Recipe do
schema "recipes" do
- column :no_longer_needed_column, :text
end
endВторое развертывание:
def change do
alter table ( "recipes" ) do
remove :no_longer_needed_column
end
endДобавление столбца со значением по умолчанию в существующую таблицу может привести к перезаписи таблицы. В течение этого времени чтения и писания заблокированы в Postgres, и записи заблокированы в MySQL и MariaDB.
ПЛОХОЙ
Примечание: это становится безопасным в:
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
endХорошо ✅
Сначала добавьте столбец, затем измените его, чтобы включить по умолчанию.
Первая миграция:
def change do
alter table ( "recipes" ) do
add :favourite , :boolean
# This took 0.27 milliseconds for 100 million rows with no fkeys,
end
endВторая миграция:
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
endСхема изменение, чтобы прочитать новый столбец:
schema "recipes" do
+ field :favourite, :boolean, default: false
end Если значение по умолчанию является изменчивым (например, clock_timestamp() , uuid_generate_v4() , random() ), каждая строка должна быть обновлена со значением, рассчитанным в то время, когда будет выполнена ALTER TABLE .
ПЛОХОЙ
Добавление летучих по умолчанию в столбец:
def change do
alter table ( :recipes ) do
modify ( :identifier , :uuid , default: fragment ( "uuid_generate_v4()" ) )
end
endДобавление столбца с нестабильным дефолтом:
def change do
alter table ( :recipes ) do
add ( :identifier , :uuid , default: fragment ( "uuid_generate_v4()" ) )
end
endХорошо ✅
Чтобы избежать потенциально длительной операции обновления, особенно если вы намереваетесь заполнить столбец в основном невыхочными значениями, это может быть предпочтительным для:
UPDATEТакже создание новой таблицы с столбцом с нестабильным по умолчанию безопасно, поскольку она не содержит никаких записей.
Ecto создает транзакцию вокруг каждой миграции и засыпание в той же транзакции, которая изменяет таблицу, держит таблицу заблокированной на протяжении всей засыпки. Кроме того, запуск одного запроса для обновления данных может вызвать проблемы для больших таблиц.
ПЛОХОЙ
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
endХорошо ✅
Есть несколько разных стратегий для выполнения безопасной засыпки. Эта статья объясняет их отличными деталями.
Изменение типа столбца может привести к перезаписи таблицы. В течение этого времени чтения и писания заблокированы в Postgres, и записи заблокированы в MySQL и MariaDB.
ПЛОХОЙ
Безопасно в Postgres:
Безопасно в MySQL/MariaDB:
def change do
alter table ( "recipes" ) do
modify :my_column , :boolean , from: :text
end
endХорошо ✅
Примите поэтапный подход:
Спросите себя: « Нужно ли мне переименовать колонку?». Вероятно, нет, но если вам нужно, читать дальше и знать, что это требует времени и усилий.
Если ECTO настроен для чтения столбца в любых запущенных экземплярах приложения, то запросы не будут выполняться при загрузке данных в ваши структуры. Это может произойти в мультизлеточных развертываниях или, если вы запускаете приложение перед запусками миграции.
Существует ярлык: не переименуйте столбец базы данных и вместо этого переименуйте имя поля схемы и настраивайте его, чтобы указать на столбец базы данных.
ПЛОХОЙ
# In your schema
schema "recipes" do
field :summary , :text
end
# In your migration
def change do
rename table ( "recipes" ) , :title , to: :summary
endВремя между работой миграции и вашим приложением, получением нового кода, может столкнуться с проблемами.
Хорошо ✅
Стратегия 1
Переименуйте поле только в схеме и настройте его, чтобы указать на столбец базы данных и сохранить столбец базы данных одинаковым. Убедитесь, что весь вызовный код полагается на старое имя поля, также обновляется для ссылки на новое имя поля.
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_minutesСтратегия 2
Примите поэтапный подход:
Спросите себя: « Нужно ли мне переименовать стол?». Вероятно, нет, но если вам нужно, читать дальше и знать, что это требует времени и усилий.
Если ECTO все еще настроен для чтения таблицы в любых запущенных экземплярах приложения, то запросы не будут выполняться при загрузке данных в ваши структуры. Это может произойти в мультизлеточных развертываниях или, если вы запускаете приложение перед запусками миграции.
Существует ярлык: переименовать только схему и не изменяйте имя таблицы базы данных.
ПЛОХОЙ
def change do
rename table ( "recipes" ) , to: table ( "dish_algorithms" )
endХорошо ✅
Стратегия 1
Переименовать только схему и все вызовов кода, и не переименованы в таблицу:
- 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")Стратегия 2
Примите поэтапный подход:
Добавление блоков проверки считывается и записывается в таблицу в Postgres, и Blocks пишет в MySQL/MariaDB, пока каждый строк проверяется.
ПЛОХОЙ
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
endХорошо ✅
Происходит две операции:
Если эти команды происходят одновременно, он получает блокировку на таблице, так как проверяет всю таблицу и полностью сканирует таблицу. Чтобы избежать этого полного сканирования таблицы, мы можем отделить операции.
В одной миграции:
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.
endВ следующей миграции:
def change do
execute "ALTER TABLE ingredients VALIDATE CONSTRAINT price_must_be_positive" , ""
# Acquires SHARE UPDATE EXCLUSIVE lock, which allows updates to continue
endОни могут быть в том же развертывании, но убедитесь, что есть 2 отдельных миграции.
Настройка не нулевого на существующих блоках столбцов считывает и записывается, пока каждая строка проверяется. Так же, как сценарий добавления проверки ограничения, происходит две операции:
Чтобы избежать полного сканирования таблицы, мы можем разделить эти две операции.
ПЛОХОЙ
def change do
alter table ( "recipes" ) do
modify :favourite , :boolean , null: false
end
endХорошо ✅
Добавьте ограничение проверки без подтверждения его, данных обратной засыпки, чтобы удовлетворить ограничение, а затем проверить его. Это будет функционально эквивалентно.
В первой миграции:
# Deployment 1
def change do
create constraint ( "recipes" , :favourite_not_null , check: "favourite IS NOT NULL" , validate: false )
endЭто обеспечит соблюдение ограничения во всех новых рядах, но не заботится о существующих рядах, пока эта строка не будет обновлена.
На этом этапе вам, вероятно, понадобится миграция данных, чтобы обеспечить удовлетворение ограничения.
Затем в миграции следующего развертывания мы обеспечим ограничение на всех рядах:
# Deployment 2
def change do
execute "ALTER TABLE recipes VALIDATE CONSTRAINT favourite_not_null" , ""
endЕсли вы используете Postgres 12+, вы можете добавить Not Null в столбец после проверки ограничения. Из Postgres 12 документов:
Установить NOT NULL может применяться только к столбцу, при условии, что ни одна из записей в таблице не содержит нулевого значения для столбца. Обычно это проверяется во время таблицы альтер, сканируя всю таблицу; Однако, если обнаружено достоверное ограничение проверки, которое доказывает, что NOU NOL не может существовать, то сканирование таблицы пропускается.
# **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 )
endЕсли ваше ограничение не удается, то сначала вы должны рассмотреть вопрос о том, чтобы охватить пробелы в желаемой целостности данных, а затем верните проверку ограничения.
Отличные миграции не могут обеспечить безопасность для необработанных заявлений SQL. Убедитесь, что то, что вы делаете, безопасно, затем используйте:
defmodule Cookbook.ExecuteRawSql do
# excellent_migrations:safety-assured-for-this-file raw_sql_executed
def change do
execute ( "..." )
end
endСоздание индекса заблокирует как чтения, так и записи.
ПЛОХОЙ
def change do
create index ( "recipes" , [ :slug ] )
# This obtains a ShareLock on "recipes" which will block writes to the table
endХорошо ✅
С помощью Postgres вместо этого создайте индекс одновременно, который не блокирует чтения. Вам необходимо будет отключить транзакции базы данных для использования CONCURRENTLY , и, поскольку ECTO получает блокировки миграции через транзакции базы данных, это также подразумевает, что конкурирующие узлы могут попытаться попытаться запустить ту же миграцию (например, в мультизлевой среде Kubernetes, которая запускает миграции перед запуском). Следовательно, некоторые узлы провалится по запуску по разным причинам.
@ disable_ddl_transaction true
@ disable_migration_lock true
def change do
create index ( "recipes" , [ :slug ] , concurrently: true )
endМиграция может по -прежнему занять некоторое время, но чтения и обновления в ряды будут продолжать работать. Например, для 100 000 000 строк потребовалось 165 секунд, чтобы добавить запустить миграцию, но при этом могут возникнуть выбора и обновления.
Не иметь других изменений в той же миграции ; Создайте только индекс одновременно и разделяйте другие изменения от более поздних миграций.
Одновременно индексы должны установить как @disable_ddl_transaction , так и @disable_migration_lock в true. Смотрите больше:
ПЛОХОЙ
defmodule Cookbook.AddIndex do
def change do
create index ( :recipes , [ :cookbook_id , :cuisine ] , concurrently: true )
end
endХорошо ✅
defmodule Cookbook.AddIndex do
@ disable_ddl_transaction true
@ disable_migration_lock true
def change do
create index ( :recipes , [ :cookbook_id , :cuisine ] , concurrently: true )
end
endДобавление блоков иностранного ключа пишет в обеих таблицах.
ПЛОХОЙ
def change do
alter table ( "recipes" ) do
add :cookbook_id , references ( "cookbooks" )
end
endХорошо ✅
В первой миграции
def change do
alter table ( "recipes" ) do
add :cookbook_id , references ( "cookbooks" , validate: false )
end
endВо второй миграции
def change do
execute "ALTER TABLE recipes VALIDATE CONSTRAINT cookbook_id_fkey" , ""
endЭти миграции могут быть в том же развертывании, но убедитесь, что они являются отдельными миграциями.
jsonВ Postgres нет оператора равенства для типа столбца JSON, что может вызвать ошибки для существующих выбора различных запросов в вашем приложении.
ПЛОХОЙ
def change do
alter table ( "recipes" ) do
add :extra_data , :json
end
endХорошо ✅
Вместо этого используйте JSONB. Некоторые говорят, что это как «json», но « b etter».
def change do
alter table ( "recipes" ) do
add :extra_data , :jsonb
end
endПЛОХОЙ
Добавление индекса не-юнического состава с более чем тремя столбцами редко повышает производительность.
defmodule Cookbook.AddIndexOnIngredients do
def change do
create index ( :recipes , [ :a , :b , :c , :d ] , concurrently: true )
end
endХорошо ✅
Вместо этого запустите индекс с столбцов, которые больше всего сужают результаты.
defmodule Cookbook.AddIndexOnIngredients do
def change do
create index ( :recipes , [ :b , :d ] , concurrently: true )
end
endДля Postgres обязательно добавьте их одновременно.
Чтобы отметить операцию в миграции в качестве безопасного использования комментария конфигурации. Это будет проигнорировано во время анализа.
Доступны два комментария конфигурации:
excellent_migrations:safety-assured-for-next-line <operation_type>excellent_migrations:safety-assured-for-this-file <operation_type>Игнорируя проверки для данной линии:
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
endИгнорируя проверки для всего файла:
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
endВозможные типы операций:
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_renamedИгнорируйте конкретные опасности для всех проверок миграции с:
config :excellent_migrations , skip_checks: [ :raw_sql_executed , :not_null_added ] Чтобы пропустить анализ миграций, которые были созданы перед добавлением этого пакета, установите временную метку из последней миграции в start_after в config:
config :excellent_migrations , start_after: "20191026080101" Каждый поощряется и приветствуется, чтобы помочь улучшить этот проект. Вот несколько способов помочь:
Copyright (C) 2021 Artur Sulej
Эта работа бесплатна. Вы можете перераспределить его и/или изменить его в соответствии с условиями лицензии MIT. Смотрите файл License.md для получения более подробной информации.