← Voltar ao blog

Declarative schema e data patches no Magento 2, com segurança

Rodrigo Biazzi

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 NULL sem 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.php ou ao config.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