Skip to content

拡張サーバースクリプトとバックグラウンドサーバースクリプト ​

第7版作成 最終更新 (日本時間)
確認バージョン1.5.8.1

拡張サーバースクリプト(パラメータフォルダに置き、全サイトに適用するサーバースクリプト)とバックグラウンドサーバースクリプト(テナント管理から登録し、スケジュール実行するサーバースクリプト)を組み合わせた実装例として、レコードの変更履歴を自動削除する仕組みを紹介します。

  • 手動実行: 拡張サーバースクリプトで編集画面に「履歴クリーンアップ」ボタンを追加します。
  • 自動実行: バックグラウンドサーバースクリプトで対象サイトの全レコードを定期的にクリーンアップします。
  • どちらも API で履歴を取得し(TableType: 'History')、「保持するバージョン数」または「保持する日数」を超えた履歴を拡張 SQL で削除します。

背景 ​

プリザンターはレコードを更新するたびに変更履歴をバージョンとして保持します。バージョン番号(Ver)は更新ごとに増えるため、Ver はそのレコードの履歴件数とほぼ一致します。長期運用やインポート・一括更新を繰り返すと履歴が膨れ、データベースサイズの増大や履歴一覧の見づらさにつながります。

画面から削除するには、編集画面の「変更履歴の一覧」タブで 1 件ずつ選んで削除する必要があり、レコード数が多いと現実的ではありません。

処理の流れ ​

図を読み込み中…

手動・自動のどちらも同じ流れで処理します。

図を読み込み中…

  1. (自動実行のみ)API で対象サイトのレコード一覧を取得する
  2. レコードごとに、API で履歴を取得する(TableType: 'History')
  3. Ver の降順に並べ、最新バージョン(= 現在のレコード)を除いて削除対象を判定する(バージョン数超過 または 経過日数超過)
  4. 削除対象があれば、拡張 SQL で該当するバージョンの履歴を削除する

前提条件 ​

History.json ​

履歴の物理削除を有効にするため、PhysicalDelete を true にします。false の場合は「変更履歴を削除」機能自体が無効になります。

json
{
    "Restore": true,
    "PhysicalDelete": true
}

Script.json ​

バックグラウンドサーバースクリプトを有効にするため、BackgroundServerScript を true にします。

json
{
    "ServerScript": true,
    "BackgroundServerScript": true
}

履歴の削除は API ではなく拡張 SQL で行う

api/items/{レコードID}/deletehistory に Selected(バージョン番号の配列)を POST しても履歴は削除できません。確認したソースでは、API(api/items/...)に履歴を削除するエンドポイントはありません(Api/ItemsController.cs)。画面用の items/{レコードID}/deletehistory は HTTP DELETE で、削除対象をフォームデータの GridCheckedItems から読みます(ItemsController.cs、RecordSelector.cs)。サーバースクリプトの httpClient.Delete() は本文を送れないため(ServerScriptModelHttpClient.cs)、この画面用のエンドポイントも使えません。そのため、このページでは拡張 SQL(extendedSql.ExecuteNonQuery)で履歴を削除しています。

拡張 SQL(履歴の削除) ​

履歴テーブルから、指定したレコード ID とバージョンの行を削除する拡張 SQL を用意します。サーバースクリプトから呼ぶ拡張 SQL には "Api": true が必要です(ExtensionUtilities.cs)。レコード ID は記録テーブルと期限付きテーブルで重複しないため、両方の履歴テーブルに同じ条件で DELETE を実行しています。

json
{
    "Name": "DeleteItemHistory",
    "Api": true,
    "CommandText": "delete from \"Results_history\" where \"ResultId\" = @Id and \"Ver\" = @Ver; delete from \"Issues_history\" where \"IssueId\" = @Id and \"Ver\" = @Ver;"
}

DANGER

拡張 SQL はプリザンターの権限チェックや History.json の PhysicalDelete の設定を通らずに、データベースを直接書き換えます。拡張 SQL を配置できるのはサーバーの管理者に限られますが、Api: true の拡張 SQL はどのサーバースクリプトからも呼べるため、SiteIdList で対象サイトを絞るなど、呼び出せる範囲を限定してください。

API キー ​

履歴の取得には API キーが必要です。テナント管理の権限を持つユーザーの API キーを作成 しておきます。

WARNING

パラメータファイルの変更後はプリザンターの再起動が必要です。パラメータ再読み込み 機能を使う場合は、特権ユーザーでログインして実行してください。

拡張サーバースクリプト(手動クリーンアップ) ​

拡張サーバースクリプトは App_Data/Parameters/ExtendedServerScripts/ に 2 つのファイル組(.json と .json.js)として配置します。

ボタンの追加(BeforeOpeningPage) ​

Actions を edit に限定しているので、ボタンは編集画面にだけ表示されます。

json
{
    "BeforeOpeningPage": true,
    "Actions": ["edit"],
    "TryCatch": true,
    "Body": "-- loaded from .json.js"
}
js
context.AddResponse('Append', '#MainCommands',
    '<button id="historyCleanup" class="button button-icon" type="button" onclick="hc_showDialog()">'
    + '<span class="ui-icon ui-icon-clock"></span>'
    + '<span>履歴クリーンアップ</span>'
    + '</button>'
);

context.AddResponse('Append', 'body',
    '<script>'
    + 'function hc_showDialog() {'
    + '  var dialog = document.createElement("dialog");'
    + '  dialog.style.cssText = "padding:20px;border:1px solid #ccc;border-radius:8px;min-width:320px";'
    + '  dialog.innerHTML = \'<form method="dialog">\''
    + '    + \'<h3 style="margin-top:0">履歴クリーンアップ</h3>\''
    + '    + \'<div style="margin-bottom:12px"><label>保持するバージョン数(0で無制限):</label><br>\''
    + '    + \'<input type="number" id="hc-maxVer" value="10" min="0" style="width:100%;padding:4px"></div>\''
    + '    + \'<div style="margin-bottom:16px"><label>保持する日数(0で無制限):</label><br>\''
    + '    + \'<input type="number" id="hc-maxDays" value="90" min="0" style="width:100%;padding:4px"></div>\''
    + '    + \'<div style="text-align:right">\''
    + '    + \'<button type="button" onclick="this.closest(\\\'dialog\\\').close()" style="margin-right:8px">キャンセル</button>\''
    + '    + \'<button type="submit" value="ok">実行</button></div></form>\';'
    + '  document.body.appendChild(dialog);'
    + '  dialog.showModal();'
    + '  dialog.addEventListener("close", function() {'
    + '    var ok = dialog.returnValue === "ok";'
    + '    var maxVer = dialog.querySelector("#hc-maxVer").value;'
    + '    var maxDays = dialog.querySelector("#hc-maxDays").value;'
    + '    dialog.remove();'
    + '    if (!ok) return;'
    + '    var url = $p.apiUrl($p.id(), "update")'
    + '      + "?cleanup=1&maxVer=" + maxVer + "&maxDays=" + maxDays;'
    + '    $.ajax({'
    + '      url: url, method: "PUT",'
    + '      contentType: "application/json", data: "{}",'
    + '      success: function() {'
    + '        $p.setMessage("#Message", JSON.stringify({ Text: "履歴のクリーンアップが完了しました", Css: "alert-success" }));'
    + '      },'
    + '      error: function() {'
    + '        $p.setMessage("#Message", JSON.stringify({ Text: "クリーンアップに失敗しました", Css: "alert-error" }));'
    + '      }'
    + '    });'
    + '  });'
    + '}'
    + '\x3c/script>'
);

HTML の <dialog> でバージョン数と日数を入力させ、「実行」をクリックすると更新 API にクエリパラメータ ?cleanup=1 を付けて呼び出します。URL の組み立てに $p.apiUrl() を使っているので、サブディレクトリ配置でも正しいパスになります。

1.5.8.1 のソースには $p.getId() と $p.message() が無いため、レコード ID の取得は $p.id()、メッセージの表示は $p.setMessage()(第 2 引数は JSON 文字列)を使います(_elements.js、message.js)。

INFO

更新 API を呼ぶため、クリーンアップを実行すると新しいバージョンが 1 つ作成されます。定期的なクリーンアップには、後述のバックグラウンドサーバースクリプトを使うことをおすすめします。

クリーンアップ処理(AfterUpdate) ​

ボタンからの更新 API リクエストを受けて、履歴の取得・判定・削除を行います。

json
{
    "AfterUpdate": true,
    "TryCatch": true,
    "Body": "-- loaded from .json.js"
}
js
// ?cleanup=1 のときだけ処理
if (context.QueryStrings.Data('cleanup') !== '1') return;

// --- 設定 ---
var apiKey = 'YOUR_API_KEY';
var baseUrl = 'http://localhost/';  // プリザンターのURL(末尾/必須)
// --- 設定ここまで ---

var maxVersions = parseInt(context.QueryStrings.Data('maxVer') || '0', 10);
var maxDays = parseInt(context.QueryStrings.Data('maxDays') || '0', 10);

if (maxVersions <= 0 && maxDays <= 0) return;

// 履歴を取得
httpClient.ResponseHeaders.Clear();
httpClient.RequestUri = baseUrl + 'api/items/' + context.Id + '/get';
httpClient.Content = JSON.stringify({
    ApiKey: apiKey,
    TableType: 'History'
});
httpClient.MediaType = 'application/json';
var response = httpClient.Post();

if (!httpClient.IsSuccess) {
    context.Log('履歴取得に失敗: RecordId=' + context.Id);
    return;
}

var result = JSON.parse(response);
var histories = result.Response.Data;

if (!histories || histories.length <= 1) return;

// Ver降順でソート
histories.sort(function (a, b) { return b.Ver - a.Ver; });

// 削除対象のバージョンを判定
var versionsToDelete = [];
var now = new Date();

for (var i = 1; i < histories.length; i++) {
    var h = histories[i];
    var shouldDelete = false;

    // バージョン数による判定
    if (maxVersions > 0 && i >= maxVersions) {
        shouldDelete = true;
    }

    // 経過日数による判定
    if (maxDays > 0) {
        var updatedTime = new Date(h.UpdatedTime);
        var diffDays = (now - updatedTime) / (1000 * 60 * 60 * 24);
        if (diffDays > maxDays) {
            shouldDelete = true;
        }
    }

    if (shouldDelete) {
        versionsToDelete.push(h.Ver);
    }
}

if (versionsToDelete.length === 0) return;

// 履歴を削除(拡張 SQL)
versionsToDelete.forEach(function (ver) {
    extendedSql.ExecuteNonQuery('DeleteItemHistory', { Id: context.Id, Ver: ver });
});

context.Log(
    '履歴クリーンアップ完了: RecordId=' + context.Id
    + ', 削除件数=' + versionsToDelete.length
);
  1. context.QueryStrings.Data('cleanup') を確認し、?cleanup=1 でなければ何もしません。
  2. httpClient で対象レコードの履歴を TableType: 'History' で取得します。
  3. Ver の降順に並べ、インデックス 0(最新バージョン = 現在のレコード)を飛ばします。
  4. バージョン数超過(i >= maxVersions)または経過日数超過(diffDays > maxDays)の履歴を削除対象にします。
  5. 削除対象のバージョン番号ごとに、拡張 SQL DeleteItemHistory で履歴を削除します。

TIP

httpClient.ResponseHeaders.Clear() は 1.4.17.1 以前で 2 回目以降の呼び出しが例外になる問題への対策です。1.5.8.1 では送信のたびに自動でクリアされるため不要ですが、書いておいても害はありません。詳しくは httpClient と外部 API 呼び出しの落とし穴 を参照してください。

バックグラウンドサーバースクリプト(スケジュール実行) ​

スケジュールが内部でどう登録・実行されるか(cron 式への変換、実行ユーザーの権限、ログ)は バックグラウンドサーバースクリプトの仕組み を参照してください。

登録手順 ​

  1. 特権ユーザーでログインします。
  2. 「テナント管理」→「サーバスクリプト」タブを開きます。
  3. 「新規作成」をクリックします。
  4. 次の内容を設定し、「追加」→「更新」をクリックします。
項目設定値
タイトル履歴自動クリーンアップ
条件バックグラウンドサーバスクリプト
スケジュール毎日 02:00(任意)
無効チェックなし
サーバスクリプト下記のスクリプト

スクリプト ​

js
// --- 設定 ---
var apiKey = 'YOUR_API_KEY';
var baseUrl = 'http://localhost/';    // プリザンターのURL(末尾/必須)
var targetSiteIds = [12345, 67890];   // クリーンアップ対象のサイトID
var maxVersions = 10;                 // 保持するバージョン数(0で無制限)
var maxDays = 90;                     // 保持する日数(0で無制限)
// --- 設定ここまで ---

targetSiteIds.forEach(function (siteId) {
    // サイト内のレコード一覧を取得
    httpClient.ResponseHeaders.Clear();
    httpClient.RequestUri = baseUrl + 'api/items/' + siteId + '/get';
    httpClient.Content = JSON.stringify({ ApiKey: apiKey });
    httpClient.MediaType = 'application/json';
    var response = httpClient.Post();

    if (!httpClient.IsSuccess) {
        context.Log('レコード一覧取得に失敗: SiteId=' + siteId);
        return;
    }

    var result = JSON.parse(response);
    var records = result.Response.Data;

    if (!records || records.length === 0) return;

    var totalDeleted = 0;

    records.forEach(function (record) {
        var recordId = record.ResultId || record.IssueId;
        if (!recordId) return;

        var deleted = cleanupHistory(apiKey, baseUrl, recordId, maxVersions, maxDays);
        totalDeleted += deleted;
    });

    context.Log(
        '履歴クリーンアップ完了: SiteId=' + siteId
        + ', レコード数=' + records.length
        + ', 削除履歴数=' + totalDeleted
    );
});

function cleanupHistory(apiKey, baseUrl, recordId, maxVersions, maxDays) {
    // 履歴を取得
    httpClient.ResponseHeaders.Clear();
    httpClient.RequestUri = baseUrl + 'api/items/' + recordId + '/get';
    httpClient.Content = JSON.stringify({
        ApiKey: apiKey,
        TableType: 'History'
    });
    httpClient.MediaType = 'application/json';
    var response = httpClient.Post();

    if (!httpClient.IsSuccess) return 0;

    var result = JSON.parse(response);
    var histories = result.Response.Data;

    if (!histories || histories.length <= 1) return 0;

    // Ver降順でソート
    histories.sort(function (a, b) { return b.Ver - a.Ver; });

    // 削除対象のバージョンを判定
    var versionsToDelete = [];
    var now = new Date();

    for (var i = 1; i < histories.length; i++) {
        var h = histories[i];
        var shouldDelete = false;

        if (maxVersions > 0 && i >= maxVersions) {
            shouldDelete = true;
        }

        if (maxDays > 0) {
            var updatedTime = new Date(h.UpdatedTime);
            var diffDays = (now - updatedTime) / (1000 * 60 * 60 * 24);
            if (diffDays > maxDays) {
                shouldDelete = true;
            }
        }

        if (shouldDelete) {
            versionsToDelete.push(h.Ver);
        }
    }

    if (versionsToDelete.length === 0) return 0;

    // 履歴を削除(拡張 SQL)
    versionsToDelete.forEach(function (ver) {
        extendedSql.ExecuteNonQuery('DeleteItemHistory', { Id: recordId, Ver: ver });
    });

    return versionsToDelete.length;
}

設定項目 ​

設定項目説明
apiKeyテナント管理権限を持つユーザーの API キー
baseUrlプリザンターのベース URL。サブディレクトリ配置なら http://localhost/pleasanter/ のように指定(末尾の / が必須)
targetSiteIdsクリーンアップ対象のサイト ID の配列
maxVersions保持するバージョン数。0 で無制限。10 なら最新 10 バージョン以外を削除
maxDays保持する日数。0 で無制限。90 なら 90 日より前の履歴を削除

レコード数が多い場合 ​

API の 1 回のリクエストで取得できるレコード数には上限があります。対象サイトのレコードが多い場合は、Offset でページングして全件を取得します。1 回の件数の上限は Api.json の PageSize(既定値 200)で、リクエストの PageSize はこれより小さい値のときだけ使われます(Api.json、ResultUtilities.cs)。リクエストに ApiGetPageSize と書いても API は読まないため、PageSize を指定します。

js
function getAllRecords(apiKey, baseUrl, siteId) {
    var allRecords = [];
    var offset = 0;
    var pageSize = 200;

    while (true) {
        httpClient.ResponseHeaders.Clear();
        httpClient.RequestUri = baseUrl + 'api/items/' + siteId + '/get';
        httpClient.Content = JSON.stringify({
            ApiKey: apiKey,
            Offset: offset,
            PageSize: pageSize
        });
        httpClient.MediaType = 'application/json';
        var response = httpClient.Post();

        if (!httpClient.IsSuccess) break;

        var result = JSON.parse(response);
        var records = result.Response.Data;

        if (!records || records.length === 0) break;

        allRecords = allRecords.concat(records);

        if (records.length < pageSize) break;
        offset += pageSize;
    }

    return allRecords;
}

削除基準 ​

基準動作
バージョン数(maxVersions)Ver 降順で先頭(最新)から数えて maxVersions 番目以降を削除。例: maxVersions = 5 で Ver 1〜20 があれば、Ver 20〜16 を保持し Ver 15〜1 を削除
経過日数(maxDays)UpdatedTime が現在から maxDays 日より前の履歴を削除
両方を指定OR 条件。どちらかに該当すれば削除

最新バージョン(現在のレコード)は常に保持されます。

text
例: maxVersions=10, maxDays=90

Ver 20 (1日前)   → 保持(最新)
Ver 19 (3日前)   → 保持(10件以内)
...
Ver 11 (30日前)  → 保持(10件以内)
Ver 10 (60日前)  → 削除(11件目以降 → バージョン数超過)
Ver  9 (80日前)  → 削除(バージョン数超過)
Ver  8 (95日前)  → 削除(バージョン数超過 + 日数超過)
...
Ver  1 (365日前) → 削除(バージョン数超過 + 日数超過)

関連ページ ​

変更履歴

第7版記事の確認版を繰り返す表現を整理する
第6版バックグラウンドサーバースクリプトの仕組みと、サーバースクリプトの他言語対応・DLL 実行・cron スケジュールの改修・設計メモを追加
第5版サーバースクリプトの仕組み・項目の変更可否・拡張サーバースクリプトの解説と、関連する改修・設計メモを追加
第4版本文から元記事や以前の版への言及を除き、正しい動作だけを書く形に整理
第3版「サーバースクリプト」を 1.5.8.1 のソースで検証して修正
第2版記事のファイル名に並び順の番号を付け、元記事リンクを frontmatter の sources に移行
第1版「サーバースクリプト」セクションの記事を追加