リンク先の列(チルダ構文)と JOIN の生成
分類項目の選択肢に [[200]] のようにサイトを指定してテーブル同士をリンクすると、一覧の列の設定で、利用可能な列の欄のドロップダウン(GridJoin)からテーブルを選び、リンク先(またはリンク元)のテーブルの列を表示・フィルタ・ソートできます。このとき内部の列名は ClassA~200,Title のような形になります。このページでは、この列名(以下チルダ構文)の読み方と、列の解決から SQL の JOIN 句ができるまでを 1.5.8.1 のソースで整理します。
- 列名は「経路(テーブルエイリアス)」と「列名」をカンマでつないだもの。経路は
項目名~サイトIDを-でつなぐ ~は親(自分の項目が相手を指している)方向、~~は子(相手の項目が自分を指している)方向- 使える経路は、サイトのリンク定義から作った一覧(
JoinOptions())にあるものだけ。手で書いた経路は通らない - JOIN はすべて LEFT OUTER JOIN で、1 段ごとに
Itemsと実テーブルの 2 つが足される
リンクの書き方([[]] 形式・JSON 形式)は リンク項目の [[]] 形式と JSON 形式 を参照してください。
SQL 例の読み方
DBMS 名のない SQL は、3 DBMS で共通の構文を使った説明用の抜粋です。プリザンターは MySQL 接続でも ansi_quotes を設定するため、識別子の二重引用符が使えます。DB のコンソールで直接実行するときの引用符・パラメータの扱いは DBMS ごとの SQL の書き方 を参照してください。... を含む例は実行用 SQL ではありません。
列名の構造
{経路},{列名}
経路 = {項目名}~{サイトID} … 親方向
| {項目名}~~{サイトID} … 子方向
| {経路}-{経路} … 多段| 列名の例 | 意味 |
|---|---|
ClassA~200,Title | 自テーブルの ClassA が指すサイト 200 のレコードの Title |
ClassA~~300,Title | サイト 300 のレコードのうち、ClassA が自テーブルのレコードを指しているものの Title |
ClassA~200-ClassB~300,Title | 自テーブル → ClassA でサイト 200 → そのレコードの ClassB でサイト 300、と辿った先の Title |
~ と ~~ の違い
どちらの方向でも、リンクの値(相手のレコード ID)を持っているのは「子」の側です。違うのは、自テーブルが子か親かです。
図を読み込み中…
| 記法 | 方向(内部名) | 経路のサイト ID | JOIN の条件 |
|---|---|---|---|
~ | 親方向(Destinations) | 親のサイト ID | 自テーブルの項目の値 = 相手の Items.ReferenceId |
~~ | 子方向(Sources) | 子のサイト ID | 自テーブルの ID = 相手の項目の値 |
経路の文字列は JoinStack.TableName() が作ります。親方向は {ColumnName}~{DestinationId}、子方向は {ColumnName}~~{SourceId} を段数ぶん - でつなぎます(JoinStack.cs)。
列名の分解(ColumnNameInfo)
列名は ColumnNameInfo で分解されます。カンマを含むかどうかで、結合した列かどうかを決めます(ColumnNameInfo.cs)。
| フィールド | ClassA~200-ClassB~300,Title の場合 |
|---|---|
ColumnName | ClassA~200-ClassB~300,Title(元の文字列) |
TableAlias | ClassA~200-ClassB~300(最初のカンマより前) |
Name | Title(カンマより後) |
SiteId | 300 |
Joined | true |
SiteId は「経路の最後の段の、最後の ~ より後ろ」です(ColumnUtilities.cs)。~~ でも同じ規則なので、ClassA~~300 は 300 になります。
Exists() は、列名がリンク先の列定義にあること、SiteId のサイトが結合対象に含まれていること、経路の各段のサイト ID が数字で、その段の項目名がそのサイトの列定義にあることを確かめます(ColumnNameInfo.cs)。
辿れる経路の決まり方
リンク先のサイト設定の収集
SiteSettings.SetLinkedSiteSettings() が、リンク先(Destinations)とリンク元(Sources)のサイト設定を再帰的に集めます。集めたサイト設定は、サイト ID を鍵にした JoinedSsHash に入ります(最初は自分自身だけが入っています)(SiteSettings.cs)。
再帰の中身は SiteSettingsList() です(SiteSettings.cs)。
- 親方向のサイトから先は親方向だけ、子方向のサイトから先は子方向だけを探します(再帰呼び出しで
destinations: true, sources: false、またはdestinations: false, sources: trueを渡している)。そのため 1 つの経路の中で~と~~は混ぜられません。ClassA~200-ClassB~300やClassA~~100-ClassB~~200は作れますが、ClassA~200-ClassB~~300は作れません。 - すでに辿ったサイト(
previouslyのリスト)は除外されます。これで A → B → A のような循環は起きません。 - 段数を直接制限するパラメータはありません。上限は、同じ方向に辿れるサイトの数で決まります。
- 公開フォーム(
context.IsForm)の処理中は、リンク先が「公開」になっている記録テーブル・期限付きテーブル・Wiki だけが対象です(CanUseFormLinkTarget)。
EnableExpandLinkPath
previously の扱いは EnableExpandLinkPath で変わります。パラメータ General.json の EnableExpandLinkPath(既定 false)と、サイト設定の EnableExpandLinkPath の両方が true のときだけ有効です(SiteSettings.cs)。
| 設定 | previously の扱い | 結果 |
|---|---|---|
| 無効(既定) | 1 つのリストを兄弟の分岐でも共有する | あるサイトへは最初に見つけた 1 経路でしか辿れない |
| 有効 | 分岐ごとにコピーして渡す | 同じサイトに別の経路からも辿れる。同じ経路の中の循環は引き続き防がれる |
選べる経路の一覧(JoinOptions)
JoinOptions() は、集めたリンクから「経路 → 表示名」の辞書を作ります。先頭には自テーブル(鍵は空文字)が入ります。一覧の列の設定のドロップダウン GridJoin の選択肢もこの辞書です(SiteUtilities.cs)(SiteSettings.cs)。表示名は JoinStack.DisplayName() が作り、-< でサイト名をつなぎます(JoinStack.cs)。
| 方向 | 表示名の例(自テーブルがサイト A) |
|---|---|
| 親方向 | [サイトB] -< サイトA |
| 多段の親方向 | [サイトC] -< サイトB -< サイトA |
| 子方向 | サイトA -< [サイトB] |
列の解決(GetColumn)
SiteSettings.GetColumn() は、列名が ColumnHash に無く、カンマを含み、経路が JoinOptions() の鍵にあるときだけ AddJoinedColumn() を呼びます(SiteSettings.cs)。
図を読み込み中…
AddJoinedColumn() はリンク先のサイト設定の列をコピーし、ColumnName をチルダ構文の名前に書き換え、選択肢(ChoiceHash)もリンク先から写して、自テーブルの Columns と ColumnHash に追加します(SiteSettings.cs)。2 回目からは ColumnHash から直接返ります。
列名を手で書いても、サイトのリンク定義から作られた経路(JoinOptions() にあるもの)でなければ null になります。循環する経路や方向を混ぜた経路はそもそも JoinOptions() に入らないので、チルダ構文でこれらを回避することはできません。
INFO
内部で自動的に足す列(リンク先のタイトルなど)は、checkJoinOption: false で JoinOptions() の確認を省いて解決しています(ColumnUtilities.cs)。画面や API から渡した列名は、公開の GetColumn() を通るので確認されます。
暗黙に足される列
一覧の SELECT 列を作るとき、ColumnUtilities.AddDefaultColumns() は経路ごとに ID・SiteId・Locked・タイトル列を足します。さらに、その経路のテーブルが持つリンク項目が表示対象に含まれていれば、リンク先のさらに先のタイトルも足します。たとえば ClassA~200,ClassB を表示していて、サイト 200 の ClassB がサイト 300 を指しているなら、ClassA~200-ClassB~300,Title が追加されます(ColumnUtilities.cs)。リンク項目を ID ではなくタイトルで表示するためです。
SELECT 句での名前
結合した列は、SELECT 句で経路をテーブルの別名として参照し、AS にチルダ構文の列名をそのまま付けます。更新日時も 経路,UpdatedTime という名前で一緒に取ります(Column.cs)。結果セットの列名が ClassA~200,Title になるので、拡張 SQL などで一覧の SQL に手を入れるときはこの名前に注意してください。
JOIN 句の生成
一覧のデータ取得(GridData)は、SELECT 列・WHERE・ORDER BY を作ったあと、それらを全部 ss.Join() に渡します。各コレクションが参照している経路(テーブル名)を集め、重複を除いて JOIN 句にします(SiteSettings.cs)。一覧に表示していない結合列でフィルタやソートをしても、その経路の JOIN は自動で足されます。
- 経路は文字数の短い順に処理します。多段の経路では前の段が先に JOIN されます。
- 同じ JOIN 条件は 1 つにまとめます(
GroupBy(JoinExpression))。 - 最後に、自テーブルと
Itemsの INNER JOIN(ItemJoin)を足します(ItemUtilities.cs)。
経路 1 つぶんの JOIN は、経路を - で分けて 1 段ずつ作ります。段ごとに Items と実テーブルの 2 つを LEFT OUTER JOIN し、~~ を含むかどうかで条件の向きを変えます(SiteSettings.cs)。段の項目がそのサイトで見つからない場合、その段は飛ばされます。
親方向(~)
自テーブルが Results、ClassA~200(サイト 200 は Issues)の場合です。SQL Server のイメージです。
left outer join "Items" as "ClassA~200_Items"
on try_cast("Results"."ClassA" as bigint) = "ClassA~200_Items"."ReferenceId"
and "ClassA~200_Items"."SiteId" = 200
left outer join "Issues" as "ClassA~200"
on "ClassA~200_Items"."ReferenceId" = "ClassA~200"."IssueId"項目の値は bigint に変換してから相手の ID と比べます。変換の書き方は DB ごとに違います(SqlServerCommandText.cs、PostgreSqlCommandText.cs、MySqlCommandText.cs)。
| DB | 変換 |
|---|---|
| SQL Server | try_cast(列 as bigint) |
| PostgreSQL | 数値項目ならそのまま。それ以外は数字だけのときに ::bigint、それ以外は null |
| MySQL | cast(列 as signed) |
リンク先のサイトへの絞り込み(SiteId = 200)は、Items の JOIN 条件に入っています。
子方向(~~)
自テーブルが Results、ClassA~~300(サイト 300 も Results)の場合です。
left outer join "Results" as "ClassA~~300"
on "Results"."ResultId" = try_cast("ClassA~~300"."ClassA" as bigint)
and "ClassA~~300"."SiteId" = 300
left outer join "Items" as "ClassA~~300_Items"
on "ClassA~~300"."ResultId" = "ClassA~~300_Items"."ReferenceId"親方向とは JOIN の順番が逆で、先に実テーブル、あとから Items です。サイトの絞り込み(SiteId = 300)は実テーブルの JOIN 条件に入ります。
多段
ClassA~200-ClassB~300 では、1 段目の別名 ClassA~200 を左側にして 2 段目を JOIN します。別名は経路を先頭からつないだもの(ClassA~200-ClassB~300)です。
-- 1 段目
left outer join "Items" as "ClassA~200_Items"
on try_cast("Results"."ClassA" as bigint) = "ClassA~200_Items"."ReferenceId"
and "ClassA~200_Items"."SiteId" = 200
left outer join "Issues" as "ClassA~200"
on "ClassA~200_Items"."ReferenceId" = "ClassA~200"."IssueId"
-- 2 段目
left outer join "Items" as "ClassA~200-ClassB~300_Items"
on try_cast("ClassA~200"."ClassB" as bigint) = "ClassA~200-ClassB~300_Items"."ReferenceId"
and "ClassA~200-ClassB~300_Items"."SiteId" = 300
left outer join "Results" as "ClassA~200-ClassB~300"
on "ClassA~200-ClassB~300_Items"."ReferenceId" = "ClassA~200-ClassB~300"."ResultId"取得した値の読み取りと権限
GridData.KeyValues()(API などで使う変換)は、列の経路(column.TableName())ごとにモデルを 1 つ作り、tableAlias を渡して DataRow から 経路,列名 の値を読み取ります。列名に ~ を含む列は、リンク先のレコードに読み取り権限が無ければ値を出しません(GridData.cs)。
全体の流れ
図を読み込み中…