はじめに
プリザンターで一覧画面を開いたり、フィルタをかけたりするとき、裏側ではどんな SQL 文が実行されているのでしょうか?
本記事では プリザンターが内部で組み立てる SQL 文の仕組み をソースコードから追いかけて解説します。アクション(一覧・カレンダー・ガントチャートなど)ごとに SELECT 句・WHERE 句・ORDER BY 句がどう変わるかを体系的に整理します。
連載構成は次の通りです。
| 回 | タイトル |
|---|---|
| 第 1 回(本記事) | アーキテクチャ編 ── SQL を組み立てる仕組みの全体像 |
| 第 2 回 | 一覧編 ── フィルタとソートの WHERE・ORDER BY |
| 第 3 回 | 編集編 ── SELECT・INSERT・UPDATE の実装 |
| 第 4 回 | カレンダー編 ── 日付範囲クエリの実装 |
| 第 5 回 | ガントチャート編 ── GanttUtilities.Where() と GanttLimit |
| 第 6 回 | バーンダウンチャート編 ── 現在 + 履歴 UNION クエリ |
| 第 7 回 | 時系列チャート編 ── HorizontalAxis と NormalAndHistory |
| 第 8 回 | カンバン編 ── KambanLimit・全件取得・カード移動 |
| 第 9 回 | クロス集計編 ── GROUP BY と日付軸グループ化式 |
| 第 10 回 | 画像ライブラリ編 ── Binaries INNER JOIN クエリ |
| 第 11 回 | 分析チャート編 ── 分析パーツごとの現在+履歴 SQL |
バージョン 1.5.2.0 を対象にしています
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 を返す |
コンクリートな処理例:Index アクションから SQL まで
一覧画面リクエストが来てから 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 COUNT
実際のコード:
public static string Index(Context context, SiteSettings ss)
{
var view = Views.GetBySession(context: context, ss: ss);
// GetGridData → GridData コンストラクタ → Get() の順に呼ばれる
var gridData = GetGridData(context: context, ss: ss, view: view);
...
}
private static GridData GetGridData(
Context context, SiteSettings ss, View view, int offset = 0)
{
return new GridData( // コンストラクタ内で Get() が自動呼び出し
context: context,
ss: ss,
view: view,
offset: offset,
pageSize: ss.GridPageSize.ToInt()); // サイト設定の1ページ件数
}
public GridData(
Context context, SiteSettings ss, View view,
int offset = 0, int pageSize = 0, ...)
{
// コンストラクタが直接 Get() を呼び出す(外部から意識しなくてよい)
Get(context, ss, view, offset: offset, pageSize: pageSize);
}
SQL 部品クラスの構造
SQL の各句はそれぞれ専用のコレクションクラスで管理されています。
SqlWhereCollection
WHERE 句の条件を保持するコレクションです。Add() でチェーンし、AND/OR の複合条件も組み立てられます。
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)
{ ... }
}
and: / or: パラメータに別の SqlWhereCollection を渡すことで、(A AND B) OR (C AND D) のようなネストした条件も表現できます。
SqlOrderByCollection
ORDER BY 句の条件を保持するコレクションです。ページネーション用の OFFSET ... FETCH NEXT も BuildCommandText() の中で付加されます。
public class SqlOrderByCollection : ListEx<SqlOrderBy>, IJoin
{
public void BuildCommandText(
ISqlObjectFactory factory,
SqlContainer sqlContainer,
ISqlCommand sqlCommand,
StringBuilder commandText,
int pageSize,
Sqls.TableTypes tableType,
int? commandCount)
{
if (Count > 0)
{
commandText.Append(
"order by ",
this
.GroupBy(o => $"{o.TableName}{o.ColumnBracket}")
.Select(o => o.FirstOrDefault())
.Select(o => o.Sql(...))
.Join(),
" ");
// pageSize が 0 以外ならページネーション句を付加
if (pageSize != 0)
{
commandText.Append(
factory.SqlCommandText.CreateDataRangeCommand(commandCount));
}
}
}
}
SqlSelect(SQL 組み上げの最終工程)
SqlSelect.BuildCommandText() が SELECT 文全体を組み上げます。組み立て順序は次の通りです。
// 1. SELECT 句(DISTINCT / TOP)
SqlColumnCollection?.BuildCommandText(commandText, distinct: Distinct, top: Top);
// 2. FROM 句
var from = From(tableType, As);
commandText.Append(from);
// 3. JOIN 句
SqlJoinCollection?.BuildCommandText(commandText: commandText);
// 4. WHERE 句
SqlWhereCollection?.BuildCommandText(commandText, select: true);
// 5. GROUP BY 句
SqlGroupByCollection?.BuildCommandText(commandText: commandText);
// 6. HAVING 句
SqlHavingCollection?.BuildCommandText(commandText: commandText);
// 7. ORDER BY 句 + ページネーション
SqlOrderByCollection?.BuildCommandText(commandText, PageSize, TableType, commandCount);
// 8. パラメータ追加
AddParams_Where(...);
AddParams_Param(...);
AddParams_Paging(...);
組み立て順序は標準的な SQL の文法通りです。
Rds.SelectIssues / Rds.SelectResults
SQL を実際に作成する際のエントリポイントとなるのが Rds クラスの SelectIssues / SelectResults メソッドです。
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\"" が異なるだけです。どちらも同じ SQL 組み立て機構を共有しています。
テーブルタイプ(TableTypes)
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 クエリが発行されます。
View クラスの役割
SQL 組み立ての中心となるのが View クラス(Libraries/Settings/View.cs)です。画面上のフィルタ・ソート設定を保持し、以下の 3 つのメソッドで SQL 部品に変換します。
// WHERE 句の生成
public SqlWhereCollection Where(Context context, SiteSettings ss, ...) { ... }
// ORDER BY 句の生成
public SqlOrderByCollection OrderBy(Context context, SiteSettings ss, ...) { ... }
// パラメータの生成(拡張フィールド等)
public SqlParamCollection Param(Context context, SiteSettings ss, ...) { ... }
View.Where() が行う処理は次の 5 ステップです。
public SqlWhereCollection Where(Context context, SiteSettings ss, ...)
{
if (where == null) where = new SqlWhereCollection();
// ① 一括処理フィルタ(バルクプロセス実行中の必須入力チェック)
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;
}
第 2 回以降では、このフィルタ処理の詳細と、アクションごとの SELECT 句・WHERE 句の違いを解説していきます。
ss.Join() による JOIN 句の自動生成
ss.Join() は SQL が参照するすべての列・WHERE・ORDER BY コレクションを調べ、必要な JOIN を自動的に決定します。
public SqlJoinCollection Join(Context context, params IJoin[] join)
{
return SqlJoinCollection(
context: context,
tableNames: join
.Where(o => o != null)
.SelectMany(o => o.JoinTableNames()) // 各コレクションが参照するテーブル名を収集
.Distinct()
.ToList());
}
IJoin インターフェースを実装する SqlColumnCollection・SqlWhereCollection・SqlOrderByCollection は、それぞれ JoinTableNames() で参照テーブル名の一覧を返します。ss.Join() はこれらを集めて重複排除し、対応する SqlJoin オブジェクトに変換します。
一覧画面の呼び出し例(GridData.Get() 内):
// SELECT 列・WHERE 句・ORDER BY 句 すべてが参照するテーブルを自動収集
join = join ?? ss.Join(
context: context,
join: new IJoin[]
{
column, // SELECT 句が参照するテーブル(例:Items, 結合サイト)
where, // WHERE 句が参照するテーブル
orderBy // ORDER BY 句が参照するテーブル(タイトルソート時に Items が追加)
});
| 状況 | 追加される JOIN |
|---|---|
| タイトル列を SELECT または ORDER BY |
Items テーブルへの INNER JOIN |
| 結合サイトの列を SELECT | リンク先テーブルへの LEFT OUTER JOIN |
| ユーザー列の WHERE | 特に追加 JOIN なし(外部キー参照のみ) |
Items テーブルへの JOIN は必ず最新バージョンのみを取得する相関サブクエリ付きで生成されます:
INNER JOIN "Items" ON "Items"."ReferenceId" = "Issues"."IssueId"
AND "Items"."Ver" = (
SELECT MAX("NItems"."Ver") FROM "Items" "NItems"
WHERE "NItems"."ReferenceId" = "Issues"."IssueId")
発行される SQL のイメージ(全体)
BuildCommandText() が組み上げる SELECT 文の全体像です。GetSelectFromTableCommand() 内で以下の順に組み立てられます。
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(...);
}
一覧画面(Issues テーブル・Status 昇順ソート・1 ページ 50 件)で生成される SQL の完全なイメージ:
-- ① データ取得 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
"Issues"."TenantId" = @TenantId_0 -- 権限フィルタ
AND "Issues"."SiteId" = @SiteId_0 -- サイト絞り込み
AND "Issues"."Status" < 900 -- 定型フィルタ(未完了)
ORDER BY
"Issues"."Status" ASC, -- ユーザー設定ソート
"Issues"."UpdatedTime" DESC, -- デフォルト追加
"Issues"."IssueId" DESC -- デフォルト追加
OFFSET @_U_Offset ROWS FETCH NEXT @_U_PageSize ROWS ONLY;
-- ② 件数取得 SELECT COUNT
SELECT COUNT(*) "Count"
FROM "Issues"
INNER JOIN "Items" ON ...(同じ JOIN)
WHERE ...(同じ WHERE 句)
2 本の SQL は Repository.ExecuteDataSet() に配列で渡し、1 回の DB ラウンドトリップで同時実行されます。
まとめ
本記事では、プリザンターの SQL 組み立てアーキテクチャを俯瞰しました。
- SQL の各句は
SqlWhereCollection・SqlOrderByCollection・SqlColumnCollectionなどの専用コレクションクラスで管理される -
Rds.SelectIssues()/Rds.SelectResults()が SQL 組み立ての起点となり、SqlSelect.BuildCommandText()で SQL 文字列に変換される - フィルタ・ソート設定は
Viewクラスが保持し、Where()/OrderBy()メソッドで SQL 部品に変換される - テーブルタイプにより通常テーブル・履歴テーブル・削除済みテーブルを切り替えられる
第 2 回では、一覧画面の SQL に絞って WHERE 句やソートの詳細を見ていきます。