Declarative schema e data patches no Magento 2, com segurança
Desde o Magento 2.3, as mudanças de banco nos módulos são feitas com declarative schema e patches, no lugar dos antigos scripts InstallSchema e UpgradeData com checagem de versão. O sistema novo é melhor, mas tem pontas afiadas. Ele vai apagar uma coluna se você removê-la do XML e a whitelist permitir. Ele vai rodar um data patch exatamente uma vez, então um patch quebrado que rodou pela metade é problema seu para limpar.
Este artigo cobre como as peças funcionam no Magento 2.4 e os hábitos que impedem uma mudança de schema de virar uma queda.
Como o declarative schema funciona
Cada módulo descreve o estado final das suas tabelas em etc/db_schema.xml. Durante o setup:upgrade, o Magento lê o arquivo de todos os módulos, compara o resultado com o banco real e gera o SQL necessário para chegar lá. Você nunca escreve ALTER TABLE na mão.
<?xml version="1.0"?>
<schema xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:Setup/Declaration/Schema/etc/schema.xsd">
<table name="vendor_order_export" resource="default" engine="innodb"
comment="Order export status">
<column xsi:type="int" name="entity_id" unsigned="true" nullable="false"
identity="true" comment="Entity ID"/>
<column xsi:type="int" name="order_id" unsigned="true" nullable="false"
comment="Sales order ID"/>
<column xsi:type="varchar" name="status" length="32" nullable="false"
default="pending" comment="Export status"/>
<column xsi:type="timestamp" name="exported_at" nullable="true"
comment="Exported at"/>
<constraint xsi:type="primary" referenceId="PRIMARY">
<column name="entity_id"/>
</constraint>
<constraint xsi:type="unique" referenceId="VENDOR_ORDER_EXPORT_ORDER_ID">
<column name="order_id"/>
</constraint>
<constraint xsi:type="foreign" referenceId="VENDOR_ORDER_EXPORT_ORDER_ID_SALES_ORDER_ENTITY_ID"
table="vendor_order_export" column="order_id"
referenceTable="sales_order" referenceColumn="entity_id"
onDelete="CASCADE"/>
<index referenceId="VENDOR_ORDER_EXPORT_STATUS" indexType="btree">
<column name="status"/>
</index>
</table>
</schema>
Um módulo também pode estender uma tabela do core declarando o mesmo nome de tabela apenas com as colunas novas. O Magento faz o merge das declarações.
O arquivo de whitelist
Como o XML descreve um estado final, remover uma coluna dele pode significar "apague essa coluna" ou "essa coluna nunca foi minha". O Magento resolve a ambiguidade com o etc/db_schema_whitelist.json. Ele lista toda tabela, coluna, índice e constraint que o módulo já declarou. O Magento só apaga ou renomeia o que aparece na whitelist.
Gere o arquivo depois de toda mudança no db_schema.xml e faça commit junto com a mudança:
bin/magento setup:db-declaration:generate-whitelist --module-name=Vendor_OrderExport
Dois erros comuns vêm desse arquivo. Esquecer de regenerá-lo faz uma coluna que você removeu do XML continuar no banco. Regenerá-lo sem cuidado em uma branch em que a coluna de outra pessoa está faltando no XML pode autorizar um drop que você não pretendia. Revise o diff da whitelist como qualquer outro código.
Removendo e renomeando
Para remover uma coluna, apague-a do XML ou marque com disabled="true". Com a whitelist no lugar, o próximo setup:upgrade apaga a coluna, junto com os dados.
Para renomear uma coluna e manter os dados, declare a coluna nova com onCreate:
<column xsi:type="varchar" name="export_status" length="32" nullable="false"
default="pending" onCreate="migrateDataFrom(status)" comment="Export status"/>
Para renomear uma tabela existe onCreate="migrateDataFromAnotherTable(old_table_name)" no nó da tabela. Os dois só funcionam quando o elemento antigo está na whitelist.
Dry run e safe mode
Antes de rodar uma mudança de schema em um banco real, olhe o SQL que o Magento pretende executar. O modo dry run grava isso em um arquivo de log e não altera nada:
bin/magento setup:upgrade --dry-run=1
less var/log/dry-run-installation.log
Leia esse arquivo em todo release que toca o schema. Qualquer coisa com DROP merece uma segunda olhada. É também a forma mais rápida de achar uma mudança inesperada causada por outro módulo ou por um erro na whitelist.
Existe também um safe mode. O setup:upgrade --safe-mode=1 despeja em var/declarative_dumps_csv os dados das colunas e tabelas que estão prestes a ser removidas, e o setup:upgrade --data-restore=1 pode carregá-los de volta depois de um rollback do código. É uma rede de segurança, não um backup. Faça um backup de verdade do banco antes de qualquer release com mudanças destrutivas.
Data patches
O schema descreve estrutura. Os data patches mudam conteúdo: valores de configuração, CMS blocks, atributos, registros default. Um data patch implementa Magento\Framework\Setup\Patch\DataPatchInterface e fica em Setup/Patch/Data. O Magento registra os patches aplicados pelo nome da classe na tabela patch_list e nunca roda a mesma classe duas vezes.
<?php
declare(strict_types=1);
namespace Vendor\OrderExport\Setup\Patch\Data;
use Magento\Framework\App\Config\Storage\WriterInterface;
use Magento\Framework\Setup\ModuleDataSetupInterface;
use Magento\Framework\Setup\Patch\DataPatchInterface;
use Magento\Framework\Setup\Patch\PatchRevertableInterface;
class AddDefaultExportConfig implements DataPatchInterface, PatchRevertableInterface
{
private const PATH = 'vendor_order_export/general/batch_size';
public function __construct(
private readonly ModuleDataSetupInterface $moduleDataSetup,
private readonly WriterInterface $configWriter
) {
}
public function apply(): self
{
$this->moduleDataSetup->getConnection()->startSetup();
$this->configWriter->save(self::PATH, '100');
$this->moduleDataSetup->getConnection()->endSetup();
return $this;
}
public function revert(): void
{
$this->moduleDataSetup->getConnection()->startSetup();
$this->configWriter->delete(self::PATH);
$this->moduleDataSetup->getConnection()->endSetup();
}
public static function getDependencies(): array
{
return [];
}
public function getAliases(): array
{
return [];
}
}
Dependências e aliases
getDependencies() devolve outras classes de patch que precisam rodar antes. Use sempre que um patch lê dados criados por outro, inclusive patches de outros módulos. Sem isso, a ordem não é algo em que se deva confiar.
getAliases() devolve nomes antigos de classe para o mesmo patch. Se você renomear ou mover uma classe de patch que já rodou em produção, coloque o nome antigo aqui, senão o Magento a trata como um patch novo e roda de novo.
Patches revertable
Implementar PatchRevertableInterface adiciona um método revert(). Ele roda quando o módulo é desinstalado com bin/magento module:uninstall. Não roda quando você faz deploy de uma versão mais antiga do código nem quando desativa o módulo, então não é um mecanismo de rollback de releases. Escreva-o para módulos que podem ser removidos e seja honesto: se o patch não pode ser revertido de forma limpa, não finja que pode.
Schema patches
O SchemaPatchInterface, em Setup/Patch/Schema, tem o mesmo formato de um data patch. Ele roda antes dos data patches e serve para mudanças estruturais que o XML não consegue expressar. Na prática, a maioria dos módulos nunca precisa de um. Se você se pegar escrevendo ALTER TABLE em um schema patch para algo que o db_schema.xml suporta, mova para o XML, senão a comparação declarativa e o seu patch vão brigar.
Hábitos que evitam quedas
Faça mudanças retrocompatíveis
Durante um deploy existe uma janela em que o banco já tem o schema novo e o código antigo ainda está rodando, ou o contrário se você precisar fazer rollback. Planeje as mudanças para que as duas versões funcionem:
- Adicione colunas novas como nullable ou com default. O código que não conhece essas colunas continua funcionando.
- Não apague nem renomeie uma coluna no mesmo release que para de usá-la. Entregue a mudança de código primeiro e remova a coluna em um release posterior.
- Evite adicionar
NOT NULLsem default em uma tabela que já tem linhas.
Cuidado com tabelas grandes
Um ALTER TABLE em sales_order, quote ou uma tabela EAV grande pode demorar bastante e travar escritas, dependendo da operação e da versão do MySQL ou MariaDB. Teste o upgrade em uma cópia do banco de produção com tamanho realista e meça o tempo. Se for lento, agende com maintenance mode ou divida a mudança.
Mantenha os data patches pequenos e defensivos
- Um propósito por patch. Fica mais fácil de nomear, revisar e usar como dependência.
- Confira antes de escrever. Mesmo que um patch rode uma vez, um patch que verifica se o registro já existe sobrevive a uma nova tentativa manual depois de uma falha.
- Não carregue coleções completas de produtos ou pedidos. Use a connection e processe em lotes.
- Injete as dependências pelo construtor. Não use o object manager diretamente.
- Nunca coloque em patches valores específicos de ambiente, como chaves de API ou URLs. Isso pertence ao
env.phpou aoconfig.php.
Saiba o que está pendente
O bin/magento setup:db:status diz se o schema ou os dados precisam de upgrade. Use no script de deploy para decidir se o maintenance mode é necessário. Se precisar rodar só uma parte, existem os comandos separados setup:db-schema:upgrade e setup:db-data:upgrade.
FAQ
Para que serve o db_schema_whitelist.json?
Ele diz ao Magento quais tabelas, colunas e índices um módulo declarou no passado, para que saiba que pode apagá-los ou renomeá-los. Gere com setup:db-declaration:generate-whitelist depois de toda mudança no db_schema.xml.
Como rodar um data patch de novo no Magento 2?
O Magento ignora qualquer patch listado na tabela patch_list. Apagar a linha dele faz o patch rodar no próximo setup:upgrade. Faça isso só quando o patch for seguro para rodar duas vezes, e prefira escrever um patch novo para mudanças novas.
O revert() roda quando eu faço rollback de um deploy?
Não. O método revert de um patch revertable roda quando o módulo é desinstalado com bin/magento module:uninstall. Fazer deploy de um código mais antigo não desfaz patches nem mudanças de schema.
Como vejo qual SQL o setup:upgrade vai rodar?
Rode bin/magento setup:upgrade --dry-run=1. Os comandos planejados são gravados em var/log/dry-run-installation.log e o banco não é alterado.
Sou Rodrigo Biazzi, desenvolvedor Magento certificado, trabalho com Magento e Adobe Commerce há mais de 12 anos. Atendo como freelancer em Magento, de correções pontuais a projetos mais longos. Se esse é o tipo de problema que você tem na sua loja, fale comigo.
Artigos relacionados
Order status customizado no Magento 2: states, statuses e workflow
Crie order statuses customizados no Magento 2, ligue-os a states com um data patch, altere-os pelo código com segurança e evite que o Magento os redefina ao salvar.
Shipping method customizado no Magento 2: crie um módulo de carrier
Como criar um shipping method customizado no Magento 2.4: config.xml, system.xml, um carrier model com collectRates, regras de frete grátis e APIs externas de tarifa.
Checklist de limpeza de malware no Magento 2 para lojistas
Checklist defensivo para uma loja Magento 2 hackeada: sinais de comprometimento, achar código injetado em arquivos e banco, rebuild limpo, novas credenciais e hardening.
Backup e restore no Magento 2: o que salvar e como
Como fazer backup e restore de uma loja Magento 2: dumps de banco que importam sem erro, mídia, env.php e a chave de criptografia, cópias offsite e restores testados.