EC-CUBE4.3 Doctrine PHP データベース バージョンアップ

EC-CUBE 4.2以降(DBAL 3系)で bin/console eccube:schema:update --dump-sql を実行すると、dtb_order.subtotal などdecimal列の CHANGE ... NUMERIC(12, 2) DEFAULT '0' が毎回出ます。これはEntityとDBのずれではなく、DBAL 3がEntity側の既定値 '0' とMySQLが返す既定値 '0.00' を文字列として比較しているためで、--force で流しても消えません。消すにはCustomize配下に比較ロジックを1つ差し込みます。

出続ける差分の見分け方

該当する差分はこの形です。

ALTER TABLE dtb_order CHANGE subtotal subtotal NUMERIC(12, 2) UNSIGNED DEFAULT '0' NOT NULL, CHANGE discount discount NUMERIC(12, 2) UNSIGNED DEFAULT '0' NOT NULL, ...;
ALTER TABLE dtb_payment CHANGE charge charge NUMERIC(12, 2) UNSIGNED DEFAULT '0';
ALTER TABLE dtb_order_item CHANGE price price NUMERIC(12, 2) DEFAULT '0' NOT NULL;
ALTER TABLE dtb_product_class CHANGE price02 price02 NUMERIC(12, 2) DEFAULT '0';

条件は「decimalで、scaleが1以上で、Entityに options={"default":0} がある列」です。標準のEntityでは dtb_order の金額7列、dtb_cart 2列、dtb_cart_item.pricedtb_order_item.pricedtb_payment.chargedtb_customer.buy_totaldtb_product_class.price02 の14列が該当します。point のようにscaleが0の列は、MySQLの既定値も '0' のまま返るので出ません。

その1文をDBに流して再度 --dump-sql を見ても同じ差分が出ます。MySQLはdecimal(12,2)の既定値を必ず 0.00 の形で保持するので、DEFAULT '0' と書いてALTERしても 0.00 に正規化されるためです。

4.1まで出なかった理由

EC-CUBE 4.1(DBAL 2.13)の比較は属性ごとで、既定値はゆるい比較でした。

// DBAL 2.13 Schema/Comparator.php diffColumn()
|| $properties1['default'] != $properties2['default']

PHPの != は数値文字列同士を数値として比べるので '0' != '0.00' はfalseになり、差分になりません。EC-CUBE 4.2でSymfony 5.4とDBAL 3系に上がってからは、列定義をSQL文字列にしてから完全一致で比べる方式に変わりました。

// DBAL 3.8 Platforms/AbstractPlatform.php columnsEqual()
if (
    $this->getColumnDeclarationSQL('', $column1Array)
    !== $this->getColumnDeclarationSQL('', $column2Array)
) {
    return false;
}

Entity側は NUMERIC(12, 2) DEFAULT '0'、DBから読んだ側は NUMERIC(12, 2) DEFAULT '0.00' になるので一致しません。.envDATABASE_SERVER_VERSION を疑いたくなりますが、5.7 / 8.0 / mariadb-10.6 / 未指定のどれにしても結果は同じでした。プラットフォームの選択ではなく比較方式の問題です。

Customizeに比較ロジックを差し込む

coreのEntityを書き換えずに済ませるため、DoctrineBundleの schema_manager_factory(2.11で確認)を使ってComparatorだけ差し替えます。ファイルは3つです。

// app/Customize/Doctrine/DBAL/Schema/DecimalDefaultAwareComparator.php
namespace Customize\Doctrine\DBAL\Schema;

use Doctrine\DBAL\Platforms\MySQL\Comparator as MySQLComparator;
use Doctrine\DBAL\Schema\Column;
use Doctrine\DBAL\Types\DecimalType;
use Doctrine\DBAL\Types\FloatType;

class DecimalDefaultAwareComparator extends MySQLComparator
{
    public function columnsEqual(Column $column1, Column $column2): bool
    {
        if ($this->hasSameNumericDefault($column1, $column2)) {
            // 既定値の表記だけが違う(0 と 0.00 など)ので、片方に揃えてから比較する
            $column2 = clone $column2;
            $column2->setDefault($column1->getDefault());
        }

        return parent::columnsEqual($column1, $column2);
    }

    private function hasSameNumericDefault(Column $column1, Column $column2): bool
    {
        foreach ([$column1, $column2] as $column) {
            $type = $column->getType();
            if (!($type instanceof DecimalType) && !($type instanceof FloatType)) {
                return false;
            }
        }
        $default1 = $column1->getDefault();
        $default2 = $column2->getDefault();
        if ($default1 === null || $default2 === null) {
            return false;
        }
        if (!is_numeric($default1) || !is_numeric($default2)) {
            return false;
        }

        return (float) $default1 === (float) $default2;
    }
}
// app/Customize/Doctrine/DBAL/Schema/MySQLSchemaManager.php
namespace Customize\Doctrine\DBAL\Schema;

use Doctrine\DBAL\Platforms\MySQL\CollationMetadataProvider\CachingCollationMetadataProvider;
use Doctrine\DBAL\Platforms\MySQL\CollationMetadataProvider\ConnectionCollationMetadataProvider;
use Doctrine\DBAL\Schema\Comparator;
use Doctrine\DBAL\Schema\MySQLSchemaManager as BaseMySQLSchemaManager;

class MySQLSchemaManager extends BaseMySQLSchemaManager
{
    public function createComparator(): Comparator
    {
        return new DecimalDefaultAwareComparator(
            $this->_platform,
            new CachingCollationMetadataProvider(
                new ConnectionCollationMetadataProvider($this->_conn)
            )
        );
    }
}
// app/Customize/Doctrine/DBAL/Schema/SchemaManagerFactory.php
namespace Customize\Doctrine\DBAL\Schema;

use Doctrine\DBAL\Connection;
use Doctrine\DBAL\Platforms\AbstractMySQLPlatform;
use Doctrine\DBAL\Schema\AbstractSchemaManager;
use Doctrine\DBAL\Schema\SchemaManagerFactory as SchemaManagerFactoryInterface;

class SchemaManagerFactory implements SchemaManagerFactoryInterface
{
    public function createSchemaManager(Connection $connection): AbstractSchemaManager
    {
        $platform = $connection->getDatabasePlatform();
        if ($platform instanceof AbstractMySQLPlatform) {
            return new MySQLSchemaManager($connection, $platform);
        }

        return $platform->createSchemaManager($connection);
    }
}

app/Customize 配下は services.yaml で自動的にサービス登録されるので、doctrine.yaml にクラス名を1行足すだけで有効になります。

# app/config/eccube/packages/doctrine.yaml
doctrine:
    dbal:
        schema_manager_factory: Customize\Doctrine\DBAL\Schema\SchemaManagerFactory

この差し替えが影響するのはスキーマ比較だけです。実行時のSQLやEntityのマッピングは変わりません。MySQL以外のプラットフォームは標準のSchemaManagerをそのまま返します。

本当の差分が消えていないことを確認する

cache:clear のあと --dump-sql が0文になれば成功ですが、それだけでは「何も検出しなくなった」可能性を排除できません。一時的にEntityを変えて差分が出ることを確かめてから戻します。text列の length を255に変えると CHANGE ... TINYTEXT が、Payment.chargedefault を5に変えると DEFAULT '5' が出て、戻すと0文に戻る、という結果なら正常です。

coreのEntity 14列を "default":"0.00" に書き換える方法でも消えますが、coreを触ることになるうえ、今後decimal列が増えるたびに同じ手当てが必要になります。Customize側の1回で済ませる方が保守は楽です。

日本発!ECオープンプラットフォーム「EC-CUBE」 EC-CUBEゴールドパートナー EC-CUBEは株式会社イーシーキューブの商標です

EC-CUBEに関するお問い合わせ


    [重要]現在公式にセキュリティサポートが切れていないPHPは8.1以上、MySQLは8.0以上で、対応しているEC-CUBEバージョンは4.2以上です。古いEC-CUBEを使っている方は適切なタイミングでバージョンアップをご検討ください。

    EC-CUBEバージョンアップ