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.price、dtb_order_item.price、dtb_payment.charge、dtb_customer.buy_total、dtb_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' になるので一致しません。.env の DATABASE_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.charge の default を5に変えると DEFAULT '5' が出て、戻すと0文に戻る、という結果なら正常です。
coreのEntity 14列を "default":"0.00" に書き換える方法でも消えますが、coreを触ることになるうえ、今後decimal列が増えるたびに同じ手当てが必要になります。Customize側の1回で済ませる方が保守は楽です。
EC-CUBEに関するお問い合わせ
[重要]現在公式にセキュリティサポートが切れていないPHPは8.1以上、MySQLは8.0以上で、対応しているEC-CUBEバージョンは4.2以上です。古いEC-CUBEを使っている方は適切なタイミングでバージョンアップをご検討ください。