Concrete CMS エクスプレスデータの重複対応・一括更新ジョブ

2025/05/12
Concrete CMSのExpressデータを大量に扱う中で、同一レコードの重複登録や既存データの更新が必要になることがあります。
このジョブプログラムは、最終更新日を基準に最新のレコードを残し、古い重複データを自動的に判別・整理することで、登録データの一括更新を効率化します。
これにより、管理作業の手間を減らし、常に最新かつ正確なデータを維持することができます。

エントリーデータの一括更新にも利用可能

本ジョブは、illust_no ごとにエントリをグルーピングし、更新日時でソートして最新の1件を残し、それ以外を削除する処理を行います。そのため、更新したい内容を同一 illust_no で新規登録し、このジョブを実行することで、既存データと自動で置き換わるような挙動を実現できます
これは、Expressエンティティにおける「一括更新」や「データ差し替え」の運用に活用できるため、定期バッチ処理やCSVインポートとの併用において強力な手段となります。単に重複データの削除を越えた利便性の高い使用方法です。

Concrete CMS ジョブ紹介:CleanupDuplicateEntries

CleanupDuplicateEntries は、Concrete CMS の Express エンティティにおいて、属性 illust_no の値が重複しているエントリを検出し、更新日時で降順ソートを施し最初の1件を残して重複を自動的に削除するジョブです。

以前に開発して紹介しているConcrete CMS エクスプレスデータベースのデータ一括削除の応用ですので、理論的な理解を深めるにはそちらも御覧ください。

特徴

  • 対象の Express エンティティ(この例では illust)からすべてのエントリを取得

  • illust_no の値でグルーピングし、重複しているグループを判定(illust_noはユニーク値であることが前提)

  • 各グループ内の先頭エントリを残し、残りを削除

  • Doctrine ORM を使って効率的にデータを操作

  • Concrete CMS の管理画面からジョブとして手動実行可能

現在の課題

  • バックグラウンド処理に未対応:大量データを扱う場合、PHP の実行時間制限やタイムアウトにより処理が完了しない可能性があります。

  • バッチ処理未実装:一括処理のため、データ量が多いとページの応答性が低下することがあります。

このジョブは、Express エンティティにおける重複データのクリーンアップを簡便に行いたいユーザー向けのシンプルで効果的なツールです。データが比較的少ない環境では即戦力となるでしょう。

Jobへの登録

主な用途は夜間のCron処理になると思いますので、この機能をJobに登録しました。

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

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

class CleanupDuplicateEntries extends Job
{
    // ジョブの名前
    public function getJobName()
    {
        return t("Cleanup Duplicate illust_no Entries by Latest Modified Date");
    }

    // ジョブの説明
    public function getJobDescription()
    {
        return t("Deletes duplicate illust_no entries, keeping only the one with the most recent modification date.");
    }

    // ジョブの実行ロジック
    public function run()
    {
        // Express エンティティと属性のハンドル名
        $entityHandle = 'illust';
        $attributeHandle = 'illust_no';

        // Express エンティティを取得
        $express = \Core::make('express');
        $entity = $express->getObjectByHandle($entityHandle);

        if (!$entity) {
            // エンティティが見つからない場合はエラーメッセージを返す
            return t("Error: Express entity '{$entityHandle}' not found.");
        }

        // エンティティ内の全エントリを取得(パーミッション無視)
        $entryList = new EntryList($entity);
        $entryList->ignorePermissions();
        $entries = $entryList->getResults();

        // illust_no ごとにグループ化する配列を初期化
        $map = [];
        foreach ($entries as $entry) {
            $illustNo = $entry->getAttribute($attributeHandle);
            if (!$illustNo) {
                continue; // illust_no が空の場合はスキップ
            }

            if (!isset($map[$illustNo])) {
                $map[$illustNo] = [];
            }
            $map[$illustNo][] = $entry;
        }

        // Doctrine のエンティティマネージャを取得
        $em = \Core::make(EntityManagerInterface::class);
        $deleted = 0;

        // 重複グループを走査し、最新の1件を残して他を削除
        foreach ($map as $illustNo => $entryGroup) {
            if (count($entryGroup) > 1) {
                // getDateModified() を使って更新日時の降順にソート
                usort($entryGroup, function ($a, $b) {
                    return $b->getDateModified() <=> $a->getDateModified();
                });

                // 最新の1件を除く残りを削除対象に
                $entriesToDelete = array_slice($entryGroup, 1);

                foreach ($entriesToDelete as $entry) {
                    $found = $em->find(Entry::class, $entry->getID());
                    if ($found) {
                        $em->remove($found);
                        $deleted++;
                    }
                }
            }
        }

        // エンティティ削除を確定(コミット)
        $em->flush();

        // 削除件数を返却
        return "{$deleted} duplicate entries deleted.";
    }
}
controller.php

<?php
namespace Concrete\Package\CleanupDuplicateEntries;

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

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

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

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

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

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

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

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

主な使用は、Cron処理になると思いますので、Job画面よりCron用のurlを取得して実行してください。

当然ですが、$entityHandleと$attributeHandleは、ご自身の環境に適した内容に変更してください。
また、$attributeHandleはユニークな属性である必要があります。

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

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

ダウンロードzip

実行方法には注意が必要

Concrete CMSでは、Jobプログラムを簡単に作成・実行できますが、処理が重い場合は実行方法に注意が必要です。
JobはPHP-FPMやApache上で動作するため、大量データを扱うとタイムアウトやWebリソースの占有が発生しやすく、Web閲覧など他の操作に影響を及ぼす可能性があります。

自動実行Jobの画面から取得できるCron用URLを使って定期的に実行する方法もありますが、これも同じ環境下で動作するため、根本的な負荷軽減にはなりません。

実際に、1万件のエントリーがある状態で1件の差し替え処理を行ったところ、約10秒を要しました。さらに、65件の処理では18分もの時間がかかり、その間ホームページの閲覧ができない状態が続きました。こうした状況は、運用上の大きな課題となります。後述のCLIによる400件処理では1時間20分を要しましたが、バックグラウンド処理のためさほど気になりません。

課題解決の方法

以下のような方法で、影響を最小限に抑えることができます:

  • Cron用URLの夜間実行
     閲覧の少ない深夜などの時間帯に、Cron用のURLを用いてJobを実行することで、ユーザーへの影響を軽減できます。

  • CLIによるバックグラウンド実行への切り替え
     ブラウザ経由ではなく、コマンドライン(CLI)でJobを実行することで、Webサーバーのタイムアウトやメモリ制限の制約を受けにくくなります。今回のケースでは、以下のコマンドで実行可能です。
     

bash

concrete/bin/concrete5 c5:job cleanup_duplicate_entries

このように、運用環境に応じた実行方法を選ぶことで、Concrete CMSの柔軟性を活かしつつ、安定したシステム運用が可能になります。実際にはこの処理をログ出力付きで夜間にCronで行っております。

PHPバージョンの調整が必要になることも

私のサーバー環境はXserverを利用しています。
「CLIによるバックグラウンド実行への切り替え」を試みた際、意外な問題に直面しました。
Xserverの初期設定では、CLIで使用されるPHPのバージョンが非常に古く、なんとPHP 5.4.16(約10年前のバージョン)が指定されていたのです。

そのため、次のようなエラーが表示され、Concrete CMSの動作要件を満たしていないことが分かりました。

concrete5 requires PHP 5.5.9+ to run.
 You are running PHP 5.4.16

whereisコマンドで確認したところ、サーバー上には複数のPHPバージョンが存在しており、Webで使用しているPHP 7.4.13も含まれていました。そこで、このバージョンをCLI用として明示的に以下のように指定の上で実行することで問題を解決しました。

/opt/php-7.4.13/bin/php concrete5 c5:job cleanup_duplicate_entries


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

運用ページを見る


#
2025/04/26
Concrete CMSのエクスプレスデータベースには標準でCSV入力が無いのですね。そこでJOBに登録して簡単に利用できるものを作ってみました。
#
2025/05/01
9,000件のConcrete CMSエクスプレスデータを削除しようと「エントリーをクリア」を実行したところ、タイムアウトエラーが発生。 手作業で一つずつ削除するのは現実的ではないため、一括削除プログラム を作成しました。
#
2025/05/12
Concrete CMSのExpressデータを大量に扱う中で、同一レコードの重複登録や既存データの更新が必要になることがあります。 このジョブプログラムは、最終更新日を基準に最新のレコードを残し、古い重複データを自動的に判別・整理することで、登録データの一括更新を効率化します。 これにより、管理作業の手間を減らし、常に最新かつ正確なデータを維持することができます。
#
2025/05/13
Concrete CMSの標準機能では、説明(description)がmeta descriptionに変換されない問題を解決するための「Description to Meta」Jobプログラムを開発しました。これにより、記事概要を自動でmeta descriptionへコピーし、検索結果の表示を最適化できます。SEO対策と運用効率を向上させるための便利なツールです。
#
2025/05/18
Concrete CMS の Express エントリ一覧において「最大 1000 件までしか取得できない」という制限に直面し、標準の EntryList や Express ブロックでは対応が難しかった。ORM の制約によりカスタマイズによる解決も困難だったが、Doctrine クエリビルダーを直接用いる方法へ移行。その結果、大規模データを扱う要件を満たす実装が実現できた。副次的にスペース区切りでのAND検索機能も獲得できた。
コメント欄を読み込み中