Desenvolvimento

Data patch já aplicado: como fazer rodar de novo em desenvolvimento

Você ajustou o patch, rodou setup:upgrade e nada aconteceu. Não é cache: o Magento acha que já aplicou aquilo e não vai tentar de novo.

Por Roger Takemiya · Publicado em · 5 min de leitura

Cena comum: você escreve um data patch, roda setup:upgrade, vê que faltou um campo, corrige a classe, roda de novo — e o Magento passa direto sem executar nada. Você limpa cache, apaga generated/, reinstala o módulo. Continua nada.

Não é bug e nem cache. O Magento guarda a lista de patches aplicados numa tabela e o seu já está lá.

Onde o Magento anota que já rodou

A classe Magento\Framework\Setup\Patch\PatchHistory declara a tabela logo no começo:

const TABLE_NAME = 'patch_list';
const CLASS_NAME = "patch_name";

A tabela é declarada em app/etc/db_schema.xml e tem duas colunas só: patch_id, auto increment, e patch_name, um varchar de 1024 com o nome completo da classe. Quando um patch termina de aplicar, o PatchApplier chama fixPatch(), que insere a linha. Se alguém tentar registrar duas vezes, o método reclama alto:

throw new \LogicException(sprintf("Patch %s cannot be applied twice", $patchName));

Ou seja: o critério não é versão de módulo, não é data, não é hash do arquivo. É o nome da classe, cru. Renomeou a classe? Vira patch novo e roda de novo. Mudou só o corpo do apply()? O Magento não olha para isso.

Para ver o que está registrado do seu módulo:

SELECT patch_id, patch_name
FROM patch_list
ORDER BY patch_id DESC
LIMIT 20;

Apagar a linha certa (e o detalhe da barra invertida)

Com o nome em mãos, o DELETE é direto. A pegadinha está na sintaxe do MySQL: dentro de uma string, a barra invertida é caractere de escape, então o nome da classe precisa vir com as barras dobradas.

DELETE FROM patch_list
WHERE patch_name = 'Acme\\Catalog\\Setup\\Patch\\Data\\AddSeoAttributes';

Se você copiar o nome com barra simples, o MySQL come as barras, a comparação não casa com nada e o DELETE devolve 0 rows affected. Aí você acha que apagou, roda o setup:upgrade e o patch continua não executando — dez minutos de confusão garantidos. Na dúvida, confirme antes:

SELECT patch_name FROM patch_list
WHERE patch_name LIKE '%AddSeoAttributes';

Um cuidado a mais: se a sua instalação usa prefixo de tabela (o valor fica em db/table_prefix no app/etc/env.php), a tabela se chama prefixo_patch_list. Com a linha fora, é só rodar:

php bin/magento setup:upgrade

Três coisas que dão errado nesse caminho

1. O patch tem aliases. O método getAliases() existe para o caso de você renomear a classe sem perder o histórico, e o PatchApplier grava uma linha para cada alias retornado. Se o seu patch declara aliases, apagar só a linha do nome principal não basta: o Magento continua achando o registro pelo apelido. Apague todas.

2. O patch não é idempotente. Rodar de novo não desfaz o que a primeira execução fez. Se o apply() cria um atributo, a segunda passada tenta criar de novo e pode estourar erro de duplicidade, ou pior, duplicar dado silenciosamente. Escrever patch que pode rodar duas vezes sem estragar nada não é frescura — é o que torna esse truque seguro.

3. Você misturou DDL com data patch. O PatchApplier envolve cada data patch numa transação, faz commit no fim e rollback se explodir. Só que CREATE TABLE e ALTER TABLE dão commit implícito no MySQL — a transação vira fumaça e o rollback não devolve nada. Se o seu patch precisa mesmo mexer em estrutura, ele deveria ser um schema patch, ou pelo menos implementar NonTransactionableInterface para não fingir uma proteção que não existe.

Quando alguma coisa quebra no meio, a mensagem é esta, e ela diz o módulo e o motivo original:

Unable to apply data patch %1 for module %2. Original exception message: %3

Em produção, esse não é o caminho

Apagar linha de patch_list num servidor de produção é assumir que você sabe exatamente o que a primeira execução deixou no banco — e quase nunca sabe. O caminho certo lá é outro: escreva um patch novo, com nome novo, que faz a correção a partir do estado atual. É mais código e é chato, mas fica registrado, sobe com o deploy e roda igual em todos os ambientes.

Se o seu patch precisa mesmo poder ser desfeito, implemente PatchRevertableInterface e escreva o revert() de verdade. Ele é chamado quando o módulo é desinstalado, e é a única forma suportada de voltar atrás sem SQL na unha.

E vale a regra que salva tempo em qualquer time: patch é código de migração, não é script de conveniência. Se você está apagando a mesma linha da patch_list pela terceira vez na semana, o que você quer é um comando de CLI próprio, não um patch. Como montar isso está no guia de desenvolvimento de módulos.

Perguntas rápidas

Renomear a classe do patch resolve também?

Resolve, porque o critério é o nome. Mas você fica com um patch a mais no módulo e um nome pior do que o original, então serve para um teste rápido e não como prática.

Existe um comando do bin/magento para desfazer um patch?

Não existe um comando que reverta um patch isolado. O revert() do PatchRevertableInterface é acionado na desinstalação do módulo, o que é bem mais amplo do que desfazer um patch específico.

Apagar a linha estraga alguma coisa no setup:db:status?

Não. O patch_list é uma lista independente do controle de versão de schema do módulo. O que muda é só que o Magento volta a considerar aquele patch como pendente e o executa no próximo setup:upgrade.

Pra conferir na fonte

  1. Patches (declarative schema) — Adobe Developer
  2. Command-line reference for Commerce on-premises — Adobe Experience League
  3. PatchHistory.php (2.4-develop) — GitHub
  4. PatchApplier.php (2.4-develop) — GitHub

data patch setup banco

Precisa de um orçamento? Ficarei feliz em ajudar. Clique Aqui