Concrete CMS エクスプレスデータベースのデータ一括削除

2025/05/01
9,000件のConcrete CMSエクスプレスデータを削除しようと「エントリーをクリア」を実行したところ、タイムアウトエラーが発生。
手作業で一つずつ削除するのは現実的ではないため、一括削除プログラム を作成しました。

Concrete CMSのエクスプレスは大量データの削除が苦手

9,000件を超えるイラストを紹介するエクスプレスデータベースを、一括登録で構築しました。
しかし、一覧表を作成する際に問題が発生しました。

PIXTAのイラスト番号をテキスト属性で登録していましたが、昨年末、PXTAの資材登録数が1億を超え、私のイラスト番号も桁上がりしました。
この影響で、イラスト番号が単なるテキスト属性では正しくソートされないという問題が発生したのです。

そこで、エクスプレスの操作画面にある「エントリーをクリア」を実行しました。
しかし、何度試しても3分でタイムアウトが発生。

仕方なくエンティティ自体の削除も試みましたが、こちらもタイムアウト。
どうやら、9,000件のデータを一度に削除することは通常はできそうもありません。

そこで、データを分割して削除するプログラムの作成を検討することにしました。

以下健忘録的にその開発記録と最終成果物を記載します。

Concrete CMS Expressエンティティ「illust」:古い順に100件削除する処理の実装と試行記録

概要

Concrete CMS の Express エンティティ illust に対して、古い順に100件のデータを削除する処理を開発した際の試行錯誤と、最終的な実装方法を記録します。

背景

  • 登録数が増加した Express エンティティ illust のメンテナンスを目的に、古い順に100件を自動削除するバッチ処理(ジョブ)を作成。

  • 当初は EntryList$entry->delete() を用いて削除を試みたが、意図通りに動作しなかった。

試行と結果

1. EntryList$entry->delete() による削除

php

$entryList = new EntryList($entity);
$entryList->sortByDateAdded('asc');
$entryList->getQueryObject()->setMaxResults(100);
$entries = $entryList->getResults();

foreach ($entries as $entry) {
    $entry->delete();
}

結果

  • 実行エラーは出ないが、削除が反映されない(件数が変わらない)。

  • $entry->delete() は Express エンティティに対しては永続化が不十分な可能性。

2. Express\EntryManager を試みる

php

$manager = \Core::make('express/entry_manager');

結果

  • Class express/entry_manager does not exist エラー。

3. Doctrine ORM を用いた削除(成功)

Doctrine の EntityManager を利用し、エントリを明示的に remove()flush() することで、削除が正しく反映されるようになった。

php

use Doctrine\ORM\EntityManagerInterface;
use Concrete\Core\Entity\Express\Entry;

$em = \Core::make(EntityManagerInterface::class);
foreach ($entries as $entry) {
    $found = $em->find(Entry::class, $entry->getID());
    if ($found) {
        $em->remove($found);
        $deleted++;
    }
}
$em->flush();

結果

  • 正常に削除され、DB上の件数も一致。

  • 実運用に耐える安定した動作。

教訓・ベストプラクティス

  • Express エンティティの削除には、$entry->delete() よりも Doctrine ORM を使う方が信頼性が高い。

  • Concrete CMS の一部機能は Laravel のように内部的に Doctrine ORM を使っており、Express もその一例。

  • 複数件削除する場合は EntityManager::remove()flush() の組み合わせが推奨される。

今後の応用

  • 条件付き削除(属性値による絞り込み)も同様の手法で可能。

  • 重複データ削除などのロジックにも拡張可能。


Jobへの登録

大量のエントリー削除は頻繁には発生しないと思われますが、必要な際にすぐ実行できるよう、この機能をJobに登録しました。

ただし、エクスプレスデータベースでリレーションが設定されている場合は注意が必要です。
この点については、本稿の最後で簡単に説明しております。

実行プログラム cleanup_old_entries.php の設定について

プログラム内のエンティティハンドル(現在 'list')は、使用するエンティティハンドルに適宜変更してください

また、開発当初は100件単位で削除 を行っていましたが、1000件でも問題なく削除可能 であることを確認しました。
そのため、公開版では削除件数を1000件に設定 しています(setMaxResults(1000))。

実行プログラム cleanup_old_entries.php

<?php
namespace Concrete\Package\CleanupOldEntries\Job;

use Concrete\Core\Job\Job;
use Concrete\Core\Express\EntryList;
use Doctrine\ORM\EntityManagerInterface;

class CleanupOldEntries extends Job
{
    public function getJobName()
    {
        return t("Cleanup Old Express Entries");
    }

    public function getJobDescription()
    {
        return t("Deletes the oldest 100 Express entries using Doctrine.");
    }

    public function run()
    {
        error_log("ジョブ開始");

        $entityHandle = 'illust';
        $express = \Core::make('express');
        $entity = $express->getObjectByHandle($entityHandle);

        if (!$entity) {
            return t("Error: Express entity '{$entityHandle}' not found.");
        }

        $entryList = new EntryList($entity);
        $entryList->sortByDateAdded('asc');
        $entryList->getQueryObject()->setMaxResults(1000);
        $entries = $entryList->getResults();

        $count = count($entries);
        error_log("取得件数: {$count}");

        if ($count === 0) {
            return t("No entries found.");
        }

        $em = \Core::make(EntityManagerInterface::class);

        foreach ($entries as $entry) {
            $entryEntity = $em->find('Concrete\Core\Entity\Express\Entry', $entry->getID());
            if ($entryEntity) {
                $em->remove($entryEntity);
            }
        }

        $em->flush();

        error_log("削除完了: {$count} 件");
        return "{$count} entries deleted.";
    }
}
controller.php

<?php
namespace Concrete\Package\CleanupOldEntries;

use Concrete\Core\Package\Package;
use Concrete\Core\Job\Job;

class Controller extends Package
{
    protected $pkgHandle = 'cleanup_old_entries';
    protected $appVersionRequired = '8.5.0';
    protected $pkgVersion = '1.0.0';

    public function getPackageName()
    {
        return t('Cleanup Old Entries');
    }

    public function getPackageDescription()
    {
        return t('エクスプレスの廃止データを削除する');
    }

    public function install()
    {
        $pkg = parent::install();
        Job::installByPackage('cleanup_old_entries', $pkg);
    }
}

ダウンロードとインストール手順

以下のリンクからファイルをダウンロードし、解凍後に packages ディレクトリ内へ配置してください。

その後、システム設定 → Concreteの拡張 → Cleanup Old Entries のインストール を実行すると、Jobに登録され、いつでも使用可能 になります。

動作確認はConcrete CMS 8.5.19のみで行っており、他のバージョンでの動作は未確認です。

なお、本プログラムの使用により不具合が発生した場合でも、自己責任となることをご了承ください。

ダウンロードzip

しかし、一度に1000件もの削除が可能なこの機能は、誤操作のリスクが高く、危険ですね。うっかり押してしまったら、取り返しがつかないかもしれません。

通常は『機能を拡張する』からアンインストールしておき、必要な時だけインストールするのが安全でしょう。


本稿の題材のイラスト紹介ページは、以下のリンクからご覧いただけます。エクスプレスデータベースに登録したPIXTAの画像URLを使用し、画像を表示できるようにしました。

イラストページを見る


Expressエンティティ削除時のリレーションに関する注意点(Doctrine ORM)

1. リレーションの種類に注意

  • Expressでは以下のようなリレーションが存在する可能性あり:

    • OneToOne

    • OneToMany

    • ManyToOne

    • ManyToMany

2. 子エンティティ(関連データ)は自動では削除されない

  • 通常、cascade={"remove"} は設定されていないため、

    • EntityManager::remove($entry) をしても 子エンティティは削除されない

    • 関連データが残り、孤立する(=オーファン)。

3. 外部キー制約で削除が失敗する場合がある

  • DBに ON DELETE RESTRICT があると、親の削除に失敗する。

  • Doctrineは flush() 時に例外を投げる。

4. 安全に削除するためのポイント

  • 削除前に 関連エンティティの存在を確認

  • 必要に応じて関連エンティティも明示的に削除する。

  • 重要な操作は Doctrine の トランザクション で行うと安全。

 

結論

Expressの削除処理は一見すると簡単ですが、リレーション(関連データ)がある場合、動作が変わる ため注意が必要です。
必ず「関連データが存在するかどうか」 を確認し、適切な対応を行ってください。

特に、リレーションがある場合は本プログラムを使用しないことを推奨します。


コメント欄を読み込み中