はじめに
前回ではコードマージシステムを解説しました。今回は、RepeatType によって呼び分けられる各ハンドラクラス(Parts 名前空間)の実装を詳しく見ていきます。各ハンドラがどのような繰り返しロジックとフィルタリングを行っているかを理解することで、コード自動生成の全貌が見えてきます。
本連載で参照するソースコードはすべて Implem/Implem.Pleasanter リポジトリのものです。リンクはコミットハッシュベースのパーマリンクを使用しています。
| 回 | テーマ |
|---|---|
| 第1回 | 全体像:エントリポイント・コマンド体系・初期化フロー |
| 第2回 | 定義ファイルとテンプレートシステム |
| 第3回 | テンプレート展開エンジン |
| 第4回 | コードマージシステム |
| 第5回(本記事) | RepeatType別のコード生成ハンドラ |
| 第6回 | プレースホルダー置換と型変換 |
RepeatType と Parts クラスの対応
第3回で見た Creators.Create() の switch 文では、RepeatType に応じて対応する Parts クラスのハンドラが呼び出されます。
| RepeatType | ハンドラクラス | ソースファイル |
|---|---|---|
Table |
Table |
Parts/Table.cs |
Column |
Column |
Parts/Column.cs |
BaseModel |
BaseModel |
Parts/BaseModel.cs |
BaseItemModel |
BaseItemModel |
Parts/BaseItemModel.cs |
Join |
Join |
Parts/Join.cs |
Form |
Form |
Parts/Form.cs |
Display |
Display |
Parts/Display.cs |
DefinitionFile |
DefinitionFile |
Parts/DefinitionFile.cs |
DefinitionRow |
DefinitionRow |
Parts/DefinitionRow.cs |
DefinitionColumn |
DefinitionColumn |
Parts/DefinitionColumn.cs |
すべてのハンドラは共通のパターンに従っています。
internal static void SetCodeCollection(
CodeDefinition codeDefinition,
List<string> codeCollection,
DataContainer dataContainer)
{
var code = string.Empty;
// 1. 繰り返し対象のコレクションを取得
// 2. フィルタリング
// 3. 各要素に対して Creators.SetCodeCollection() を呼び出し
// - テンプレートを再帰展開
// - 値プレースホルダーを置換
// - 結果を codeCollection に追加
}
Table ハンドラ
Table ハンドラは、プリザンターの各テーブル(Depts、Groups、Users、Sites、Issues、Results、Wikis など)を繰り返し単位としてコードを生成します。
internal static void SetCodeCollection(
CodeDefinition codeDefinition,
List<string> codeCollection,
DataContainer dataContainer)
{
dataContainer.Type = "Table";
var code = string.Empty;
var parentTableName = dataContainer.TableName;
var parentModelName = dataContainer.ModelName;
Def.TableNameCollection(order: codeDefinition.Order)
.Where(o => !CheckExclude(codeDefinition, o))
.ForEach(tableName =>
{
dataContainer.TableName = tableName;
dataContainer.ModelName = Def.ModelNameByTableName(tableName);
Creators.SetCodeCollection(
ref code, codeCollection, codeDefinition, dataContainer,
() => ReplaceCode(ref code, codeDefinition, dataContainer));
});
dataContainer.TableName = parentTableName;
dataContainer.ModelName = parentModelName;
}
Table.CheckExclude:テーブルレベルのフィルタリング
internal static bool CheckExclude(CodeDefinition codeDefinition, string tableName)
{
var columns = Def.ColumnDefinitionCollection
.Where(o => o.TableName == tableName).ToList();
if (codeDefinition.ItemOnly && !columns.Any(o => o.ItemId > 0)) return true;
if (codeDefinition.NotItem && columns.Any(o => o.ItemId > 0)) return true;
if (codeDefinition.GenericUi && !columns.Any(o => o.GenericUi)) return true;
if (codeDefinition.HasIdentity && !columns.Any(o => o.Identity)) return true;
if (codeDefinition.HasTableNameId && tableName.CsTypeIdColumn().IsNullOrEmpty()) return true;
if (codeDefinition.Exclude.Split(',').Contains(tableName)) return true;
if (!codeDefinition.Include.IsNullOrEmpty()
&& !codeDefinition.Include.Split(',').Contains(tableName)) return true;
return false;
}
フィルタ条件はすべて除外判定(true を返すと対象外)です。主なフィルタの意味を表にまとめます。
| フィルタ | 条件 | 対象テーブルの例 |
|---|---|---|
ItemOnly |
ItemId > 0 のカラムを持つテーブルのみ |
Issues, Results, Wikis |
NotItem |
ItemId > 0 のカラムを持たないテーブルのみ |
Depts, Groups, Users |
GenericUi |
GenericUi カラムを持つテーブルのみ |
Issues, Results |
HasIdentity |
IDENTITY 列を持つテーブルのみ | ほぼすべて |
HasTableNameId |
テーブル名+Id のカラムがあるテーブルのみ | Users(UserId), Issues(IssueId) |
Exclude |
カンマ区切りで指定されたテーブルを除外 | 定義で指定 |
Include |
カンマ区切りで指定されたテーブルのみ対象 | 定義で指定 |
Table.ReplaceCode:テーブル名の値置換
private static void ReplaceCode(
ref string code, CodeDefinition codeDefinition, DataContainer dataContainer)
{
foreach (var placeholder in code.RegexValues(CodePatterns.ReplacementPlaceholder))
{
switch (placeholder)
{
case "ModelName":
code = code.Replace("#ModelName#", dataContainer.ModelName);
break;
case "modelName":
code = code.Replace("#modelName#", dataContainer.ModelName.ToLowerFirstChar());
break;
case "modelname":
code = code.Replace("#modelname#", dataContainer.ModelName.ToLower());
break;
case "TableName":
code = code.Replace("#TableName#", dataContainer.TableName);
break;
case "tableName":
code = code.Replace("#tableName#", dataContainer.TableName.ToLowerFirstChar());
break;
case "tablename":
code = code.Replace("#tablename#", dataContainer.TableName.ToLower());
break;
}
}
// ReplaceOld / ReplaceNew の適用
if (!codeDefinition.ReplaceOld.IsNullOrEmpty())
{
code = code.Replace(codeDefinition.ReplaceOld, codeDefinition.ReplaceNew);
}
}
大文字・小文字のバリエーションで6種類のプレースホルダーが提供されています。
| プレースホルダー | 変換 | 例(Issues テーブル) |
|---|---|---|
#ModelName# |
そのまま | Issue |
#modelName# |
先頭を小文字 | issue |
#modelname# |
すべて小文字 | issue |
#TableName# |
そのまま | Issues |
#tableName# |
先頭を小文字 | issues |
#tablename# |
すべて小文字 | issues |
Column ハンドラ
Column ハンドラは、特定テーブルの各カラムを繰り返し単位としてコードを生成します。テーブルハンドラの内側で呼ばれることが多く、テーブル×カラムの2重ループ構造になります。
internal static void SetCodeCollection(
CodeDefinition codeDefinition,
List<string> codeCollection,
DataContainer dataContainer)
{
var code = string.Empty;
var columnCollection = Def.ColumnDefinitionCollection
.Where(o => o.TableName == dataContainer.TableName)
.Where(o => !CheckExclude(codeDefinition, o))
.OrderBy(o => o[Strings.CoalesceEmpty(codeDefinition.Order, "No")])
.ToList();
var count = columnCollection.Count();
columnCollection.ForEach(columnDefinition =>
{
dataContainer.ColumnName = columnDefinition.ColumnName;
Creators.SetCodeCollection(
ref code, codeCollection, codeDefinition, dataContainer,
() => ReplaceCode(ref code, codeDefinition, columnDefinition, count));
dataContainer.ColumnName = string.Empty;
});
}
Column.CheckExclude:カラムレベルのフィルタリング
Column.CheckExclude() は CodeDefiner 中で最も多くのフィルタ条件を持つメソッドです。主要な条件を分類して紹介します。
型に関するフィルタ
| フィルタ | 条件 |
|---|---|
IncludeTypeName |
DB型名が指定リストに含まれる |
ExcludeTypeName |
DB型名が指定リストに含まれない |
IncludeTypeCs |
C#型名が指定リストに含まれる |
ExcludeTypeCs |
C#型名が指定リストに含まれない |
キーに関するフィルタ
| フィルタ | 条件 |
|---|---|
Pk / NotPk
|
主キーである / でない |
Identity / NotIdentity
|
IDENTITY列である / でない |
Unique / NotUnique
|
ユニーク制約がある / ない |
IdentityOrPk |
IDENTITY か PK の少なくとも一方 |
UI に関するフィルタ
| フィルタ | 条件 |
|---|---|
Form |
フォーム表示対象 |
Select |
SELECT対象 |
Update |
UPDATE対象 |
GridColumn |
一覧表示対象 |
FilterColumn |
フィルタ表示対象 |
EditorColumn |
エディタ表示対象 |
TitleColumn |
タイトル表示対象 |
その他のフィルタ
| フィルタ | 条件 |
|---|---|
Calc / NotCalc
|
計算式カラムである / でない |
Join / NotJoin
|
JOINカラムである / でない |
History / PkHistory
|
履歴対象 / PK履歴対象 |
Class / NotClass
|
クラス型カラム / でない |
Null / NotNull
|
NULL許容 / 非NULL |
Session |
セッション保存対象 |
NotBase |
ベースモデルのカラムでない |
NotBase フィルタの特殊なロジック
NotBase フィルタは、ベースモデル(全テーブル共通)に含まれるカラムを除外するために使われます。ただし、EachModel フラグが立っているカラムは除外しません。
if (codeDefinition.NotBase)
{
if (Def.ItemModelNameCollection().Contains(columnDefinition.ModelName))
{
// Itemモデルの場合: BaseItemColumnに同名カラムがあれば除外
if (Def.ExistsColumnBaseItem(o =>
o.ColumnName == columnDefinition.ColumnName && !o.EachModel))
return true;
}
else
{
// 非Itemモデルの場合: BaseColumnに同名カラムがあれば除外
if (Def.ExistsColumnBase(o =>
o.ColumnName == columnDefinition.ColumnName && !o.EachModel))
return true;
}
}
BaseModel / BaseItemModel ハンドラ
BaseModel と BaseItemModel は、全テーブルに共通するカラムのコードを生成します。
// BaseModel: 全テーブル共通カラム
internal static void SetCodeCollection(...)
{
Def.BaseColumnDefinitionCollection(order: codeDefinition.Order)
.Where(o => !Column.CheckExclude(codeDefinition, o))
.Where(o => !o.EachModel) // EachModel=true は除外
.ForEach(columnDefinition => { ... });
}
// BaseItemModel: Itemテーブル共通カラム
internal static void SetCodeCollection(...)
{
Def.BaseItemColumnDefinitionCollection(order: codeDefinition.Order)
.Where(o => Column.CheckExclude(codeDefinition, o) == false)
.Where(o => !o.EachModel) // EachModel=true は除外
.ForEach(columnDefinition => { ... });
}
EachModel フラグは重要な概念です。ベースカラム定義に含まれるカラムでも、EachModel = true の場合は各テーブル固有のコードとして生成されます。これにより、共通カラムでありながらテーブルごとに異なる実装が必要なケースに対応できます。
Join ハンドラ
Join ハンドラは、テーブル結合に関するコードを生成します。JOINカラム(JoinTableName が設定されているカラム)を対象に繰り返します。
Joinハンドラは特に多くのプレースホルダーを持ちます。
| プレースホルダー | 説明 |
|---|---|
#JoinTableName# |
結合先テーブル名 |
#JoinType# |
結合タイプ(SqlJoin.JoinTypes.Inner 等) |
#JoinExpression# |
結合条件式 |
#TableNameAlias# |
テーブルエイリアス |
#ColumnBracket# |
カラムのブラケット表記 |
#ColumnBrackets# |
計算列を含むカラムブラケット |
JoinType の変換
internal static string JoinType(ColumnDefinition columnDefinition)
{
switch (columnDefinition.JoinType)
{
case "inner join": return "SqlJoin.JoinTypes.Inner";
case "left outer join": return "SqlJoin.JoinTypes.LeftOuter";
case "right outer join":return "SqlJoin.JoinTypes.RightOuter";
default: return string.Empty;
}
}
DefinitionFile / DefinitionRow / DefinitionColumn ハンドラ
これら3つのハンドラは、定義ファイル自体のメタ情報をコードに変換するために使われます。主に Def.cs(定義アクセサ)の自動生成に活用されます。
DefinitionFile ハンドラ
定義ファイルの種類(Code, Column, Template, ViewMode, Demo, Sql)を繰り返し単位とします。
internal static void SetCodeCollection(...)
{
dataContainer.XlsIoCollection.Keys.ForEach(definitionName =>
{
dataContainer.DefinitionName = definitionName;
Creators.SetCodeCollection(ref code, codeCollection, codeDefinition,
dataContainer,
() => ReplaceCode(ref code, codeDefinition, dataContainer));
});
}
| プレースホルダー | 説明 | 例 |
|---|---|---|
#File# |
定義ファイル名 |
Code, Column
|
#file# |
定義ファイル名(先頭小文字) |
code, column
|
#ColumnNames# |
カラム名のリスト | "Id", "Body", ... |
DefinitionRow ハンドラ
定義ファイル内の各行(各定義ID)を繰り返し単位とします。
internal static void SetCodeCollection(...)
{
dataContainer.XlsIoCollection[dataContainer.DefinitionName]
.XlsSheet.AsEnumerable()
.Skip(1) // ヘッダ行をスキップ
.Where(o => o[0].ToString() != string.Empty) // ID が空でない行
.ForEach(definitionRow =>
Creators.SetCodeCollection(ref code, codeCollection, codeDefinition,
dataContainer,
() => code = code.Replace(
"#Id#",
ReservedWords.ValidName(definitionRow[0].ToString()))));
}
ここで ReservedWords.ValidName() が呼ばれ、定義IDが有効なC#識別子に変換されます。
DefinitionColumn ハンドラ
定義ファイルの各カラム(プロパティ)を繰り返し単位とします。
internal static void SetCodeCollection(...)
{
dataContainer.XlsIoCollection[dataContainer.DefinitionName]
.XlsSheet.Columns
.ForEach(definitionColumnName =>
Creators.SetCodeCollection(ref code, codeCollection, codeDefinition,
dataContainer,
() =>
{
var definitionColumnNameAlternate = definitionColumnName
.EscapeReservedWord()
.Replace("-", "_");
var definitionColumnType = dataContainer
.XlsIoCollection[dataContainer.DefinitionName]
.XlsSheet[0][definitionColumnName].ToString();
ReplaceCode(ref code, codeDefinition, ...);
}));
}
| プレースホルダー | 説明 |
|---|---|
#DefColumnName# |
カラム名(エスケープ済み) |
#DefColumnNameOriginal# |
カラム名(元の名前) |
#Type# |
カラムの型 |
#CastType# |
キャスト式 |
#SetDefault# |
デフォルト値の初期化式 |
Form ハンドラ
Form ハンドラは、テーブル内のフォーム定義を繰り返し単位とします。
internal static void SetCodeCollection(...)
{
Def.ColumnDefinitionCollection
.Where(o => o.TableName == dataContainer.TableName)
.Select(o => new { ModelName = o.ModelName, FormName = o.FormName })
.Distinct() // ユニークなフォーム名の組み合わせ
.ForEach(data =>
{
dataContainer.FormName = data.FormName;
Creators.SetCodeCollection(ref code, codeCollection, codeDefinition,
dataContainer,
() => ReplaceCode(ref code, codeDefinition, data.FormName, data.ModelName));
});
}
#FormName# プレースホルダーは、FormName が未設定の場合は ModelName + "Form" に自動展開されます。
Display ハンドラ
Display ハンドラは、多言語対応の表示文字列を繰り返し単位とします。
internal static void SetCodeCollection(...)
{
DisplayAccessor.Displays.DisplayHash
.Select(o => o.Value)
.ForEach(display => display.Languages
.Where(element => !CheckExclude(codeDefinition, display, element))
.ForEach(element =>
Creators.SetCodeCollection(ref code, codeCollection, codeDefinition,
dataContainer,
() => ReplaceCode(ref code, display, element))));
}
| プレースホルダー | 説明 |
|---|---|
#DisplayId# |
表示ID(言語サフィックス付き) |
#DisplayContent# |
表示テキスト |
#DisplayCssClass# |
CSSクラス名 |
#DisplayContentEncoded# |
HTMLエンコード済みテキスト |
特有のフィルタとして DisplayLanguages(多言語エントリを含めるか)と ClientScript(クライアントスクリプト用のエントリか)があります。
ハンドラ間の関係図
各ハンドラの繰り返し階層と、ネストの関係を図にまとめます。
まとめ
第5回では、10種類の RepeatType ハンドラの実装を解説しました。
- すべてのハンドラは 繰り返し → フィルタ → 再帰展開 → 値置換 の共通パターンに従う
-
Table ハンドラ はテーブルレベルのフィルタリング(
ItemOnly、GenericUi等)を行う - Column ハンドラ は最も多くのフィルタ条件を持ち、型・キー・UI属性などで細かく制御する
-
BaseModel / BaseItemModel は共通カラムのコード生成を担い、
EachModelフラグで個別化を制御する - DefinitionFile / Row / Column は定義メタ情報のコード化に使われる
- ハンドラは ネスト可能で、Table → Column のような多重ループが実現される
次回(最終回)は、各ハンドラ内で行われるプレースホルダー置換と型変換のロジック(Converts.cs、ReservedWords.cs)を解説します。