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?

プリザンターのCodeDefinerコード自動生成を解剖する[第5回:RepeatType別ハンドラ]

0
Posted at

はじめに

前回ではコードマージシステムを解説しました。今回は、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 ハンドラは、プリザンターの各テーブル(DeptsGroupsUsersSitesIssuesResultsWikis など)を繰り返し単位としてコードを生成します。

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 ハンドラ

BaseModelBaseItemModel は、全テーブルに共通するカラムのコードを生成します。

// 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 ハンドラ はテーブルレベルのフィルタリング(ItemOnlyGenericUi 等)を行う
  • Column ハンドラ は最も多くのフィルタ条件を持ち、型・キー・UI属性などで細かく制御する
  • BaseModel / BaseItemModel は共通カラムのコード生成を担い、EachModel フラグで個別化を制御する
  • DefinitionFile / Row / Column は定義メタ情報のコード化に使われる
  • ハンドラは ネスト可能で、Table → Column のような多重ループが実現される

次回(最終回)は、各ハンドラ内で行われるプレースホルダー置換と型変換のロジック(Converts.csReservedWords.cs)を解説します。

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?