Skip to content

内部で動く SQL 文 ​

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

プリザンターは 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 が次のコレクションを持ちます。

図を読み込み中…

クラス担当
SqlColumnCollectionSELECT 句
SqlJoinCollectionJOIN 句
SqlWhereCollectionWHERE 句
SqlGroupByCollectionGROUP BY 句
SqlOrderByCollectionORDER BY 句(ページネーション句も付加)
SqlParamCollectionパラメータ(INSERT / UPDATE の値など)

SqlWhereCollection は Add() でチェーンして条件を積み上げます。and: / or: パラメータに別の SqlWhereCollection を渡すことで、(A AND B) OR (C AND D) のようなネストした条件も表現できます。

csharp
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 の文法どおりの順に組み立てられます。

csharp
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(...);
}

SqlSelect.cs#L334-L380

エントリポイント:Rds.SelectIssues / Rds.SelectResults ​

SQL 作成のエントリポイントは Rds クラスの SelectIssues / SelectResults です。通常・履歴・削除済みの各テーブル名を持った SqlSelect を返します。

csharp
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,
    };
}

Rds.cs#L6921-L6961

SelectResults も同じシグネチャで、TableBracket = "\"Results\"" が異なるだけです。

テーブルタイプ(Sqls.TableTypes) ​

値FROM の対象使用場面
Normal"Issues"通常の一覧・カレンダーなど
History"Issues_history"変更履歴表示
Deleted"Issues_deleted"ゴミ箱表示
NormalAndHistory"Issues" UNION ALL "Issues_history"変更履歴を含めた一覧(履歴表示 ON 時)
csharp
tableType: tableType ?? ((view.ShowHistory == true)
    ? Sqls.TableTypes.NormalAndHistory
    : ss.TableType),

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

ss.Join() による JOIN 句の自動生成 ​

ss.Join() は、IJoin インタフェースを実装する SqlColumnCollection・SqlWhereCollection・SqlOrderByCollection に JoinTableNames() を呼び出して参照テーブル名を収集し、重複排除して SqlJoin に変換します。

csharp
public SqlJoinCollection Join(Context context, params IJoin[] join)
{
    return SqlJoinCollection(
        context: context,
        tableNames: join
            .Where(o => o != null)
            .SelectMany(o => o.JoinTableNames())  // 各コレクションが参照するテーブル名を収集
            .Distinct()
            .ToList());
}

SiteSettings.cs#L5520-L5533

状況追加される JOIN
タイトル列を SELECT または ORDER BYItems テーブルへの INNER JOIN
結合サイトの列を SELECTリンク先テーブルへの LEFT OUTER JOIN
ユーザー列の WHERE特に追加 JOIN なし(外部キー参照のみ)

結合サイトの列(ClassA~200,Title のような列名)の読み方と、経路ごとの JOIN 句の組み立ては リンク先の列(チルダ構文)と JOIN の生成 にまとめています。

Items テーブルへの JOIN には、最新バージョンのレコードのみを取得するための相関サブクエリが自動付与されます。このため、複数バージョンがあっても最新の Title だけが JOIN されます。

sql
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 ​

処理フロー ​

text
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

IssueUtilities.Index() はセッションからビュー設定を読み取り(Views.GetBySession())、GetGridData() に渡します。GetGridData() は GridData コンストラクタを呼ぶだけで、コンストラクタ内で Get() が自動的に呼ばれます(IssueUtilities.cs#L31-L57、GridData.cs#L26-L55)。1 ページの件数はサイト設定の GridPageSize です。

実際の SQL 組み立てはすべて GridData.Get() の中で行われます。

図を読み込み中…

csharp
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());
}

GridData.cs#L55-L150

INFO

データ取得と件数取得の 2 本の SQL は Repository.ExecuteDataSet() に配列で渡され、1 回の DB ラウンドトリップで同時実行されます。

SELECT 列の決定(ss.GetGridColumns()) ​

サイト設定の「一覧に表示する項目」(GridColumns)から SELECT 句に使う列一覧を決めます。Ver 列は変更履歴表示の判定に使うため必ず含められます(GridData.cs#L70-L92)。

csharp
// 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 条件を積み上げます。

csharp
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;
}

View.cs#L1838-L1884

定型フィルタ(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 を基準に日付が埋め込まれます。

csharp
// 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 の場合の例です。

sql
"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 検索になります。

csharp
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
}

「プリザンター」で検索した場合の条件の例です。

sql
(
    "Items"."Title" like N'%プリザンター%'
    OR "Issues"."Body" like N'%プリザンター%'
    OR "Issues"."ClassA" like N'%プリザンター%'
    ...
)
sql
(
    "Items"."Title" like '%プリザンター%'
    OR "Issues"."Body" like '%プリザンター%'
    OR "Issues"."ClassA" like '%プリザンター%'
    ...
)
sql
(
    "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 条件を組み立てます。

csharp
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;
}

View.cs#L3324-L3374

  • ソート設定をしていなくても、末尾に UpdatedTime DESC, <ID 列> DESC が必ず付加されます。これにより同一条件での再現性のあるページネーションが保証されます
  • タイトル列(Title / ItemTitle)でソートすると、Items テーブルの Title 列を参照する ORDER BY になり、Items への JOIN が自動で追加されます

「ステータス昇順」でソートした場合の例です。

sql
ORDER BY
    "Issues"."Status" ASC,
    "Issues"."UpdatedTime" DESC,  -- デフォルト追加
    "Issues"."IssueId" DESC        -- デフォルト追加
OFFSET 0 ROWS FETCH NEXT 50 ROWS ONLY
sql
ORDER BY
    "Issues"."Status" ASC,
    "Issues"."UpdatedTime" DESC,  -- デフォルト追加
    "Issues"."IssueId" DESC        -- デフォルト追加
OFFSET 0 ROWS FETCH NEXT 50 ROWS ONLY
sql
ORDER 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 件の場合のイメージです。

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
    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 句)
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
    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 句)
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
    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 BYColumnSorterHash + 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 ​

処理フロー ​

text
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 Links

INFO

new IssueModel(context, ss, issueId: X) のコンストラクタは内部で自動的に Get() を呼び出し、指定した issueId のレコードを SELECT します。コンストラクタを呼ぶだけでレコードが取得済みになります。

レコード取得(SELECT) ​

編集画面・詳細画面でレコードを 1 件取得するのが IssueModel.Get() です。

csharp
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;
}

IssueModel.cs#L864-L915

WHERE 句の基本条件は Rds.IssuesWhereDefault() が生成する SiteId + IssueId です。

sql
WHERE "Issues"."SiteId" = @SiteId_0
  AND "Issues"."IssueId" = @IssueId_0

URL に ver クエリパラメータが含まれる場合(例:/items/{id}/edit?ver=3)は、NormalAndHistory(通常 + 履歴の UNION ALL)を対象に、指定バージョンで絞り込んでクエリが実行されます。これにより過去バージョンの内容を参照できます。

csharp
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);  // 通常テーブル
}

IssueModel.cs#L757-L773

項目一覧画面編集画面
SELECT 列GridColumns(一覧表示列)EditorColumns(編集表示列)
WHERE 条件フィルタ・権限 + SiteIdSiteId + IssueId のみ
ORDER BYColumnSorterHash + デフォルト通常なし
ページネーションあり(OFFSET … FETCH NEXT)なし
件数取得 SQLあり(SELECT COUNT)なし

新規画面 ​

新規作成画面は基本的に SQL を発行しません(空の IssueModel を使用)。ただし別レコードからのコピー作成(CopyFrom パラメータ指定時、かつ ss.AllowReferenceCopy == true)では、コピー元レコードを IssueModel のコンストラクタ経由で SELECT し、読み取り権限を確認したうえで CopyAndInit() で値を複写します。

レコード新規作成(INSERT) ​

新規保存時は IssueModel.Create() → CreateStatements() で INSERT 文を生成します。

csharp
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;
}

IssueModel.cs#L1809-L1876

sql
-- 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, ...)
sql
-- 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, ...)
sql
-- 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
②INSERTItems
③INSERTIssues
④INSERTLinks
⑤UPDATE / INSERTBinaries(添付ファイル)
⑥INSERTPermissions(行レベル権限設定時)

これらは Repository.ExecuteScalar_response() でトランザクション実行され、採番した ID が返ります。

レコード更新(UPDATE) ​

更新時は IssueModel.Update() → UpdateStatements() で SQL を積み上げます。

csharp
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;
}

IssueModel.cs#L2108-L2181

③ の内側の UpdateStatements() は、Rds.UpdateIssues() に続けて new SqlStatement() { IfConflicted = true, Id = IssueId } を積みます。

sql
-- バージョンアップ時のみ: 現行レコードを 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  -- 楽観的排他制御
sql
-- バージョンアップ時のみ: 現行レコードを 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  -- 楽観的排他制御
sql
-- バージョンアップ時のみ: 現行レコードを 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バージョンアップ時のみ
③UPDATEIssues常に
③UPDATEItems(タイトル更新)タイトル変更時
④UPDATE / INSERT / DELETEBinaries添付ファイル変更時
⑤UPDATE / INSERTPermissions行レベル権限設定時

Items テーブルの自動更新 ​

UPDATE 後、UpdateRelatedRecordsStatements() で Issues とは別に Items テーブルも更新されます。タイトル変更時だけでなく、全文検索索引の更新のために毎回実行されます。

csharp
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),
        ...));
    ...
}

IssueModel.cs#L2317-L2340

sql
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 と一致するレコード
sql
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 と一致するレコード
sql
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(選択肢ラベルに変換済み)を保持します。

関連ページ ​

変更履歴

第6版記事の確認版を繰り返す表現を整理する
第5版内部 SQL の採番・更新日時・ページ取得をDBMSごとに説明
第4版リンク項目の列指定と JOIN の組み立て、一覧のスクロール読み込みの解説と、一覧・カレンダー・サイトメニューまわりの改修・設計メモを追加
第3版「内部実装を読む」を 1.5.8.1 のソースで検証して修正
第2版記事のファイル名に並び順の番号を付け、元記事リンクを frontmatter の sources に移行
第1版「内部実装を読む」に検索・内部 SQL・CRUD・タイムゾーン・開発環境を追加