拡張サーバースクリプトとバックグラウンドサーバースクリプト
拡張サーバースクリプト(パラメータフォルダに置き、全サイトに適用するサーバースクリプト)とバックグラウンドサーバースクリプト(テナント管理から登録し、スケジュール実行するサーバースクリプト)を組み合わせた実装例として、レコードの変更履歴を自動削除する仕組みを紹介します。
- 手動実行: 拡張サーバースクリプトで編集画面に「履歴クリーンアップ」ボタンを追加します。
- 自動実行: バックグラウンドサーバースクリプトで対象サイトの全レコードを定期的にクリーンアップします。
- どちらも API で履歴を取得し(
TableType: 'History')、「保持するバージョン数」または「保持する日数」を超えた履歴を拡張 SQL で削除します。
背景
プリザンターはレコードを更新するたびに変更履歴をバージョンとして保持します。バージョン番号(Ver)は更新ごとに増えるため、Ver はそのレコードの履歴件数とほぼ一致します。長期運用やインポート・一括更新を繰り返すと履歴が膨れ、データベースサイズの増大や履歴一覧の見づらさにつながります。
画面から削除するには、編集画面の「変更履歴の一覧」タブで 1 件ずつ選んで削除する必要があり、レコード数が多いと現実的ではありません。
処理の流れ
図を読み込み中…
手動・自動のどちらも同じ流れで処理します。
図を読み込み中…
- (自動実行のみ)API で対象サイトのレコード一覧を取得する
- レコードごとに、API で履歴を取得する(
TableType: 'History') Verの降順に並べ、最新バージョン(= 現在のレコード)を除いて削除対象を判定する(バージョン数超過 または 経過日数超過)- 削除対象があれば、拡張 SQL で該当するバージョンの履歴を削除する
前提条件
History.json
履歴の物理削除を有効にするため、PhysicalDelete を true にします。false の場合は「変更履歴を削除」機能自体が無効になります。
{
"Restore": true,
"PhysicalDelete": true
}Script.json
バックグラウンドサーバースクリプトを有効にするため、BackgroundServerScript を true にします。
{
"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 を実行しています。
{
"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 に限定しているので、ボタンは編集画面にだけ表示されます。
{
"BeforeOpeningPage": true,
"Actions": ["edit"],
"TryCatch": true,
"Body": "-- loaded from .json.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 リクエストを受けて、履歴の取得・判定・削除を行います。
{
"AfterUpdate": true,
"TryCatch": true,
"Body": "-- loaded from .json.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
);context.QueryStrings.Data('cleanup')を確認し、?cleanup=1でなければ何もしません。httpClientで対象レコードの履歴をTableType: 'History'で取得します。Verの降順に並べ、インデックス 0(最新バージョン = 現在のレコード)を飛ばします。- バージョン数超過(
i >= maxVersions)または経過日数超過(diffDays > maxDays)の履歴を削除対象にします。 - 削除対象のバージョン番号ごとに、拡張 SQL
DeleteItemHistoryで履歴を削除します。
TIP
httpClient.ResponseHeaders.Clear() は 1.4.17.1 以前で 2 回目以降の呼び出しが例外になる問題への対策です。1.5.8.1 では送信のたびに自動でクリアされるため不要ですが、書いておいても害はありません。詳しくは httpClient と外部 API 呼び出しの落とし穴 を参照してください。
バックグラウンドサーバースクリプト(スケジュール実行)
スケジュールが内部でどう登録・実行されるか(cron 式への変換、実行ユーザーの権限、ログ)は バックグラウンドサーバースクリプトの仕組み を参照してください。
登録手順
- 特権ユーザーでログインします。
- 「テナント管理」→「サーバスクリプト」タブを開きます。
- 「新規作成」をクリックします。
- 次の内容を設定し、「追加」→「更新」をクリックします。
| 項目 | 設定値 |
|---|---|
| タイトル | 履歴自動クリーンアップ |
| 条件 | バックグラウンドサーバスクリプト |
| スケジュール | 毎日 02:00(任意) |
| 無効 | チェックなし |
| サーバスクリプト | 下記のスクリプト |
スクリプト
// --- 設定 ---
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 を指定します。
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 条件。どちらかに該当すれば削除 |
最新バージョン(現在のレコード)は常に保持されます。
例: 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日前) → 削除(バージョン数超過 + 日数超過)