Skip to content

複数テーブルの統合ビュー ​

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

一覧画面は 1 つのサイト(テーブル)に 1 つです。複数のテーブルのレコードを 1 つの一覧にまとめて見る標準機能はありません。このページは、複数テーブルを横断する一覧を新しいサイト種別として足す改修の設計メモです。本体の標準機能ではありません。

要件 ​

要件内容
統合一覧複数の記録テーブル・期限付きテーブルのレコードを 1 つの一覧に出す
新しいサイト種別ReferenceType = 'IntegratedView'
JSON 定義モード(標準)元のテーブル・列・結合方法を JSON で書く
拡張 SQL モード(上級者向け)拡張 SQL の結果をそのまま出す。JSON 定義モードとはどちらか一方
2 段階のフィルタ・ソート元のテーブルごと(結合前)と、まとめた結果(結合後)
既存の UI結合後のフィルタ・ソートは、通常の一覧と同じヘッダ・フィルター領域で操作する
編集各行から元のテーブルの編集画面へ移動でき、ダイアログでも編集できる
権限元のテーブルごとに、ユーザーの権限の条件を自動で入れる(JSON 定義モード)
性能目標 500ms 以下。件数上限の必須化・結合前のフィルタ・元テーブル数の上限で抑える

前提にした現行実装(1.5.8.1) ​

近い既存機能 ​

機能できること足りないこと
ダッシュボードの一覧パーツ(DashboardPart.IndexSites)複数サイトの一覧を並べて出す1 つの一覧にはまとまらない。列はそれぞれのサイトの一覧設定のまま
リンク先の列(ClassA~200,Title)リンクしたテーブルの列を JOIN して出すサイトのリンク定義に沿った JOIN だけで、任意の条件では結合できない
拡張 SQLOnSelectingColumn・OnSelectingWhere・OnSelectingOrderBy などで一覧の SQL に手を入れるFROM 句(どのテーブルを読むか)は変えられない

リンク先の列の仕組みは リンク先の列(チルダ構文)と JOIN の生成 にまとめています。

一覧のデータ取得 ​

GridData.Get() は、一覧の列から SELECT 列を作り、View.Where()・View.OrderBy() で WHERE・ORDER BY を作り、ss.Join() で JOIN を足して Rds.Select() を実行します(GridData.cs)。View.Where() の中で Permissions.SetPermissionsWhere() が権限の条件を足します。詳しくは 内部で動く SQL 文 を参照してください。

フィルタ・ソートの状態は View の ColumnFilterHash(列名 → 値)、ColumnFilterSearchTypes(列名 → 検索の種類)、ColumnSorterHash(列名 → 昇順・降順)に入ります。

一覧でのダイアログ編集は SiteSettings.GridEditorType(None = 0・Grid = 10・Dialog = 20)の Dialog です(SiteSettings.cs)。

データアクセス層 ​

本体は EF Core・Dapper・SqlKata などを使わず、独自の Rds 層(Implem.IRds のインターフェースと、SQL Server・PostgreSQL・MySQL の実装)で ADO.NET を直接使っています。1.5.8.1 で参照しているドライバは Microsoft.Data.SqlClient 7.0.2、Npgsql 10.0.3、MySqlConnector 2.6.1 です。SQL は Implem.Libraries/DataSources/SqlServer/ の SqlSelect・SqlWhere・SqlOrderBy・SqlJoin などの部品で組み立てます。

クエリの組み立て方の選択 ​

候補長所短所
EF CoreLINQ で型安全Rds 層と共存しにくく、DbContext の導入が大きい
SqlKataUNION・JOIN を組め、DB の方言も変換する依存が増え、Rds 層と二重管理になる
Dapper軽いクエリの組み立ては手書き
Rds 層を拡張(推奨)依存が増えない。3 つの DB の方言に対応済み。View.Where()・View.OrderBy() の結果をそのまま使えるUNION の部品が無いので足す必要がある

UNION ALL 用の部品(例: SqlUnion)を足し、既存の ISqlCommandText の方式で DB ごとの違いを吸収します。クエリが複雑になってきたら SqlKata をあらためて検討します。

設計 ​

データソースのモード ​

モード取得フィルタ・ソート用途
JSON 定義JSON の定義から SQL を組み立てる2 段階で使える標準
拡張 SQL拡張 SQL の CommandText を実行する使えない(SQL の中で書く)複雑な SQL が要るとき

1 つの統合ビューで使えるのはどちらか一方です。設定画面で「データソース」を切り替え、選ばなかった方の設定は無視します。件数上限 Limit は両方で使います。

2 段階のフィルタ・ソート(JSON 定義モード) ​

図を読み込み中…

段階対象フィルタ・ソートの列設定する場所
1(ソース別)元のテーブル元のテーブルの列JSON 定義に保存
2(統合)まとめた結果As で付けた別名一覧のヘッダ・フィルター領域。状態はビューのセッションに保存(既存の仕組み)

どちらも既存の ColumnFilterHash・ColumnSorterHash と同じ形にして、View.Where()・View.OrderBy() を使います。

JSON 定義 ​

json
{
    "Title": "課題・成果物統合ビュー",
    "DataSourceType": "Json",
    "Sources": [
        {
            "SiteId": 100,
            "Alias": "a",
            "Editable": true,
            "Columns": [
                { "Name": "IssueId", "As": "Id" },
                { "Name": "Title" },
                { "Name": "Status" },
                { "Name": "ClassA", "As": "Category" }
            ],
            "ColumnFilterHash": { "Status": "[\"100\",\"200\",\"300\"]" },
            "ColumnSorterHash": { "UpdatedTime": "desc" }
        },
        {
            "SiteId": 200,
            "Alias": "b",
            "Editable": false,
            "Columns": [
                { "Name": "ResultId", "As": "Id" },
                { "Name": "Title" },
                { "Name": "Status" },
                { "Name": "ClassA", "As": "Category" }
            ],
            "ColumnFilterHash": { "Status": "[\"100\",\"200\"]" },
            "ColumnSorterHash": {}
        }
    ],
    "CombineType": "UnionAll",
    "JoinCondition": null,
    "IntegratedFilterHash": { "Category": "インフラ" },
    "IntegratedSorterHash": { "Title": "asc" },
    "Limit": 100
}
プロパティ型内容
Titlestring表示名
DataSourceTypestringJson(既定)または ExtendedSql
Sources配列元のテーブル(JSON 定義モードだけ)
Sources[].SiteIdlongサイト ID
Sources[].AliasstringJOIN で使う別名
Sources[].Editablebool編集画面への移動を許すか(既定 false)
Sources[].Columns[].Name / Asstring列名と、まとめた結果での別名
Sources[].ColumnFilterHash / ColumnSorterHash辞書段階 1 のフィルタ・ソート
CombineTypestringUnionAll・InnerJoin・LeftOuterJoin
JoinConditionstringJOIN の条件(例: a.ClassA = b.ResultId)
ExtendedSqlNamestring拡張 SQL の名前(拡張 SQL モードだけ)
IntegratedFilterHash / IntegratedSorterHash辞書段階 2 のフィルタ・ソート(JSON 定義モードだけ)
Limitint件数の上限(両モード)

UNION ALL の例は、次の SQL と同じ意味です。

sql
(SELECT IssueId AS Id, Title, Status, ClassA AS Category
   FROM Issues
  WHERE /* 権限の条件 */ AND Status IN (100, 200, 300))
UNION ALL
(SELECT ResultId AS Id, Title, Status, ClassA AS Category
   FROM Results
  WHERE /* 権限の条件 */ AND Status IN (100, 200))
-- 段階 2
ORDER BY Title ASC

JOIN の場合は CombineType を InnerJoin などにし、JoinCondition に a.ClassA = b.ResultId のように別名付きで条件を書きます。

拡張 SQL モードは次のように書き、ExtendedSqlName で 拡張 SQL の定義を指します。

json
{
    "Title": "カスタム SQL 統合ビュー",
    "DataSourceType": "ExtendedSql",
    "ExtendedSqlName": "IntegratedView_CustomQuery",
    "Limit": 200
}

保存先とクラス ​

SiteSettings に IntegratedViews(List<IntegratedView>)を足して定義を持ちます。JSON は本体標準の Newtonsoft.Json で読みます。

図を読み込み中…

UNION ALL の組み立て(案)
csharp
public SqlStatement ToUnionSqlStatement(Context context)
{
    var statements = new List<SqlStatement>();
    foreach (var source in Sources)
    {
        var ss = SiteSettingsUtilities.Get(context: context, siteId: source.SiteId);
        var column = new SqlColumnCollection();
        source.Columns.ForEach(col => column.Add(new SqlColumn(
            columnBracket: $"\"{col.Name}\"",
            tableName: ss.ReferenceType,
            _as: col.As ?? col.Name)));
        // 編集画面へのリンク用の隠し列
        column.Add(new SqlColumn(
            columnBracket: "\"SiteId\"", tableName: ss.ReferenceType, _as: "_SourceSiteId"));
        column.Add(new SqlColumn(
            columnBracket: $"\"{Rds.IdColumn(ss.ReferenceType)}\"",
            tableName: ss.ReferenceType,
            _as: "_SourceId"));
        // 段階 1: 権限の条件とソース別のフィルタ・ソート
        var sourceView = new View
        {
            ColumnFilterHash = source.ColumnFilterHash,
            ColumnSorterHash = source.ColumnSorterHash
        };
        var where = sourceView.Where(context: context, ss: ss);
        statements.Add(Rds.Select(
            tableName: ss.ReferenceType,
            column: column,
            where: where,
            orderBy: sourceView.OrderBy(context: context, ss: ss)));
    }
    // 段階 2 は UNION ALL の結果に対して IntegratedFilterHash / IntegratedSorterHash を当てる
    return Rds.Union(statements); // Rds.Union は追加する部品
}

View.Where() は既定で権限の条件(SetPermissionsWhere)も足すので、別に呼ぶ必要はありません。

一覧と編集 ​

一覧は既存の一覧の HTML の構造を流用し、元のテーブル名の列と操作の列を足します。

html
<tr data-source-site-id="100" data-source-id="1">
  <td class="iv-source"><span class="iv-badge issues">課題管理</span></td>
  <td>サーバ移行作業</td>
  <td>進行中</td>
  <td class="iv-actions">
    <a href="/items/1/edit" class="iv-edit-link" title="編集画面を開く"></a>
    <button class="iv-edit-modal" data-site-id="100" data-id="1" title="ダイアログで編集"></button>
  </td>
</tr>
  • 編集のリンクとボタンは Editable: true のソースの行にだけ出します。URL は _SourceId から /items/{id}/edit を作ります。/items/{id} はレコード ID から ItemModel がサイトを解決するので、元のテーブルの編集画面が開きます。
  • さらに行ごとに更新権限を見て、権限が無ければ出しません。
  • ダイアログは既存の #EditorDialog($p.openEditorDialog)の仕組みを流用し、閉じたら一覧を読み直します(grid.js)。

実装方式の比較 ​

観点A: サーバースクリプト + 拡張 SQLB: 新しいサイト種別(推奨)C: ダッシュボードのパーツ
仕組み拡張 SQL の CommandText に UNION ALL を書くReferenceType = 'IntegratedView' と専用の設定タブDashboardPartType に統合ビューを足す
実装の手間◎ 小さい○ 中〜大○ 中
保守△ パラメータファイルが散らばる◎ サイト設定にまとまる○ ダッシュボードにまとまる
操作△ JSON・SQL の手書き◎ 専用 UI と既存のヘッダ○ ダッシュボードの UI
権限△ 自前で書く◎ 既存の仕組み○ 既存の仕組み
フィルタ・ソート△ SQL に書く◎ 2 段階 + 既存のヘッダ○ 2 段階
拡張性◎ SQL の自由度◎ 独立した種別なので足しやすい○

B を推奨します。通常のテーブルと同じ並びにサイトとして置け、一覧のフィルタ・ソートの UI をそのまま使え、統合ビュー自体の閲覧・編集の権限も既存の仕組みで管理できます。最初は UNION ALL だけ、次に JOIN、そのあとエクスポート・API と段階的に足せます。

ファイル内容
Libraries/Settings/SiteSettings.csIntegratedViews と関連の処理
Models/Sites/SiteUtilities.csIntegratedView 種別の一覧画面・設定画面
Models/Items/ItemModel.csIntegratedView の振り分け
新規 Libraries/Settings/IntegratedView.cs・IntegratedViewSource.cs定義のクラス
新規 Models/IntegratedViews/IntegratedViewUtilities.cs一覧の HTML(既存のヘッダを流用)
新規 wwwroot/src/scripts/generals/ のスクリプトダイアログ編集

性能 ​

対策優先度内容
件数上限の必須化必須Limit が無ければ GridPageSize(未設定ならパラメータの GridPageSize、既定 20)を使い、上限なしの SQL を出さない
結合前のフィルタ必須ソース別のフィルタで結合前に件数を減らす。設定画面で勧める
元テーブル数の上限必須推奨 5、最大 10 を超えたら保存時にエラー
ページ分け高OFFSET ... FETCH NEXT(SQL Server)・LIMIT ... OFFSET(PostgreSQL・MySQL)で分けて取る
JOIN のキーの索引高JOIN の条件にする列の索引を勧める
Ajax での読み込み・短いキャッシュ中〜低必要なら
csharp
public int EffectiveLimit(SiteSettings ss)
{
    return Limit ?? ss.GridPageSize ?? Parameters.General.GridPageSize;
}

重いのは、結合前のフィルタが無く索引も効かない全件の読み込み、UNION ALL のあとの ORDER BY(件数に比例する並べ替え)、JOIN のキーに索引が無い場合、表示行数が多い場合の HTML です。実装時は、クエリ 500ms 以下・応答全体 1,000ms 以下を目安に、実行計画に警告が無いことを確かめます。

安全面 ​

観点対策
SQL インジェクションJSON の値は Rds 層の部品を通してパラメータ化する。JSON から生の SQL は実行しない。JoinCondition は「別名.列名 = 別名.列名」の形だけ許し、関数・サブクエリ・;・コメントは拒否する
テナントSiteSettingsUtilities.Get() でテナントを確かめ、別テナントのサイトは拒否する
サイトの読み取りソースごとに読み取り権限を確かめ、無ければそのソースを飛ばす
行の権限ソースごとに SetPermissionsWhere() の条件を入れる
列Columns[].Name は各ソースの列定義(ColumnDefinitionHash)にあるものだけ。非表示の列や、列の閲覧権限がある列はまとめた結果でも同じ扱いにする
編集Editable に加え、行ごとに更新権限を確かめる
定義の変更サイトの管理権限を持つユーザーだけ
JSON の読み込みTypeNameHandling.None のまま。定義の大きさ(例: 64KB)とソースの数に上限。不明なプロパティは無視
内部の列_SourceSiteId・_SourceId は表示せず data-* に入れる
XSS・CSRF値の出力は HtmlBuilder でエスケープする。送信は既存のトークンの仕組みを使う

拡張 SQL モード

拡張 SQL モードでは、管理者が書いた SQL がそのまま実行され、権限の条件は自動では入りません。拡張 SQL はパラメータファイルに置くので、設定できるのはサーバーのファイルを触れる管理者だけです。権限の条件は SQL の中に自分で書く必要があります。

既存機能との関係 ​

項目扱い
既存のサイト種別影響なし
フィルタの保存ソース別は JSON 定義、統合は既存のビューのセッション(JSON 定義モードだけ)
エクスポート・APIあとで対応

関連ページ ​

変更履歴

第1版リンク項目の列指定と JOIN の組み立て、一覧のスクロール読み込みの解説と、一覧・カレンダー・サイトメニューまわりの改修・設計メモを追加