db_schema.xml in Magento 2 | 5 Common Mistakes to Avoid
AI generated
Magento 2 · Declarative Schema

db_schema.xml
5 mistakes developers should avoid

Declarative Schema replaces old InstallScripts, but only when tables, constraints, indexes, and patches are kept cleanly separated. These five mistakes cause most of the problems in Magento projects.

11 min read Magento 2.4.8 Declarative Schema

1. Why db_schema.xml matters

db_schema.xml Magento 2 is the central mechanism for declarative database schema. Instead of manually creating tables through old install or upgrade scripts, you describe the desired target state of the database in XML. Magento compares this target state against the current database and calculates the necessary changes from it. That makes schema changes more traceable, reproducible, and easier to maintain.

The benefit is significant, but only if the file is used cleanly. db_schema.xml describes structure: tables, columns, indexes, and constraints. It is not intended for data migration, default records, complex transformations, or business logic. Exactly at this boundary, many mistakes happen. Developers put too much into the schema, forget patches, or change existing tables without a clear upgrade strategy.

Anyone working with Magento 2.4.8 should use declarative schema as the default. New modules no longer need InstallScripts. If a module needs its own tables, the definition belongs in app/code/Vendor/Module/etc/db_schema.xml. Data changes belong in Data Patches, complex structural migrations in Schema Patches, or deliberately planned upgrade steps.

In teams, it's also important that schema changes are reviewed like regular application code. A new column looks harmless but can affect index size, import speed, API responses, and deployment time. A new foreign key relationship can improve data quality but also block deletion processes. A table that becomes too wide can later slow down indexers and exports. That's why every change to db_schema.xml Magento 2 should start with the question of how much data exists today and how much data is realistic in twelve months.


<?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="mironsoft_example" resource="default" engine="innodb" comment="Mironsoft Example">
        <column xsi:type="int" name="entity_id" unsigned="true" nullable="false" identity="true"
                comment="Entity ID"/>
        <column xsi:type="varchar" name="title" nullable="false" length="255" comment="Title"/>
        <column xsi:type="timestamp" name="created_at" nullable="false" default="CURRENT_TIMESTAMP"
                comment="Created At"/>
        <constraint xsi:type="primary" referenceId="PRIMARY">
            <column name="entity_id"/>
        </constraint>
    </table>
</schema>

2. Mistake 1: Still using InstallScripts

The first mistake is the most common one: developers create a new module and still write InstallSchema or UpgradeSchema. In modern Magento projects, that's no longer the right approach. If you need a new table, it belongs in db_schema.xml Magento 2. Old setup scripts make upgrades harder to predict and lead more quickly to divergent database states between development, staging, and production.

This doesn't mean legacy modules need to be rebuilt immediately. Legacy code has to be migrated in a controlled way. For new modules, though, the rule is clear: no InstallScript for table structure. Declarative schema is the standard. It describes the target state, and Magento takes care of the difference.

A practical side effect: new developers understand a module faster when the table structure sits in a fixed location. They don't have to read and mentally execute several historical upgrade scripts to reconstruct the current state. Especially in Magento shops maintained over the long term, this saves a lot of time. The schema becomes documentation of the current data model.


<?php
declare(strict_types=1);

namespace Mironsoft\Example\Setup;

use Magento\Framework\Setup\InstallSchemaInterface;

/**
 * Deprecated approach for new Magento modules.
 */
class InstallSchema implements InstallSchemaInterface
{
    // Do not create new tables this way in new Magento 2 modules.
}

The correct approach is a clean db_schema.xml. This way, every developer immediately sees which tables and columns the module expects. In addition, Magento can systematically detect schema differences.

3. Mistake 2: Forcing data changes into db_schema.xml

The second mistake is more subtle: db_schema.xml is used for things that aren't structure. One example is inserting configuration values, initial records, or complex transformations. That's not what db_schema.xml Magento 2 is for. It describes tables and columns, not business data.

If your module needs default configuration, you use config.xml. If it needs initial records or migrations, you use Data Patches. If a one-time structural change can't be represented cleanly by declarative means, a Schema Patch may be appropriate. This separation matters because schema and data have different lifecycles.


<?php
declare(strict_types=1);

namespace Mironsoft\Example\Setup\Patch\Data;

use Magento\Framework\Setup\ModuleDataSetupInterface;
use Magento\Framework\Setup\Patch\DataPatchInterface;

/**
 * Inserts initial example data for the module.
 */
final class AddInitialExampleData implements DataPatchInterface
{
    public function __construct(
        private readonly ModuleDataSetupInterface $moduleDataSetup
    ) {}

    public function apply(): self
    {
        $connection = $this->moduleDataSetup->getConnection();
        $connection->insertOnDuplicate(
            $this->moduleDataSetup->getTable('mironsoft_example'),
            [
                'entity_id' => 1,
                'title' => 'Initial Example'
            ],
            ['title']
        );

        return $this;
    }

    public static function getDependencies(): array
    {
        return [];
    }

    public function getAliases(): array
    {
        return [];
    }
}

4. Mistake 3: Defining constraints and indexes sloppily

The third mistake concerns constraints and indexes. Many tables work fine at first even without clean foreign keys or indexes. Performance problems or data inconsistencies show up later. A well-built db_schema.xml Magento 2 defines not just columns, but also primary keys, unique constraints, foreign keys, and sensible indexes.

Clear referenceId values are especially important. They should be stable, readable, and unique. If you keep changing them, Magento may detect unnecessary drop-and-create operations. For foreign keys, the delete behavior must also be chosen deliberately. CASCADE is not automatically correct. Sometimes SET NULL or deliberately preventing deletion makes more sense from a business perspective.


<?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="mironsoft_example_item" resource="default" engine="innodb" comment="Example Item">
        <column xsi:type="int" name="item_id" unsigned="true" nullable="false" identity="true"
                comment="Item ID"/>
        <column xsi:type="int" name="example_id" unsigned="true" nullable="false" comment="Example ID"/>
        <column xsi:type="varchar" name="sku" nullable="false" length="64" comment="SKU"/>
        <constraint xsi:type="primary" referenceId="PRIMARY">
            <column name="item_id"/>
        </constraint>
        <constraint xsi:type="foreign" referenceId="MIRONSOFT_EXAMPLE_ITEM_EXAMPLE_ID"
                    table="mironsoft_example_item" column="example_id"
                    referenceTable="mironsoft_example" referenceColumn="entity_id"
                    onDelete="CASCADE"/>
        <constraint xsi:type="unique" referenceId="MIRONSOFT_EXAMPLE_ITEM_EXAMPLE_ID_SKU">
            <column name="example_id"/>
            <column name="sku"/>
        </constraint>
        <index referenceId="MIRONSOFT_EXAMPLE_ITEM_SKU" indexType="btree">
            <column name="sku"/>
        </index>
    </table>
</schema>

5. Mistake 4: Ignoring the whitelist

The fourth mistake is ignoring db_schema_whitelist.json. With declarative schema, Magento needs to know which tables, columns, and constraints a module is allowed to manage. The whitelist protects against accidentally dropping database structures. If it's missing or outdated, schema changes can't be cleanly applied in certain situations.

The whitelist matters especially for removed columns or constraints. Developers often only test adding new columns locally. Removing or renaming comes later and only surfaces in staging or production. That's why the whitelist shouldn't be treated as a side effect in a team, but as part of the schema change itself.

In pull requests, the whitelist should therefore be deliberately reviewed. If db_schema.xml was changed but the whitelist stays unchanged, that's not automatically wrong, but it's a checkpoint. Especially for modules installed across multiple shops, an outdated whitelist can later lead to hard-to-explain upgrade differences.


# Mark-Shust wrapper from the project root:
bin/magento setup:db-declaration:generate-whitelist --module-name=Mironsoft_Example

The generated file lives in the module under etc/db_schema_whitelist.json. It should be versioned if the module manages declarative schema. For every relevant schema change, check whether the whitelist needs to be updated.

6. Mistake 5: Changes without an upgrade strategy

The fifth mistake is missing planning. Creating a new table is easy. It gets harder when columns are renamed, types are changed, data is migrated, or constraints are tightened. A change in db_schema.xml Magento 2 can work without problems on an empty development database and still be risky in production.

Example: a nullable column suddenly becomes nullable="false". Locally that works because barely any data exists. In production, though, the column contains null values. The upgrade can fail. That's why every relevant schema change needs a small upgrade strategy: what data exists? Does a Data Patch need to fill values beforehand? Is the change backward compatible? How large is the table?


<?php
declare(strict_types=1);

namespace Mironsoft\Example\Setup\Patch\Data;

use Magento\Framework\Setup\ModuleDataSetupInterface;
use Magento\Framework\Setup\Patch\DataPatchInterface;

/**
 * Backfills missing titles before the column becomes required.
 */
final class BackfillMissingTitles implements DataPatchInterface
{
    public function __construct(
        private readonly ModuleDataSetupInterface $moduleDataSetup
    ) {}

    public function apply(): self
    {
        $connection = $this->moduleDataSetup->getConnection();
        $table = $this->moduleDataSetup->getTable('mironsoft_example');

        $connection->update(
            $table,
            ['title' => 'Untitled'],
            'title IS NULL OR title = ""'
        );

        return $this;
    }

    public static function getDependencies(): array
    {
        return [];
    }

    public function getAliases(): array
    {
        return [];
    }
}

7. Comparison: db_schema.xml vs. patch

Clean handling of db_schema.xml Magento 2 starts with the right responsibility. Not every database change belongs in the same file. Structure belongs in declarative schema. Data belongs in Data Patches. One-time, complex structural workflows may require Schema Patches. Whoever respects this boundary avoids many upgrade problems.

Task Right place Comment
New table db_schema.xml Declarative target state of the database
New column db_schema.xml Check existing data first for required columns
Initial records Data Patch Don't hide it inside schema files
Data migration Data Patch Plan it idempotently and deliberately
Complex structural migration Schema Patch Only when declarative schema alone isn't enough

A good checkpoint before every merge is the question: "Does this change describe structure or data?" If it's structure, it usually belongs in db_schema.xml. If it's data, it belongs in a patch. If it's both, order and dependency must be defined deliberately.

Mironsoft

Magento 2 database, modules, and upgrade strategies

Planning schema changes without upgrade risk?

We build Magento 2 modules with clean declarative schema, Data Patches, a clear upgrade order, and verifiable database changes for staging and production.

Schema review

Checking db_schema.xml, constraints, indexes, and the whitelist

Patches

Cleanly separating Data Patches and Schema Patches

Deployment

Evaluating upgrade order and data volume up front

9. Summary

db_schema.xml Magento 2 is the right place for declarative table structure. The most common mistakes happen when developers keep using old InstallScripts, hide data changes inside the schema, neglect constraints, ignore the whitelist, or deploy changes without an upgrade strategy.

The robust rule is simple: structure in db_schema.xml, data in Data Patches, plan complex edge cases deliberately. Every schema change should be tested locally, on staging, and with a realistic amount of data. That's how Magento upgrades stay predictable.

For professional Magento modules, this isn't extra effort, it's risk reduction. Clean database changes prevent deployment failures, inconsistencies, and expensive rework. The larger the shop and the more integrations accessing the same tables, the more important this discipline becomes.

db_schema.xml Magento 2: the essentials at a glance

Structure

Tables, columns, constraints, and indexes belong in db_schema.xml.

Data

Initial records and migrations belong in Data Patches, not in schema files.

Whitelist

Check and version db_schema_whitelist.json for relevant schema changes.

Upgrade

Always evaluate required columns, type changes, and drops against real data.

10. FAQ: db_schema.xml in Magento 2

1 What is db_schema.xml in Magento 2?
It describes the declarative database schema of a module: tables, columns, constraints, and indexes.
2 Should you still use InstallSchema?
Not for new modules. Table structure belongs in db_schema.xml, not in old InstallScripts.
3 Do data changes belong in db_schema.xml?
No. Data changes belong in Data Patches. db_schema.xml describes structure.
4 What is db_schema_whitelist.json for?
It defines managed schema elements and protects against accidental drops. Check and version it for relevant changes.
5 How do you generate the whitelist?
With the Magento wrapper: bin/magento setup:db-declaration:generate-whitelist --module-name=Vendor_Module.
6 When do you need a Data Patch?
When data needs to be inserted, migrated, or backfilled, for example before a new required column.
7 When do you need a Schema Patch?
For complex structural migrations that can't be cleanly represented through declarative schema alone.
8 What's dangerous about nullable=false?
Existing null values can block the upgrade. Set clean values beforehand via a Data Patch.
9 Should foreign keys always delete with CASCADE?
No. Only use CASCADE when child data should automatically be deleted for business reasons.
10 How do you test schema changes?
On an empty database and with a realistic amount of data. Also check the whitelist, constraints, indexes, and possible data migrations.