0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

プリザンター内部で動くSQL文を調べてみる[第1回:アーキテクチャ編]

0
Posted at

はじめに

プリザンターで一覧画面を開いたり、フィルタをかけたりするとき、裏側ではどんな 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

実際のコード:

Models/Issues/IssueUtilities.cs
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ページ件数
}

Libraries/Models/GridData.cs
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 の複合条件も組み立てられます。

Implem.Libraries/DataSources/SqlServer/SqlWhereCollection.cs
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() の中で付加されます。

Implem.Libraries/DataSources/SqlServer/SqlOrderByCollection.cs
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 文全体を組み上げます。組み立て順序は次の通りです。

Implem.Libraries/DataSources/SqlServer/SqlSelect.cs
// 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 メソッドです。

Libraries/DataSources/Rds.cs
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 時)
Libraries/Models/GridData.cs
tableType: tableType ?? ((view.ShowHistory == true)
    ? Sqls.TableTypes.NormalAndHistory
    : ss.TableType),

一覧画面で「変更履歴」を表示すると NormalAndHistory になり、UNION ALL クエリが発行されます。

View クラスの役割

SQL 組み立ての中心となるのが View クラス(Libraries/Settings/View.cs)です。画面上のフィルタ・ソート設定を保持し、以下の 3 つのメソッドで SQL 部品に変換します。

Libraries/Settings/View.cs
// 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 ステップです。

Libraries/Settings/View.cs
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 を自動的に決定します。

Libraries/Settings/SiteSettings.cs
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() 内):

Libraries/Models/GridData.cs
// 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() 内で以下の順に組み立てられます。

Implem.Libraries/DataSources/SqlServer/SqlSelect.cs
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 句やソートの詳細を見ていきます。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?