内部で動く SQL 文
プリザンターは SQL 文を直接書かず、SqlWhereCollection などの部品クラスを組み合わせ、SqlSelect.BuildCommandText() で SQL 文字列に組み上げています。 このページでは、その組み立てアーキテクチャと、一覧画面(フィルタ・ソート・ページネーション)と編集画面(1 件取得・INSERT・UPDATE)で実際に発行される SQL を整理します。
対象バージョン
バージョン 1.5.2.0 のソースコードを対象にしています。コード例は要点を抜粋・簡略化したものです。
SQL 例の読み方
DBMS 名のない SQL は、3 DBMS で共通の構文を使った説明用の抜粋です。プリザンターは MySQL 接続でも ansi_quotes を設定するため、識別子の二重引用符が使えます。DB のコンソールで直接実行するときの引用符・パラメータの扱いは DBMS ごとの SQL の書き方 を参照してください。... を含む例は実行用 SQL ではありません。
SQL 組み立てのアーキテクチャ
レイヤー構成
図を読み込み中…
| レイヤー | クラス / メソッド | 役割 |
|---|---|---|
| アクション層 | IssueUtilities.Index() など | リクエストを受け取り、データ取得を指示する |
| データ層 | GridData, CalendarDataRows() など | SQL 部品を組み立て、Rds.Select~ を呼び出す |
| SQL 組み立て層 | View.Where(), View.OrderBy() | フィルタ・ソート条件を SqlXxxCollection に変換する |
| SQL 実行層 | SqlSelect.BuildCommandText() | SqlXxxCollection を SQL 文字列に組み上げる |
| 実行エンジン | Repository.ExecuteDataSet() | ADO.NET でクエリを実行し、DataSet を返す |
SQL 部品クラス
SQL の各句は専用のコレクションクラスで管理されています。SqlSelect が次のコレクションを持ちます。
図を読み込み中…
| クラス | 担当 |
|---|---|
SqlColumnCollection | SELECT 句 |
SqlJoinCollection | JOIN 句 |
SqlWhereCollection | WHERE 句 |
SqlGroupByCollection | GROUP BY 句 |
SqlOrderByCollection | ORDER BY 句(ページネーション句も付加) |
SqlParamCollection | パラメータ(INSERT / UPDATE の値など) |
SqlWhereCollection は Add() でチェーンして条件を積み上げます。and: / or: パラメータに別の SqlWhereCollection を渡すことで、(A AND B) OR (C AND D) のようなネストした条件も表現できます。
public class SqlWhereCollection : ListEx<SqlWhere>, IJoin
{
public string Clause = "where ";
public string MultiClauseOperator = " and ";
public SqlWhereCollection Add(
string tableName = null,
string[] columnBrackets = null,
string name = null,
object value = null,
string _operator = "=",
string multiColumnOperator = " or ",
string multiParamOperator = " and ",
SqlStatement sub = null,
string raw = null,
SqlWhereCollection and = null,
SqlWhereCollection or = null,
bool _using = true)
{ ... }
}SqlOrderByCollection.BuildCommandText() は ORDER BY 句を出力し、pageSize が 0 以外なら factory.SqlCommandText.CreateDataRangeCommand() でページネーション句(OFFSET ... FETCH NEXT)を付加します。
SqlSelect.BuildCommandText() の組み立て順序
SELECT 文全体は GetSelectFromTableCommand() 内で、標準的な SQL の文法どおりの順に組み立てられます。
private void GetSelectFromTableCommand(...)
{
// 1. UNION(UNION ALL の場合)を先頭に付加
AddUnion(commandText, unionType);
// 2. SELECT 句(DISTINCT / TOP 付き)
SqlColumnCollection?.BuildCommandText(..., distinct: Distinct, top: Top);
// 3. FROM 句(TableType に応じてテーブル名を切り替え)
commandText.Append(From(tableType, As));
// 4. JOIN 句
SqlJoinCollection?.BuildCommandText(commandText: commandText);
// 5. WHERE 句
SqlWhereCollection?.BuildCommandText(..., select: true);
// 6. GROUP BY 句
SqlGroupByCollection?.BuildCommandText(commandText: commandText);
// 7. HAVING 句
SqlHavingCollection?.BuildCommandText(...);
// 8. ORDER BY 句 + OFFSET ... FETCH NEXT(pageSize != 0 のとき自動付加)
SqlOrderByCollection?.BuildCommandText(..., pageSize: PageSize, ...);
// 9. パラメータ追加(WHERE / HAVING / ページネーション / Param)
AddParams_Where(...);
AddParams_Having(...);
AddParams_Paging(...);
AddParams_Param(...);
}エントリポイント:Rds.SelectIssues / Rds.SelectResults
SQL 作成のエントリポイントは Rds クラスの SelectIssues / SelectResults です。通常・履歴・削除済みの各テーブル名を持った SqlSelect を返します。
public static SqlSelect SelectIssues(
string dataTableName = null,
Sqls.TableTypes tableType = Sqls.TableTypes.Normal,
SqlColumnCollection column = null,
SqlJoinCollection join = null,
SqlWhereCollection where = null,
SqlGroupByCollection groupBy = null,
SqlOrderByCollection orderBy = null,
SqlParamCollection param = null,
bool distinct = false,
int top = 0,
int offset = 0,
int pageSize = 0,
bool _using = true)
{
return new SqlSelect
{
TableBracket = "\"Issues\"",
HistoryTableBracket = "\"Issues_history\"",
DeletedTableBracket = "\"Issues_deleted\"",
SqlColumnCollection = column,
SqlJoinCollection = join,
SqlWhereCollection = where,
SqlGroupByCollection = groupBy,
SqlOrderByCollection = orderBy,
SqlParamCollection = param,
Distinct = distinct,
Top = top,
Offset = offset,
PageSize = pageSize,
};
}SelectResults も同じシグネチャで、TableBracket = "\"Results\"" が異なるだけです。
テーブルタイプ(Sqls.TableTypes)
| 値 | FROM の対象 | 使用場面 |
|---|---|---|
Normal | "Issues" | 通常の一覧・カレンダーなど |
History | "Issues_history" | 変更履歴表示 |
Deleted | "Issues_deleted" | ゴミ箱表示 |
NormalAndHistory | "Issues" UNION ALL "Issues_history" | 変更履歴を含めた一覧(履歴表示 ON 時) |
tableType: tableType ?? ((view.ShowHistory == true)
? Sqls.TableTypes.NormalAndHistory
: ss.TableType),一覧画面で「変更履歴」を表示すると NormalAndHistory になり、UNION ALL クエリが発行されます。
ss.Join() による JOIN 句の自動生成
ss.Join() は、IJoin インタフェースを実装する SqlColumnCollection・SqlWhereCollection・SqlOrderByCollection に JoinTableNames() を呼び出して参照テーブル名を収集し、重複排除して SqlJoin に変換します。
public SqlJoinCollection Join(Context context, params IJoin[] join)
{
return SqlJoinCollection(
context: context,
tableNames: join
.Where(o => o != null)
.SelectMany(o => o.JoinTableNames()) // 各コレクションが参照するテーブル名を収集
.Distinct()
.ToList());
}| 状況 | 追加される JOIN |
|---|---|
| タイトル列を SELECT または ORDER BY | Items テーブルへの INNER JOIN |
| 結合サイトの列を SELECT | リンク先テーブルへの LEFT OUTER JOIN |
| ユーザー列の WHERE | 特に追加 JOIN なし(外部キー参照のみ) |
結合サイトの列(ClassA~200,Title のような列名)の読み方と、経路ごとの JOIN 句の組み立ては リンク先の列(チルダ構文)と JOIN の生成 にまとめています。
Items テーブルへの JOIN には、最新バージョンのレコードのみを取得するための相関サブクエリが自動付与されます。このため、複数バージョンがあっても最新の Title だけが JOIN されます。
INNER JOIN "Items" ON "Items"."ReferenceId" = "Issues"."IssueId"
AND "Items"."Ver" = (
SELECT MAX("NItems"."Ver") FROM "Items" "NItems"
WHERE "NItems"."ReferenceId" = "Issues"."IssueId")一覧画面(Index アクション)の SQL
処理フロー
GET /items/{siteId}
└─ IssueUtilities.Index() ← L31 IssueUtilities.cs
└─ GetGridData(context, ss, view, offset) ← L239 IssueUtilities.cs
└─ new GridData(context, ss, view,
offset, pageSize) ← L26 GridData.cs
└─ Get() ← L55 GridData.cs
├─ ss.GetGridColumns() → ① SELECT 列の決定
├─ View.Where() → ② WHERE 句の生成
├─ View.OrderBy() → ③ ORDER BY 句の生成
├─ ss.Join() → ④ JOIN 句の自動決定
├─ Rds.Select() ⑤ データ取得 SELECT
└─ Rds.SelectCount() ⑥ 件数取得 SELECT COUNTIssueUtilities.Index() はセッションからビュー設定を読み取り(Views.GetBySession())、GetGridData() に渡します。GetGridData() は GridData コンストラクタを呼ぶだけで、コンストラクタ内で Get() が自動的に呼ばれます(IssueUtilities.cs#L31-L57、GridData.cs#L26-L55)。1 ページの件数はサイト設定の GridPageSize です。
実際の SQL 組み立てはすべて GridData.Get() の中で行われます。
図を読み込み中…
private void Get(
Context context, SiteSettings ss, View view,
Sqls.TableTypes tableType, ...)
{
// ① 表示列を取得
var gridColumns = ss.GetGridColumns(context, view, includedColumns: true);
// ② SELECT 句を組み立て
column = column ?? ColumnUtilities.SqlColumnCollection(
context, ss, view, columns: gridColumns);
// ③ WHERE 句を組み立て(フィルタ・権限)
where = view.Where(context, ss, where: where);
// ④ ORDER BY 句を組み立て(ソート設定)
var orderBy = view.OrderBy(context, ss);
// ⑤ JOIN 句を組み立て(列・WHERE・ORDER BY が参照する結合先)
join = join ?? ss.Join(context, join: new IJoin[] { column, where, orderBy });
// ⑥ クエリを実行(データ + 件数)
var statements = new List<SqlStatement>
{
Rds.Select(tableName: ss.ReferenceType, ...,
column: column, join: join, where: where,
orderBy: orderBy, offset: offset, pageSize: pageSize),
Rds.SelectCount(tableName: ss.ReferenceType, join: join, where: where)
};
var dataSet = Repository.ExecuteDataSet(context, statements: statements.ToArray());
}INFO
データ取得と件数取得の 2 本の SQL は Repository.ExecuteDataSet() に配列で渡され、1 回の DB ラウンドトリップで同時実行されます。
SELECT 列の決定(ss.GetGridColumns())
サイト設定の「一覧に表示する項目」(GridColumns)から SELECT 句に使う列一覧を決めます。Ver 列は変更履歴表示の判定に使うため必ず含められます(GridData.cs#L70-L92)。
// Ver 列は必ず含める(変更履歴表示の判定に使用)
if (!gridColumns.Any(o => o.ColumnName == "Ver"))
{
gridColumns.Add(ss.GetColumn(context: context, columnName: "Ver"));
}| 列の種類 | 追加される SELECT 列 | 追加される JOIN |
|---|---|---|
| 通常列(Status, Manager, …) | "Issues"."列名" | なし |
| タイトル列(ItemTitle) | "Items"."Title" AS "ItemTitle" | Items INNER JOIN |
| 結合サイト列 | "テーブル別名"."列名" | 結合先 LEFT OUTER JOIN |
WHERE 句(View.Where())
View.Where() は次の 5 ステップで WHERE 条件を積み上げます。
public SqlWhereCollection Where(Context context, SiteSettings ss, ...)
{
// ① 一括処理フィルタ
SetBulkProcessingFilter(context, ss, process, where);
// ② 定型フィルタ(未完了・自分・期限間近など)
SetGeneralsWhere(context, ss, where);
// ③ 列フィルタ(画面のフィルタ欄)
SetColumnsWhere(context, ss, where);
// ④ テキスト検索フィルタ
SetSearchWhere(context, ss, where, itemJoin);
// ⑤ 権限フィルタ(テナント・サイト・行レベル)
Permissions.SetPermissionsWhere(context, ss, where, permissionType);
return where;
}定型フィルタ(SetGeneralsWhere)
画面上部のフィルタチェックボックスは次の条件に変換されます。
| フィルタ | 生成される条件 |
|---|---|
未完了(Incomplete) | "Issues"."Status" < 900(Parameters.General.CompletionCode) |
自分(Own) | ("Issues"."Manager" = @_U OR "Issues"."Owner" = @_U)(@_U は context.UserId。この _U は条件ごとに付ける名前で、DBMS によらず _U です。PostgreSQL・MySQL でもログインユーザー用の @ipU にはならず、@_U のまま別のパラメータとして渡されます。1 回に複数の文を実行するときは末尾に文の番号が付きます。View.cs、SqlWhere.cs、SqlStatement.cs) |
期限間近(NearCompletionTime) | "Issues"."CompletionTime" between '今日 - NearCompletionTimeBeforeDays' and '今日 + NearCompletionTimeAfterDays + 1' |
遅延(Delay) | 未完了かつ ProgressRate が Def.Sql.ProgressRateDelay の期待値を下回る |
期限間近の実装です。DateTime.Now.Date を基準に日付が埋め込まれます。
// View.cs SetGeneralsWhere
if (NearCompletionTime == true && HasNearCompletionTimeColumns(context, ss))
{
where.Add(
tableName: ss.ReferenceType,
columnBrackets: "\"CompletionTime\"".ToSingleArray(),
_operator: " between '{0}' and '{1}'".Params(
DateTime.Now.Date.AddDays(ss.NearCompletionTimeBeforeDays * -1),
DateTime.Now.Date.AddDays(ss.NearCompletionTimeAfterDays + 1)));
}NearCompletionTimeBeforeDays=3、NearCompletionTimeAfterDays=3 の場合の例です。
"Issues"."CompletionTime" between '2026-03-12' and '2026-03-19'列フィルタ(SetColumnsWhere)
フィルタ欄に入力された値は ColumnFilterHash(列名→値の辞書)に格納され、SetColumnsWhere() で SQL 条件に変換されます。キーの接頭辞で扱いが変わり、or_ / and_ は中身を再帰的に処理して OR / AND のグループにします。
| キーの接頭辞 | 意味 |
|---|---|
or_ | OR グループ(where.Or(or: orWhere)) |
and_ | AND グループ(where.Add(and: andWhere)) |
eq_ | 完全一致 |
notEq_ | 否定一致 |
列の型に応じて条件の組み方が変わります。
| 列の型 | 生成される SQL の例 |
|---|---|
| 数値(decimal) | "Issues"."NumA" >= 100 AND "Issues"."NumA" <= 200 |
| 日時(datetime) | "Issues"."StartTime" >= @_D_0 AND "Issues"."StartTime" < @_D_1 |
| 文字列(nvarchar) | "Issues"."ClassA" like N'%keyword%' |
| 分類(選択肢) | "Issues"."ClassA" in (@ClassA_0, @ClassA_1) |
| チェックボックス | "Issues"."CheckA" = 1 |
テキスト検索フィルタ(SetSearchWhere)
画面上部の検索ボックスの値は Search プロパティに格納され、SetSearchWhere() で LIKE 検索に変換されます。複数のキーワードを「 or 」でつなぐか改行で区切ると OR 検索になります。
private void SetSearchWhere(
Context context, SiteSettings ss, SqlWhereCollection where, bool itemJoin)
{
Search?
.Replace(" ", " ")
.Replace(" or ", "\n") // "A or B" → "A\nB" として OR 分割
.Split('\n')
.Where(o => !o.IsNullOrEmpty())
.ForEach(search =>
collection.Add(and: new SqlWhereCollection()
.FullTextWhere(context, ss, searchText: search, ...)));
if (collection.Any())
where.Add(or: collection); // キーワード間は OR
}「プリザンター」で検索した場合の条件の例です。
(
"Items"."Title" like N'%プリザンター%'
OR "Issues"."Body" like N'%プリザンター%'
OR "Issues"."ClassA" like N'%プリザンター%'
...
)(
"Items"."Title" like '%プリザンター%'
OR "Issues"."Body" like '%プリザンター%'
OR "Issues"."ClassA" like '%プリザンター%'
...
)(
"Items"."Title" like '%プリザンター%'
OR "Issues"."Body" like '%プリザンター%'
OR "Issues"."ClassA" like '%プリザンター%'
...
)検索方式や Items.FullText を使う全文検索の詳細は 検索機能の内部実装 を参照してください。
権限フィルタ(SetPermissionsWhere)
SetPermissionsWhere は、対象に TenantId 列がある場合だけテナント条件を追加します。Issues・Results にはこの列がないため、両テーブルへ TenantId の条件を直接付けることはできません。通常のサイトでは SiteId で絞り込み、サイト権限がなく権限確認が必要な場合にレコード単位の権限条件を追加します。統合サイトの場合は別の分岐で扱います(Permissions.cs)。
ORDER BY 句(View.OrderBy())
View.OrderBy() は ColumnSorterHash(列名→昇降順の辞書)から ORDER BY 条件を組み立てます。
public SqlOrderByCollection OrderBy(Context context, SiteSettings ss, ...)
{
// 拡張 SQL による ORDER BY のカスタマイズ
orderBy.OnSelectingOrderByExtendedSqls(context, ss, ...);
// 変更履歴表示時は IssueId DESC, Ver DESC に固定
if (ShowHistory == true) SetSorterHashOnShowHistory(ss);
// 画面で設定したソート列を適用
if (ColumnSorterHash?.Any() == true)
{
ColumnSorterHash.ForEach(data =>
{
var column = ss.GetColumn(context, data.Key);
if (column != null)
OrderBy(context, ss, orderBy, data, column);
});
}
// デフォルトソート:UpdatedTime DESC, IssueId DESC を追加(未設定時)
if (!orderBy.Any(o => o.ColumnBracket == "\"UpdatedTime\""))
orderBy.Add(tableName: ss.ReferenceType,
columnBracket: "\"UpdatedTime\"",
orderType: SqlOrderBy.Types.desc);
if (!orderBy.Any(o => o.ColumnBracket == $"\"{Rds.IdColumn(ss.ReferenceType)}\""))
orderBy.Add(tableName: ss.ReferenceType,
columnBracket: $"\"{Rds.IdColumn(ss.ReferenceType)}\"",
orderType: SqlOrderBy.Types.desc);
return orderBy;
}- ソート設定をしていなくても、末尾に
UpdatedTime DESC, <ID 列> DESCが必ず付加されます。これにより同一条件での再現性のあるページネーションが保証されます - タイトル列(
Title/ItemTitle)でソートすると、ItemsテーブルのTitle列を参照する ORDER BY になり、Itemsへの JOIN が自動で追加されます
「ステータス昇順」でソートした場合の例です。
ORDER BY
"Issues"."Status" ASC,
"Issues"."UpdatedTime" DESC, -- デフォルト追加
"Issues"."IssueId" DESC -- デフォルト追加
OFFSET 0 ROWS FETCH NEXT 50 ROWS ONLYORDER BY
"Issues"."Status" ASC,
"Issues"."UpdatedTime" DESC, -- デフォルト追加
"Issues"."IssueId" DESC -- デフォルト追加
OFFSET 0 ROWS FETCH NEXT 50 ROWS ONLYORDER BY
"Issues"."Status" ASC,
"Issues"."UpdatedTime" DESC, -- デフォルト追加
"Issues"."IssueId" DESC -- デフォルト追加
LIMIT 50 OFFSET 0ページネーション
OFFSET ... FETCH NEXT は SqlOrderByCollection.BuildCommandText() 内で pageSize != 0 のときに自動付加されます。
| 項目 | SQL パラメータ | 値の出所 |
|---|---|---|
| オフセット(開始行) | @_U_Offset | 画面から送られた GridOffset(読み込み済みの行数) |
| 取得件数 | @_U_PageSize | サイト設定の「1 ページの行数」(GridPageSize) |
画面側の追加読み込み(無限スクロール)
一覧画面にページ番号の切り替えはなく、スクロールで次の GridPageSize 件を読み足す方式です(1.5.8.1)。
図を読み込み中…
- 末尾の検知は
$p.pageObserve()が一覧の直後に作る高さ 1px の#GridObserverをIntersectionObserverで見ています(observer.js、grid.js)。ダイアログが開いている間は読み込みません(scroll.js)。 - レスポンシブ表示で画面幅が 1025px 未満のときは
#ViewModeContainerのscrollイベントで(responsive.js)、ダッシュボードの一覧パーツは.grid-stack-item-contentのscrollイベントで$p.dashboardPaging()を呼びます(scrollevents.js、scroll.js)。ダッシュボードは#GridOffset_{サフィックス}を使います。 - サーバー側の
GridRows()は、GridOffsetが0のときだけ既存の行を消し(Remove(".grid tr"))、それ以外は末尾に足します(Append)。GridOffsetが送られなかったときも0として扱われ、先頭から描き直しになります。応答ではClearFormData("GridOffset")で送信データからGridOffsetを消しています(IssueUtilities.cs)。 - 次の値は
SiteSettings.GridNextOffset()で、offset + 取得件数 < 全件数ならoffset + GridPageSize、そうでなければ-1(終わり)です(SiteSettings.cs)。 GridPageSizeはテーブルの管理 > 一覧の「ページ当たりの表示件数」です。未設定ならGeneral.jsonのGridPageSize(既定20)で、画面で選べる範囲はGridPageSizeMin(10)〜GridPageSizeMax(200)です(SiteSettings.cs、SiteUtilities.cs、General.json)。
ページ番号で切り替える方式に変える場合の改修箇所は 一覧のページネーション(ページ切り替え) にまとめています。
一覧画面で発行される SQL の全体像
Issues テーブル・Status 昇順ソート・1 ページ 50 件の場合のイメージです。
-- ① データ取得 SELECT
SELECT
"Issues"."IssueId",
"Issues"."Ver",
"Issues"."SiteId",
"Items"."Title" AS "ItemTitle",
"Issues"."Status",
"Issues"."Manager",
"Issues"."Owner",
"Issues"."WorkValue",
"Issues"."ProgressRate",
"Issues"."StartTime",
"Issues"."CompletionTime",
"Issues"."UpdatedTime"
FROM "Issues"
INNER JOIN "Items"
ON "Items"."ReferenceId" = "Issues"."IssueId"
AND "Items"."Ver" = (
SELECT MAX("NItems"."Ver") FROM "Items" "NItems"
WHERE "NItems"."ReferenceId" = "Issues"."IssueId")
WHERE
EXISTS (SELECT 1 FROM "Sites" WHERE "Sites"."SiteId" = "Issues"."SiteId" AND "Sites"."TenantId" = @TenantId_0) -- 権限フィルタ
AND "Issues"."SiteId" = @SiteId_0 -- サイト絞り込み
AND "Issues"."Status" < 900 -- 定型フィルタ(未完了)
ORDER BY
"Issues"."Status" ASC, -- ユーザー設定ソート
"Issues"."UpdatedTime" DESC, -- デフォルト追加
"Issues"."IssueId" DESC -- デフォルト追加
OFFSET @_Offset0 ROWS FETCH NEXT @_PageSize0 ROWS ONLY;
-- ② 件数取得 SELECT COUNT
SELECT COUNT(*) "Count"
FROM "Issues"
INNER JOIN "Items" ON ...(同じ JOIN)
WHERE ...(同じ WHERE 句)-- ① データ取得 SELECT
SELECT
"Issues"."IssueId",
"Issues"."Ver",
"Issues"."SiteId",
"Items"."Title" AS "ItemTitle",
"Issues"."Status",
"Issues"."Manager",
"Issues"."Owner",
"Issues"."WorkValue",
"Issues"."ProgressRate",
"Issues"."StartTime",
"Issues"."CompletionTime",
"Issues"."UpdatedTime"
FROM "Issues"
INNER JOIN "Items"
ON "Items"."ReferenceId" = "Issues"."IssueId"
AND "Items"."Ver" = (
SELECT MAX("NItems"."Ver") FROM "Items" "NItems"
WHERE "NItems"."ReferenceId" = "Issues"."IssueId")
WHERE
EXISTS (SELECT 1 FROM "Sites" WHERE "Sites"."SiteId" = "Issues"."SiteId" AND "Sites"."TenantId" = @TenantId_0) -- 権限フィルタ
AND "Issues"."SiteId" = @SiteId_0 -- サイト絞り込み
AND "Issues"."Status" < 900 -- 定型フィルタ(未完了)
ORDER BY
"Issues"."Status" ASC, -- ユーザー設定ソート
"Issues"."UpdatedTime" DESC, -- デフォルト追加
"Issues"."IssueId" DESC -- デフォルト追加
OFFSET @ipOffset0 ROWS FETCH NEXT @ipPageSize0 ROWS ONLY;
-- ② 件数取得 SELECT COUNT
SELECT COUNT(*) "Count"
FROM "Issues"
INNER JOIN "Items" ON ...(同じ JOIN)
WHERE ...(同じ WHERE 句)-- ① データ取得 SELECT
SELECT
"Issues"."IssueId",
"Issues"."Ver",
"Issues"."SiteId",
"Items"."Title" AS "ItemTitle",
"Issues"."Status",
"Issues"."Manager",
"Issues"."Owner",
"Issues"."WorkValue",
"Issues"."ProgressRate",
"Issues"."StartTime",
"Issues"."CompletionTime",
"Issues"."UpdatedTime"
FROM "Issues"
INNER JOIN "Items"
ON "Items"."ReferenceId" = "Issues"."IssueId"
AND "Items"."Ver" = (
SELECT MAX("NItems"."Ver") FROM "Items" "NItems"
WHERE "NItems"."ReferenceId" = "Issues"."IssueId")
WHERE
EXISTS (SELECT 1 FROM "Sites" WHERE "Sites"."SiteId" = "Issues"."SiteId" AND "Sites"."TenantId" = @TenantId_0) -- 権限フィルタ
AND "Issues"."SiteId" = @SiteId_0 -- サイト絞り込み
AND "Issues"."Status" < 900 -- 定型フィルタ(未完了)
ORDER BY
"Issues"."Status" ASC, -- ユーザー設定ソート
"Issues"."UpdatedTime" DESC, -- デフォルト追加
"Issues"."IssueId" DESC -- デフォルト追加
LIMIT @ipPageSize0 OFFSET @ipOffset0;
-- ② 件数取得 SELECT COUNT
SELECT COUNT(*) "Count"
FROM "Issues"
INNER JOIN "Items" ON ...(同じ JOIN)
WHERE ...(同じ WHERE 句)ページ範囲のパラメータ名と構文は SqlSelect.cs、PostgreSqlCommandText.cs、MySqlCommandText.cs に合わせています。例の末尾の 0 は文の番号です。テナント ID は Issues の列ではないため、上の説明用 SQL では Sites を参照しています。実際の権限条件は 権限フィルタ を参照してください。
他のビューモードとの比較
| 項目 | 一覧(GridData) | カレンダー | ガントチャート | カンバン |
|---|---|---|---|---|
| SELECT 列 | GridColumns(多数・可変) | Id・From・To・Title など最小限 | Id・WorkValue・StartTime など進捗列 | Id・Title・Status + X/Y/Value 列 |
| ORDER BY | ColumnSorterHash + UpdatedTime DESC | なし | なし | なし |
| GROUP BY | なし | なし | なし | なし(C# 集計) |
| ページネーション | OFFSET … FETCH NEXT | なし(全件) | なし(全件) | なし(全件) |
| 件数 SQL | あり(SELECT COUNT) | なし(取得後に C# で件数判定) | なし(取得後に C# で件数判定) | あり(InRange で COUNT) |
| 上限制御 | GridPageSize でページ分割 | CalendarLimit で制限 | GanttLimit で制限 | KambanLimit で制限 |
カレンダーは SELECT COUNT を発行せず、取得したデータ行の件数を CalendarUtilities.InRange() で CalendarLimit と比べます(1.5.8.1 のソースでは CalendarUtilities.cs#L20-L32、呼び出し元は IssueUtilities.cs#L9062-L9077)。カンバンの InRange() は IssuesCount() の SELECT COUNT を先に発行します(IssueUtilities.cs#L11368)。ビューごとの詳細は 画面別の内部 SQL を参照してください。
編集画面の SQL
処理フロー
GET /items/{id}/edit
└─ IssueUtilities.Editor(context, ss, issueId, clearSessions) ← L1465 IssueUtilities.cs
└─ new IssueModel(context, ss, issueId: id, ...) ← L738 IssueModel.cs
└─(コンストラクタ内で自動的に)Get(context, ss, ...) ← L864 IssueModel.cs
└─ Rds.SelectIssues(EditorColumns,
WHERE: SiteId + IssueId)
① SELECT(1件取得)
POST /items/{id}/update
└─ IssueModel.Update(context, ss, ...) ← L1949 IssueModel.cs
└─ UpdateStatements(context, ss, ...) ← L2108 IssueModel.cs
├─ IfDuplicatedStatements() SELECT(重複チェック・設定時のみ)
├─ IssuesCopyToStatement() INSERT INTO Issues_history(バージョンアップ時のみ)
├─ UpdateIssues() ② UPDATE Issues
└─ UpdateRelatedRecordsStatements()
├─ UpdateItems() ③ UPDATE Items(タイトル・全文検索索引)
└─ InsertLinks() INSERT / DELETE LinksINFO
new IssueModel(context, ss, issueId: X) のコンストラクタは内部で自動的に Get() を呼び出し、指定した issueId のレコードを SELECT します。コンストラクタを呼ぶだけでレコードが取得済みになります。
レコード取得(SELECT)
編集画面・詳細画面でレコードを 1 件取得するのが IssueModel.Get() です。
public IssueModel Get(
Context context,
SiteSettings ss,
View view = null,
Sqls.TableTypes tableType = Sqls.TableTypes.Normal,
SqlColumnCollection column = null,
SqlWhereCollection where = null,
SqlOrderByCollection orderBy = null, ...)
{
// WHERE: デフォルトは SiteId + IssueId のみ
where = (view != null)
? Rds.IssuesWhere().SiteId(SiteId)
: where ?? Rds.IssuesWhereDefault(context, issueModel: this);
// 列フィルタ(タイムスタンプ照合など)を追加
view = view ?? new View();
view.SetColumnsWhere(context, ss, where,
siteId: SiteId, id: IssueId, timestamp: Timestamp.ToDateTime());
// SELECT 列: EditorColumns(編集画面の表示列)+拡張 SQL 列
column = (column ?? Rds.IssuesEditorColumns(ss))
?.SetExtendedSqlSelectingColumn(context, ss, view);
// JOIN: 列・WHERE・ORDER BY が参照するテーブルを自動算出
join = join ?? ss.Join(context, join: new IJoin[] { column, where, orderBy });
// クエリ実行して IssueModel にセット
Set(context, ss, Repository.ExecuteTable(context, statements:
Rds.SelectIssues(
tableType: tableType,
column: column,
join: join,
where: where,
orderBy: orderBy)));
return this;
}WHERE 句の基本条件は Rds.IssuesWhereDefault() が生成する SiteId + IssueId です。
WHERE "Issues"."SiteId" = @SiteId_0
AND "Issues"."IssueId" = @IssueId_0URL に ver クエリパラメータが含まれる場合(例:/items/{id}/edit?ver=3)は、NormalAndHistory(通常 + 履歴の UNION ALL)を対象に、指定バージョンで絞り込んでクエリが実行されます。これにより過去バージョンの内容を参照できます。
if (context.QueryStrings.ContainsKey("ver"))
{
Get(
context: context,
tableType: Sqls.TableTypes.NormalAndHistory, // 履歴テーブルを含む
column: column,
where: Rds.IssuesWhereDefault(context, this)
.Issues_Ver(context.QueryStrings.Int("ver")), // 指定バージョンで絞り込み
ss: ss);
}
else
{
Get(context: context, ss: ss, view: view, column: column); // 通常テーブル
}| 項目 | 一覧画面 | 編集画面 |
|---|---|---|
| SELECT 列 | GridColumns(一覧表示列) | EditorColumns(編集表示列) |
| WHERE 条件 | フィルタ・権限 + SiteId | SiteId + IssueId のみ |
| ORDER BY | ColumnSorterHash + デフォルト | 通常なし |
| ページネーション | あり(OFFSET … FETCH NEXT) | なし |
| 件数取得 SQL | あり(SELECT COUNT) | なし |
新規画面
新規作成画面は基本的に SQL を発行しません(空の IssueModel を使用)。ただし別レコードからのコピー作成(CopyFrom パラメータ指定時、かつ ss.AllowReferenceCopy == true)では、コピー元レコードを IssueModel のコンストラクタ経由で SELECT し、読み取り権限を確認したうえで CopyAndInit() で値を複写します。
レコード新規作成(INSERT)
新規保存時は IssueModel.Create() → CreateStatements() で INSERT 文を生成します。
public List<SqlStatement> CreateStatements(Context context, SiteSettings ss, ...)
{
var statements = new List<SqlStatement>();
// ① 重複チェック(IfDuplicated: 一意制約の事前検証 SELECT)
statements.AddRange(IfDuplicatedStatements(ss: ss));
statements.AddRange(new List<SqlStatement>
{
// ② Items テーブルに INSERT(ReferenceId を自動採番で取得)
Rds.InsertItems(
selectIdentity: true,
param: Rds.ItemsParam()
.ReferenceType("Issues")
.SiteId(SiteId)
.Title(Title.DisplayValue)),
// ③ Issues テーブルに INSERT
Rds.InsertIssues(
param: Rds.IssuesParamDefault(
context, ss, issueModel: this, setDefault: true)),
// ④ Links テーブルに INSERT(リンク項目)
InsertLinks(context, ss, setIdentity: true),
});
// ⑤ 添付ファイルを Binaries テーブルに UPDATE/INSERT
statements.AddRange(UpdateAttachmentsStatements(context, ss));
// ⑥ 行レベル権限を Permissions テーブルに INSERT
statements.AddRange(PermissionUtilities.InsertStatements(...));
return statements;
}-- Items テーブルへの INSERT(ID は自動採番)
INSERT INTO "Items" ("Creator", "Updator", "ReferenceType", "SiteId", "Title", ...)
VALUES (@_U, @_U, N'Issues', @SiteId_0, @Title_0, ...)
-- Issues テーブルへの INSERT(Items の採番 ID を外部キーとして使用)
INSERT INTO "Issues" ("Creator", "Updator", "IssueId", "SiteId", "Ver", "Title",
"StartTime", "CompletionTime", "WorkValue", "ProgressRate", "Status", ...)
VALUES (@_U, @_U, @_I, @SiteId_0, @Ver_0, @Title_0,
@StartTime_0, @CompletionTime_0, @WorkValue_0, @ProgressRate_0, @Status_0, ...)-- Items テーブルへの INSERT(ID は自動採番)
INSERT INTO "Items" ("Creator", "Updator", "ReferenceType", "SiteId", "Title", ...)
VALUES (@ipU, @ipU, 'Issues', @SiteId_0, @Title_0, ...)
-- Issues テーブルへの INSERT(Items の採番 ID を外部キーとして使用)
INSERT INTO "Issues" ("Creator", "Updator", "IssueId", "SiteId", "Ver", "Title",
"StartTime", "CompletionTime", "WorkValue", "ProgressRate", "Status", ...)
VALUES (@ipU, @ipU, @ipI, @SiteId_0, @Ver_0, @Title_0,
@StartTime_0, @CompletionTime_0, @WorkValue_0, @ProgressRate_0, @Status_0, ...)-- Items テーブルへの INSERT(ID は自動採番)
INSERT INTO "Items" ("Creator", "Updator", "ReferenceType", "SiteId", "Title", ...)
VALUES (@ipU, @ipU, 'Issues', @SiteId_0, @Title_0, ...)
-- Issues テーブルへの INSERT(Items の採番 ID を外部キーとして使用)
INSERT INTO "Issues" ("Creator", "Updator", "IssueId", "SiteId", "Ver", "Title",
"StartTime", "CompletionTime", "WorkValue", "ProgressRate", "Status", ...)
VALUES (@ipU, @ipU, @ipI, @SiteId_0, @Ver_0, @Title_0,
@StartTime_0, @CompletionTime_0, @WorkValue_0, @ProgressRate_0, @Status_0, ...)後続の INSERT で使う採番済み ID は SQL Server では @_I、PostgreSQL・MySQL では @ipI です(SQLServer/Identity.sql、PostgreSQL/Identity.sql、MySQL/Identity.sql)。この値を設定する処理は上の抜粋では省略しています。
SqlInsert.Build_InsertStatement() が SqlParamCollection の各エントリを列名と値のリストに変換し、INSERT INTO ... (columns) VALUES (params) を組み上げます(SqlInsert.cs#L41-L108)。
| 順序 | SQL の種類 | 対象テーブル |
|---|---|---|
| ① | SELECT(重複チェック) | Issues |
| ② | INSERT | Items |
| ③ | INSERT | Issues |
| ④ | INSERT | Links |
| ⑤ | UPDATE / INSERT | Binaries(添付ファイル) |
| ⑥ | INSERT | Permissions(行レベル権限設定時) |
これらは Repository.ExecuteScalar_response() でトランザクション実行され、採番した ID が返ります。
レコード更新(UPDATE)
更新時は IssueModel.Update() → UpdateStatements() で SQL を積み上げます。
public List<SqlStatement> UpdateStatements(
Context context, SiteSettings ss, ...)
{
// WHERE: SiteId + IssueId + UpdatedTime(楽観的排他用タイムスタンプ)
var where = Rds.IssuesWhereDefault(context, issueModel: this)
.UpdatedTime(timestamp, _using: timestamp.InRange() && checkConflict);
// ① 重複チェック
statements.AddRange(IfDuplicatedStatements(ss: ss));
// ② バージョンアップ時:現行レコードを Issues_history にコピー
if (verUp)
{
statements.Add(Rds.IssuesCopyToStatement(
where: where,
tableType: Sqls.TableTypes.History,
ColumnNames()));
Ver++;
}
// ③ Issues テーブルを UPDATE
statements.AddRange(UpdateStatements(context, ss, where: where, ...));
// ④ 添付ファイルを Binaries テーブルに UPDATE / INSERT
statements.AddRange(UpdateAttachmentsStatements(context, ss, verUp: verUp));
// ⑤ 行レベル権限を Permissions テーブルに UPDATE / INSERT
if (ss.PermissionForUpdating?.Any() == true)
statements.AddRange(PermissionUtilities.UpdateStatements(...));
return statements;
}③ の内側の UpdateStatements() は、Rds.UpdateIssues() に続けて new SqlStatement() { IfConflicted = true, Id = IssueId } を積みます。
-- バージョンアップ時のみ: 現行レコードを history テーブルにコピー
INSERT INTO "Issues_history" ("IssueId", "SiteId", "Ver", "Title", ...)
SELECT "IssueId", "SiteId", "Ver", "Title", ...
FROM "Issues"
WHERE "Issues"."SiteId" = @SiteId_0
AND "Issues"."IssueId" = @IssueId_0
-- Issues テーブルを UPDATE
UPDATE "Issues"
SET "Updator" = @_U,
"UpdatedTime" = GETDATE(),
"Title" = @Title_0,
"Status" = @Status_0,
"CompletionTime" = @CompletionTime_0,
"ProgressRate" = @ProgressRate_0,
...
WHERE "Issues"."SiteId" = @SiteId_0
AND "Issues"."IssueId" = @IssueId_0
AND "Issues"."UpdatedTime" = @UpdatedTime_0 -- 楽観的排他制御-- バージョンアップ時のみ: 現行レコードを history テーブルにコピー
INSERT INTO "Issues_history" ("IssueId", "SiteId", "Ver", "Title", ...)
SELECT "IssueId", "SiteId", "Ver", "Title", ...
FROM "Issues"
WHERE "Issues"."SiteId" = @SiteId_0
AND "Issues"."IssueId" = @IssueId_0
-- Issues テーブルを UPDATE
UPDATE "Issues"
SET "Updator" = @ipU,
"UpdatedTime" = CURRENT_TIMESTAMP,
"Title" = @Title_0,
"Status" = @Status_0,
"CompletionTime" = @CompletionTime_0,
"ProgressRate" = @ProgressRate_0,
...
WHERE "Issues"."SiteId" = @SiteId_0
AND "Issues"."IssueId" = @IssueId_0
AND "Issues"."UpdatedTime" = @UpdatedTime_0 -- 楽観的排他制御-- バージョンアップ時のみ: 現行レコードを history テーブルにコピー
INSERT INTO "Issues_history" ("IssueId", "SiteId", "Ver", "Title", ...)
SELECT "IssueId", "SiteId", "Ver", "Title", ...
FROM "Issues"
WHERE "Issues"."SiteId" = @SiteId_0
AND "Issues"."IssueId" = @IssueId_0
-- Issues テーブルを UPDATE
UPDATE "Issues"
SET "Updator" = @ipU,
"UpdatedTime" = CURRENT_TIMESTAMP(3),
"Title" = @Title_0,
"Status" = @Status_0,
"CompletionTime" = @CompletionTime_0,
"ProgressRate" = @ProgressRate_0,
...
WHERE "Issues"."SiteId" = @SiteId_0
AND "Issues"."IssueId" = @IssueId_0
AND "Issues"."UpdatedTime" = @UpdatedTime_0 -- 楽観的排他制御SqlUpdate.Build_UpdateStatement() が SqlParamCollection の Updating = true なエントリを 列名 = @パラメータ の形式に変換し、UPDATE ... SET ... WHERE ... を組み上げます(SqlUpdate.cs#L49-L103)。
楽観的排他制御
UPDATE の WHERE 句には UpdatedTime = @UpdatedTime_0 が含まれます。画面表示時と更新送信時のタイムスタンプが一致しない場合(他ユーザーが先に更新した場合)、UPDATE の影響行数が 0 になり、IfConflicted = true の SqlStatement がそれを検知して競合エラーを返します。確認したソースでも同じ実装です(IssueModel.cs#L2146-L2151、IssueModel.cs#L2224-L2229)。
| 順序 | SQL の種類 | 対象テーブル | 条件 |
|---|---|---|---|
| ① | SELECT(重複チェック) | Issues | 一意制約設定時のみ |
| ② | INSERT(history コピー) | Issues_history | バージョンアップ時のみ |
| ③ | UPDATE | Issues | 常に |
| ③ | UPDATE | Items(タイトル更新) | タイトル変更時 |
| ④ | UPDATE / INSERT / DELETE | Binaries | 添付ファイル変更時 |
| ⑤ | UPDATE / INSERT | Permissions | 行レベル権限設定時 |
Items テーブルの自動更新
UPDATE 後、UpdateRelatedRecordsStatements() で Issues とは別に Items テーブルも更新されます。タイトル変更時だけでなく、全文検索索引の更新のために毎回実行されます。
public List<SqlStatement> UpdateRelatedRecordsStatements(...)
{
var fullText = FullText(context, ss: ss); // 全文検索用テキストを生成
var statements = new List<SqlStatement>();
// Items テーブルを UPDATE(タイトル + 全文検索索引)
statements.Add(Rds.UpdateItems(
where: Rds.ItemsWhere().ReferenceId(IssueId),
param: Rds.ItemsParam()
.SiteId(SiteId)
.Title(Title.DisplayValue) // タイトルを最新値に更新
.FullText(fullText, _using: fullText != null) // 全文検索索引を更新
.SearchIndexCreatedTime(DateTime.Now, _using: fullText != null),
...));
...
}UPDATE "Items"
SET "SiteId" = @SiteId_0,
"Title" = @Title_0, -- 最新のタイトル値
"FullText" = @FullText_0, -- 全文検索用インデックス
"SearchIndexCreatedTime" = @SearchIndexCreatedTime_0,
"Updator" = @_U,
"UpdatedTime" = GETDATE()
WHERE "Items"."ReferenceId" = @ReferenceId_0 -- IssueId と一致するレコードUPDATE "Items"
SET "SiteId" = @SiteId_0,
"Title" = @Title_0, -- 最新のタイトル値
"FullText" = @FullText_0, -- 全文検索用インデックス
"SearchIndexCreatedTime" = @SearchIndexCreatedTime_0,
"Updator" = @ipU,
"UpdatedTime" = CURRENT_TIMESTAMP
WHERE "Items"."ReferenceId" = @ReferenceId_0 -- IssueId と一致するレコードUPDATE "Items"
SET "SiteId" = @SiteId_0,
"Title" = @Title_0, -- 最新のタイトル値
"FullText" = @FullText_0, -- 全文検索用インデックス
"SearchIndexCreatedTime" = @SearchIndexCreatedTime_0,
"Updator" = @ipU,
"UpdatedTime" = CURRENT_TIMESTAMP(3)
WHERE "Items"."ReferenceId" = @ReferenceId_0 -- IssueId と一致するレコードINFO
Issues テーブルの Title 列と Items テーブルの Title 列は独立して管理されます。Issues の Title はフォームから入力される値そのものを保持し、Items の Title は表示用の DisplayValue(選択肢ラベルに変換済み)を保持します。