CodeDefiner(テーブル作成とコード自動生成)
プリザンターの Models ディレクトリなどにある大量の .cs ファイルは、CodeDefiner が定義ファイル(JSON)とテンプレート(_Body.txt)から自動生成したものです。このページでは、CodeDefiner のコマンド体系、定義ファイルの構造、テンプレート展開エンジン、コードマージ、RepeatType 別ハンドラ、プレースホルダー置換と型変換、そして複数 RDBMS 向けのデータベースの作成・移行とパラメータの引き継ぎ(merge)の仕組みをまとめます。
基本の仕組みは テンプレート + プレースホルダー置換 + 再帰展開 の組み合わせです。
INFO
ソースへのリンクは Implem/Implem.Pleasanter のコミット固定のパーマリンクです。コード自動生成部分はコミット 203cac8 を参照しています。コマンド・オプション、データベースの作成・更新、merge の節は 1.5.8.1(コミット fdcbb3f)です。
全体像
CodeDefiner の役割
CodeDefiner は、プリザンターのソリューションに含まれるコマンドラインツールで、主に次の役割を持ちます。
- データベースの構成(テーブル作成・マイグレーション)
- C# コードの自動生成(Model、Utility、Rds など)
- 定義アクセサコードの生成(
Def.cs内の定義クラス) - ソリューションのバックアップ
ソリューション内の位置づけ
図を読み込み中…
CodeDefiner は Implem.DefinitionAccessor を通じて定義ファイルを読み込み、その定義に基づいて Implem.Pleasanter のソースコードを生成・更新します。
処理の流れ
図を読み込み中…
処理部品の関係(入力 → 処理 → 出力)は次のとおりです。
図を読み込み中…
エントリポイントとコマンド体系
起動は Starter.Main() から始まります。最初の引数をアクション名として取得し、/ で始まるオプション引数をハッシュテーブルに格納します。
Implem.CodeDefiner/Starter.cs#L28-L66
ValidateArgs(args);
var argHash = ArgsType(args);
var action = args[0];コマンド
| コマンド | DB 構成 | 定義コード生成 | MVC コード生成 | 説明 |
|---|---|---|---|---|
_rds | ✅ | - | - | DB 構成のみ |
rds | ✅ | ✅ | ✅ | DB 構成+全コード生成 |
_def | - | ✅ | - | 定義コードのみ |
def | - | ✅ | ✅ | 定義コード+MVC コード |
mvc | - | - | ✅ | MVC コードのみ |
splits_rds | - | - | - | Rds.cs のファイル分割 |
backup | - | - | - | ソリューションのバックアップ |
migrate | ✅ | - | - | 別の DBMS からのデータ移行(DB 構成のあと Migration.json の移行元から移す) |
ConvertTime | - | - | - | 全テーブルの日時列を指定の時差だけずらす(/h で時差を指定。既定 -9) |
merge | - | - | - | バージョンアップ時のパラメータの引き継ぎ(後述) |
trial | ✅ | - | - | トライアル・コミュニティ版向けの DB 構成 |
1.5.8.1 のソースで確認した一覧です(Starter.cs#L67-L127)。_rds に /c を付けると、DB を変更せずにマイグレーションの要否だけを確認します(後述の「確認だけ行う(/c)」)。
ConvertTime は _Bases などを除く全テーブル(_deleted / _history を含む)の datetime 型の列と、コメントの日時をずらします。/h は 9、-5:30、+9:00 のような「時」または「時:分」の形式で、それ以外は Invalid time format のエラーになります(TimeConverter.cs#L14-L74)。DB の日時はサーバーのタイムゾーンで入っているので(タイムゾーンの考え方)、サーバーのタイムゾーンを変えるときに使うコマンドです。
コード自動生成に関係するのは rds、_def、def、mvc の 4 つです。
オプション
| オプション | 引数 | 説明 |
|---|---|---|
/p | パス | サービスパス(Implem.Pleasanter のディレクトリ) |
/t | ターゲット | 特定の定義 ID のみを生成(mvc / def / rds) |
/l | 言語 | 既定の言語。有効な値なら Service.json の DefaultLanguage に書き込む |
/z | タイムゾーン | 既定のタイムゾーン。OS のタイムゾーン ID(例: Tokyo Standard Time)で指定し、Service.json の TimeZoneDefault に書き込む。存在しない ID は InvalidTimeZoneException |
/s | - | SA のパスワードをコンソールで入力し、Rds.json の UID=sa;PWD=...; の部分に書き込む |
/r | - | /s と一緒に指定すると、Owner / User のパスワードをランダムな GUID に書き換える |
/f | - | Issues / Results の列が定義より多い(列が減る)ときの中止を無視して続ける |
/y | - | 確認の入力(y)を省略する |
/c | - | _rds で、DB を変更せずにマイグレーションの要否だけを確認する |
/h | 時差 | ConvertTime でずらす時差 |
/b / /i | パス | merge のバックアップ先 / インストール先 |
/e | - | trial で拡張列のファイルを使う |
/p、/b、/i は次の引数をそのまま値として受け取ります(/p C:\web\pleasanter\Implem.Pleasanter のように空白で区切る)。ほかのオプションも、次の引数が / で始まらなければ値として受け取ります。同じ 2 文字のオプションを 2 回書くとエラーになります(Starter.cs#L212-L238、Starter.cs#L383-L396、Initializer.cs#L68-L91、Initializer.cs#L1280-L1331)。
/t は、開発時に特定のコード定義だけを再生成したい場合に便利です。
初期化
コード生成の前に Initializer.Initialize() で定義ファイルが読み込まれます。このメソッドはプリザンター本体からも呼ばれる共通の初期化処理で、codeDefiner: true を渡すと CodeDefiner 固有の初期化が有効になります。
Initializer.Initialize(
path,
assemblyVersion: Assembly.GetExecutingAssembly().GetName().Version.ToString(),
setLanguage: argHash.Get("l"),
setTimeZone: argHash.Get("z"),
codeDefiner: true,
setSaPassword: argHash.ContainsKey("s"),
setRandomPassword: argHash.ContainsKey("r"));中心となるのは SetDefinitions() です。
public static void SetDefinitions()
{
Displays.DisplayHash = DisplayHash();
Def.SetCodeDefinition(); // ← コード定義の読み込み
Def.SetColumnDefinition(); // ← カラム定義の読み込み
Def.SetTemplateDefinition();
Def.SetViewModeDefinition();
Def.SetDemoDefinition();
Def.SetSqlDefinition();
SetDisplayAccessor();
SetColumnDefinitionAccessControl();
}| 定義名 | ディレクトリ | 用途 |
|---|---|---|
| Code | Definition_Code/ | C# コードのテンプレートと生成ルール |
| Column | Definition_Column/ | テーブルカラムの定義 |
| Template | Definition_Template/ | サイトテンプレート |
| ViewMode | Definition_ViewMode/ | ビューモード定義 |
| Demo | Definition_Demo/ | デモデータ定義 |
| Sql | Definition_Sql/ | SQL テンプレート |
コード自動生成で特に重要なのは Code と Column です。
生成の 3 フェーズ
コマンドに応じて、次の 3 フェーズが順に実行されます。
図を読み込み中…
- DB 構成(
ConfigureDatabase): テーブル定義に基づいてスキーマを作成・更新します(後述の「テーブル作成の仕組み」を参照)。 - 定義コード生成(
DefinitionAccessorCreator): 定義ファイルにアクセスするための C# コードを生成します。 - MVC コード生成(
MvcCreator): Model・Utility・Rds などの C# コードを生成します。
DefinitionAccessorCreator
DefinitionAccessorCreator.cs#L9-L25
Source が "Def" の定義だけを抽出し、テンプレートを展開してからマージします。
private static void CreateCode()
{
Def.CodeDefinitionCollection
.OrderBy(o => o.Id)
.Where(o => o.Source == "Def") // Source が "Def" のものだけ
.ForEach(codeDefinition =>
{
var code = Creators.Create(codeDefinition, new DataContainer("DefinitionFile"));
if (!code.IsNullOrEmpty())
{
Merger.Merge(codeDefinition.OutputPath, code, codeDefinition.MergeToExisting);
}
});
}MvcCreator
internal static void Create(string target)
{
CreateEachTable(target); // テーブルごとに繰り返し生成
CreateNotRepeat(target); // 繰り返しなしの生成
}- CreateEachTable: 全テーブル名(
Depts、Groups、Users、Sites、Issues、Results、Wikisなど)をループし、Source == "Mvc"かつRepeatType == "Table"のコード定義を各テーブルに適用します。/tで指定したターゲットがあれば、そのIdの定義だけに絞り込みます。 - CreateNotRepeat:
RepeatTypeが空の定義(テーブルに依存しない共通コード)を 1 回だけ生成します。
private static void CreateEachTable(string target)
{
Def.TableNameCollection().ForEach(tableName =>
Def.CodeDefinitionCollection
.Where(o => target.IsNullOrEmpty() || o.Id == target)
.Where(o => o.Source == "Mvc") // Source が "Mvc" のもの
.Where(o => o.RepeatType == "Table") // RepeatType が "Table"
.Where(o => !Table.CheckExclude(o, tableName))
.ForEach(codeDefinition =>
{
var dataContainer = new DataContainer("Table");
var modelName = Def.ModelNameByTableName(tableName);
dataContainer.TableName = tableName;
dataContainer.ModelName = modelName;
var code = ReplacePlaceholder(
Creators.Create(codeDefinition, dataContainer),
tableName, modelName);
var fileName = ReplacePlaceholder(
Directories.Outputs(codeDefinition.OutputPath),
tableName, modelName);
Merger.Merge(fileName, code, codeDefinition.MergeToExisting);
}));
}DataContainer:コンテキストの受け渡し
生成中のテーブル名・カラム名などのコンテキストは DataContainer で各部品に引き回され、プレースホルダー置換の情報源になります。
| プロパティ | 説明 |
|---|---|
XlsIoCollection | 定義ファイル(Excel/JSON)のコレクション |
Type | コンテナの種類("Table" や "DefinitionFile") |
DefinitionName | 処理中の定義ファイル名("Code"、"Column" など) |
ModelName | モデル名("User"、"Issue" など) |
TableName | テーブル名("Users"、"Issues" など) |
FormName | フォーム名 |
ColumnName | カラム名 |
定義ファイル
格納場所
Implem.Pleasanter/App_Data/Definitions/
├── Definition_Code/ ← コード生成テンプレートと設定
│ ├── *.json ← 定義の属性(約876ファイル)
│ └── *_Body.txt ← テンプレート本体(約875ファイル)
└── Definition_Column/ ← カラム定義(約460ファイル)
└── *.jsonJSON と Body.txt のペア
Definition_Code/ には、1 つのコード定義につき最大 2 つのファイルがペアで存在します。
- JSON ファイル: 出力先パス、繰り返しタイプ、フィルタ条件などのメタ情報
_Body.txtファイル: C# コードのテンプレート本体。<!--子定義ID-->形式と#置換名#形式のプレースホルダーを含みます
{
"Id": "Base",
"OutputPath": "Models\\Shared\\_BaseModel.cs",
"Source": "Mvc"
}{
"Id": "Base_Property",
"Indent": "2",
"Separator": "\\r\\n",
"NotCalc": "1",
"Exclude": "SiteSettings"
}if (ss.PermissionForCreating != null)
{
ss.SetPermissions(
context: context,
referenceId: #ModelName#Id);
}JSON の Id とファイル名の対応は、Base.json(Id: "Base")→ Base_Body.txt、Model_ReloadPermissions.json → Model_ReloadPermissions_Body.txt のようになります。Body.txt が存在しない定義もあり、その場合は JSON 内の Body プロパティに直接テンプレートを書くか、子定義を組み合わせた展開のみを行います。
読み込み(XlsIo)
定義ファイルの読み込みは XlsIo クラスが担当します。歴史的に Excel ファイル(.xlsm)から読み込んでいた名残でこの名前ですが、現在は JSON ファイルベースの読み込みが主流です。
Implem.Libraries/Classes/XlsIo.cs#L9-L50
図を読み込み中…
*_Body.txt は、ファイル名から定義 ID を逆引きし、対応する行の Body 列に設定されます。これにより、メタ情報は JSON、テンプレート本体はテキストファイルで管理できます。
CodeDefinition のプロパティ
基本プロパティ:
| プロパティ | 型 | 説明 | 例 |
|---|---|---|---|
Id | string | 定義の一意識別子 | "Base", "Model_Property" |
Body | string | テンプレート本体 | C# コードテンプレート |
OutputPath | string | 出力ファイルのパス | "Models\\#ModelName#\\..." |
Source | string | 定義の種別 | "Mvc", "Def" |
RepeatType | string | 繰り返しの種類 | "Table", "Column" など |
MergeToExisting | bool | 既存コードにマージするか | true / false |
テンプレート制御プロパティ:
| プロパティ | 型 | 説明 |
|---|---|---|
Indent | int | テンプレートのインデント深度 |
Separator | string | 繰り返し展開時の区切り文字(例:"\\r\\n") |
Order | string | カラムの並び順を指定するフィールド名 |
NoSpace | bool | 空行を除去するか |
ReplaceOld | string | 展開後に置換する旧文字列 |
ReplaceNew | string | 展開後に置換する新文字列 |
文字列フィルタ(カンマ区切り):
| プロパティ | フィルタ対象 | 説明 |
|---|---|---|
Include / Exclude | テーブル名 / カラム名 | 包含 / 除外指定 |
IncludeTypeName / ExcludeTypeName | DB の型名 | 特定の型名だけに適用 / 除外 |
IncludeTypeCs / ExcludeTypeCs | C# の型名 | 特定の C# 型だけに適用 / 除外 |
IncludeDefaultCs / ExcludeDefaultCs | C# デフォルト値 | 特定のデフォルト値を含む / 除外 |
ブール型フィルタ:
| プロパティ | 条件を満たす場合に生成 |
|---|---|
Pk / NotPk | 主キーである / 主キーでない |
Identity / NotIdentity | IDENTITY 列である / でない |
Unique / NotUnique | ユニーク制約がある / ない |
Session | セッション保存対象のカラム |
Form | フォーム表示対象のカラム |
Select | SELECT 対象のカラム |
Update | UPDATE 対象のカラム |
Calc / NotCalc | 計算式カラムである / でない |
Join / NotJoin | JOIN 対象のカラムである / でない |
History | 履歴対象のカラム |
ItemOnly / NotItem | Item テーブルのみ / Item 以外 |
GenericUi / NotGenericUi | 汎用 UI テーブルのみ / それ以外 |
HasIdentity / HasNotIdentity | テーブルに IDENTITY 列がある / ない |
Class / NotClass | クラス型カラムである / でない |
フィルタは 1 つの定義に複数組み合わせることができ、すべての条件を満たすテーブル / カラムに対してのみコードが生成されます(AND 条件)。
SavedMemory / RestoreBySavedMemory(Memento パターン)
CodeDefinition の各プロパティには Saved プレフィックス付きのペア(Id と SavedId、Body と SavedBody など)があります。入れ子展開時に SetCodeDefinitionOption() でプロパティが一時的に書き換えられるため、初期化時に全プロパティを Saved* に保存しておき、展開後に RestoreBySavedMemory() で一括復元します。
// Creators.Create() 内
Def.SetCodeDefinitionOption(placeholder, codeChildDefinition);
// ... コード生成処理 ...
codeChildDefinition.RestoreBySavedMemory(); // 元の値に復元カラム定義(Definition_Column)
{
"Id": "Users_UserId",
"ModelName": "User",
"TableName": "Users",
"Label": "ユーザ",
"ColumnName": "UserId",
"LabelText": "ユーザID",
"No": "1",
"TypeName": "int",
"Pk": "1",
"Identity": "1",
"Seed": "1",
"ControlType": "Id"
}| プロパティ | 説明 | 例 |
|---|---|---|
ModelName | モデル名 | "User" |
TableName | テーブル名 | "Users" |
ColumnName | カラム名 | "UserId" |
TypeName | DB 上のデータ型 | "int", "nvarchar" |
TypeCs | C# 上のデータ型 | "Title", "Status" など |
Pk | 主キーの順序(0 = 主キーでない) | "1" |
Identity | IDENTITY 列か | "1" |
MaxLength | 最大長 | "100" |
Default | デフォルト値 | "" |
DefaultCs | C# でのデフォルト値 | "string.Empty" |
RecordingData | 記録時のデータ変換 | ".ToJson()" |
ByForm | フォーム入力時の変換 | カスタム変換式 |
ByApi | API 入力時の変換 | カスタム変換式 |
ByDataRow | DataRow 読込時の変換 | カスタム変換式 |
カラム定義は、プレースホルダー置換の値を提供するだけでなく、フィルタ条件の判定にも使われます。たとえば Pk が 0 のカラムは、Pk フィルタ付きの定義では生成対象から除外されます。
テンプレート展開エンジン(Creators.cs)
2 種類のプレースホルダー
Utilities/CodePatterns.cs#L3-L8
internal static class CodePatterns
{
internal const string IdPlaceholder = "<!--.*?-->";
internal const string Id = "[A-Za-z0-9_]+";
internal const string ReplacementPlaceholder = "(?<=#)[^#]+?(?=#)";
}| パターン | 正規表現 | マッチ例 | 用途 |
|---|---|---|---|
IdPlaceholder | <!--.*?--> | <!--Model_Property--> | 子定義の埋め込み位置を検出 |
Id | [A-Za-z0-9_]+ | Model_Property | IdPlaceholder 内から定義 ID を抽出 |
ReplacementPlaceholder | (?<=#)[^#]+?(?=#) | #ModelName# 中の ModelName | 値置換プレースホルダーを検出 |
<!--ID-->: 他のCodeDefinitionを参照して展開する構造的プレースホルダー#Name#: テーブル名やカラム名などの値で直接置換する値プレースホルダー
Creators.Create() の処理
図を読み込み中…
internal static string Create(CodeDefinition codeDefinition, DataContainer dataContainer)
{
var code = codeDefinition.FormattedCode();
foreach (var placeholder in code.RegexValues(CodePatterns.IdPlaceholder))
{
var id = placeholder.RegexFirst(CodePatterns.Id);
var codeChildDefinition = Def.CodeDefinitionCollection
.FirstOrDefault(o => o.Id == id);
if (codeChildDefinition != null)
{
Def.SetCodeDefinitionOption(placeholder, codeChildDefinition);
var codeChildCollection = new List<string>();
switch (codeChildDefinition.RepeatType)
{
case "Table":
Table.SetCodeCollection(
codeChildDefinition, codeChildCollection, dataContainer);
break;
case "Column":
Column.SetCodeCollection(
codeChildDefinition, codeChildCollection, dataContainer);
break;
// ... 他のRepeatType ...
}
code = CodeChildCollection(
code, placeholder, codeChildDefinition, codeChildCollection);
codeChildDefinition.RestoreBySavedMemory();
}
}
ReplaceCode(ref code, dataContainer.TableName);
return code;
}FormattedCode():インデント付与
単一行なら Indent 分のタブ + Body、複数行なら各行にインデントを付与します。空行はそのまま、<!-- で始まる行(子定義プレースホルダー)にはインデントを付けません。子定義は展開時に自身のインデントを適用するため、二重インデントを防いでいます。最後に NoSpace に応じて空行を除去します。
再帰展開
子定義のテンプレートにさらに孫定義のプレースホルダーがあれば、Creators.SetCodeCollection() を中継して Creators.Create() が再帰的に呼ばれます。
図を読み込み中…
internal static void SetCodeCollection(
ref string code,
List<string> codeCollection,
CodeDefinition codeDefinition,
DataContainer dataContainer,
Action replaceCode)
{
code = Create(codeDefinition, dataContainer); // ← 再帰呼び出し
replaceCode(); // 値プレースホルダーの置換
codeCollection.Add(code); // 結果をコレクションに追加
}インラインオプション(SetCodeDefinitionOption)
構造的プレースホルダーには括弧でオプションを埋め込めます。SetCodeDefinitionOption() が括弧内をカンマ・= で分解し、子定義のプロパティ(Indent、NotPk、Include など)を一時的に上書きします。同じ子定義を異なる条件で再利用できます。
<!--Model_Property(Indent=3, NotPk=1)-->
<!-- 主キーカラムだけを対象にして展開 -->
<!--Model_Property(Pk=1)-->
<!-- 主キーでないカラムだけを対象にして展開 -->
<!--Model_Property(NotPk=1)-->展開結果の結合
各ハンドラで生成されたコード片は Separator で結合されます("\\r\\n" は実際の改行に変換)。
| Separator の値 | 実際の区切り | 用途の例 |
|---|---|---|
\\r\\n | 改行 | プロパティ宣言の羅列 |
,\\r\\n | カンマ+改行 | enum の値やメソッド引数 |
| (空) | 空文字列 | コード片を直接結合 |
展開の具体例
親定義 Model(Source: "Mvc"、RepeatType: "Table")のテンプレートと、子定義 Model_Property(Indent: 2、Separator: "\\r\\n")が次のとおりだとします。
namespace #ServiceName#.Models
{
public class #ModelName#Model : _BaseModel
{
<!--Model_Property-->
}
}public #Type# #ColumnName# { get; set; }Users テーブルの場合、<!--Model_Property--> が Column.SetCodeCollection() で Users の各カラムに展開・結合され、最後に MvcCreator.ReplacePlaceholder() で #ServiceName# → Implem.Pleasanter、#ModelName# → User に置換されます。
namespace Implem.Pleasanter.Models
{
public class UserModel : _BaseModel
{
public int UserId { get; set; }
public string LoginId { get; set; }
public string Name { get; set; }
// ...
}
}RepeatType 別のハンドラ(Parts)
対応表
| RepeatType | 繰り返し対象 | ハンドラ(Parts/) |
|---|---|---|
| (空) | 繰り返しなし(1 回だけ展開) | - |
Table | 各テーブル(Depts, Users, Issues など) | Table.cs |
Column | 各テーブルの各カラム | Column.cs |
BaseModel | 全テーブル共通のカラム | BaseModel.cs |
BaseItemModel | Item テーブル共通のカラム | BaseItemModel.cs |
Join | JOIN カラム定義 | Join.cs |
Form | フォーム定義 | Form.cs |
Display | 多言語表示文字列 | Display.cs |
DefinitionFile | 定義ファイル(Code, Column, Template など) | DefinitionFile.cs |
DefinitionRow | 定義ファイル内の各行 | DefinitionRow.cs |
DefinitionColumn | 定義ファイルの各カラム | DefinitionColumn.cs |
すべてのハンドラは 繰り返し対象の取得 → フィルタ → Creators.SetCodeCollection() で再帰展開 → 値置換 → codeCollection に追加 という共通パターンです。
ネストの関係は次のとおりです。
図を読み込み中…
Table
Def.TableNameCollection(order: codeDefinition.Order) をループし、DataContainer の TableName / ModelName を切り替えながら展開します。終了後は親の値に戻します。
テーブルレベルのフィルタは Table.CheckExclude() にあり、すべて除外判定(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() では大文字・小文字の 6 種類のプレースホルダーを置換し、最後に ReplaceOld / ReplaceNew を適用します。
| プレースホルダー | 変換 | 例(Issues テーブル) |
|---|---|---|
#ModelName# | そのまま | Issue |
#modelName# | 先頭を小文字 | issue |
#modelname# | すべて小文字 | issue |
#TableName# | そのまま | Issues |
#tableName# | 先頭を小文字 | issues |
#tablename# | すべて小文字 | issues |
Column
現在のテーブルのカラムを CheckExclude で絞り込み、Order(未指定なら No)で並べて展開します。Table ハンドラの内側で呼ばれることが多く、テーブル × カラムの 2 重ループになります。
Column.CheckExclude() は CodeDefiner で最も多くのフィルタ条件を持つメソッドです。
| 分類 | フィルタ |
|---|---|
| 型 | IncludeTypeName、ExcludeTypeName、IncludeTypeCs、ExcludeTypeCs |
| キー | Pk / NotPk、Identity / NotIdentity、Unique / NotUnique、IdentityOrPk |
| UI | Form、Select、Update、GridColumn、FilterColumn、EditorColumn、TitleColumn |
| その他 | Calc / NotCalc、Join / NotJoin、History / PkHistory、Class / NotClass、Null / NotNull、Session、NotBase |
NotBase はベースモデル(全テーブル共通)に含まれるカラムを除外するフィルタです。Item モデルなら BaseItem カラム、それ以外なら Base カラムに同名カラムがあれば除外しますが、EachModel フラグが立っているカラムは除外しません。
BaseModel / BaseItemModel
全テーブル共通(BaseModel)/ Item テーブル共通(BaseItemModel)のカラムのコードを生成します。どちらも EachModel = true のカラムを除外します。EachModel は、ベースカラムでありながらテーブルごとに異なる実装が必要なカラムを、各テーブル固有のコードとして生成するためのフラグです。
Join
JoinTableName が設定されているカラムを対象に繰り返します。
| プレースホルダー | 説明 |
|---|---|
#JoinTableName# | 結合先テーブル名 |
#JoinType# | 結合タイプ(SqlJoin.JoinTypes.Inner など) |
#JoinExpression# | 結合条件式 |
#TableNameAlias# | テーブルエイリアス |
#ColumnBracket# | カラムのブラケット表記 |
#ColumnBrackets# | 計算列を含むカラムブラケット |
#JoinType# はカラム定義の JoinType から変換されます(inner join → SqlJoin.JoinTypes.Inner、left outer join → SqlJoin.JoinTypes.LeftOuter、right outer join → SqlJoin.JoinTypes.RightOuter)。
Form
テーブル内のユニークな ModelName / FormName の組み合わせを繰り返します。#FormName# は、FormName が未設定なら ModelName + "Form" に展開されます。
Display
DisplayAccessor.Displays.DisplayHash の各表示文字列の各言語を繰り返します。多言語リソースのコード生成については 多言語対応の実装 も参照してください。
| プレースホルダー | 説明 |
|---|---|
#DisplayId# | 表示 ID(言語サフィックス付き) |
#DisplayContent# | 表示テキスト |
#DisplayCssClass# | CSS クラス名 |
#DisplayContentEncoded# | HTML エンコード済みテキスト |
特有のフィルタとして DisplayLanguages(多言語エントリを含めるか)と ClientScript(クライアントスクリプト用のエントリか)があります。
DefinitionFile / DefinitionRow / DefinitionColumn
定義ファイル自体のメタ情報をコード化するハンドラで、主に Def.cs(定義アクセサ)の生成に使われます。
| ハンドラ | 繰り返し対象 | 主なプレースホルダー |
|---|---|---|
DefinitionFile | 定義ファイルの種類(Code, Column, Template, ViewMode, Demo, Sql) | #File#、#file#、#ColumnNames# |
DefinitionRow | 定義ファイル内の各行(ヘッダ行を除き、ID が空でない行) | #Id#(ReservedWords.ValidName() で C# 識別子化) |
DefinitionColumn | 定義ファイルの各カラム | #DefColumnName#(エスケープ済み)、#DefColumnNameOriginal#、#Type#、#CastType#、#SetDefault# |
プレースホルダー置換と型変換
Column.ReplaceCode
| プレースホルダー | 説明 | 値の例 |
|---|---|---|
#ColumnName# | カラム名(先頭大文字) | UserId, LoginId |
#columnName# | カラム名(先頭小文字) | userId, loginId |
#Type# | C# 型(TypeCs → TypeName の優先順) | string, int, Title |
#RecordingType# | DB 記録時の型 | int, nvarchar |
#RecordingData# | 記録時のデータ変換式 | .ToJson() |
#CastType# | キャスト式 | .ToInt(), .ToString() |
#DefaultData# | デフォルト値 | 0, string.Empty |
#InitialValue# | 初期値 | 0, false, "[]" |
#Hash# | ハッシュ変換式(Hash 列の場合) | .Sha512Cng() |
#MaxLength# | 最大長バリデーション | .MaxLength(100) |
#Calc# | 計算式 | テーブル名・モデル名を置換済みの式 |
#ColumnCount# | 対象カラムの総数 | 15 |
#GridEnable# | 一覧表示の有効/無効 | 1, 0 |
固定のもの以外に、カラム定義のプロパティ名がそのままプレースホルダーとして使えます(動的プレースホルダー)。Def.ColumnXls.XlsSheet.Columns に含まれる名前なら #プロパティ名# がそのカラムの値に置換されるため、カラム定義に新しいプロパティを追加するだけでテンプレートから使えます。
型変換プレースホルダー(Converts.cs)
データの入口に応じた型変換コードを生成します。
| プレースホルダー | 用途 | ソースメソッド |
|---|---|---|
#ByForm#(value) | フォーム入力からの変換 | Converts.ByForm() |
#ByApi#(data.Property) | API 入力からの変換 | Converts.ByApi() |
#ByDataRow#(dataRow) | DataRow からの変換 | Converts.ByDataRow() |
#BySession#(session) | セッションからの変換 | Converts.BySession() |
たとえばテンプレート #ColumnName# = #ByForm#(value); は、Users の LoginId(nvarchar)なら LoginId = value.ToString();、UserId(int)なら UserId = value.ToInt(); に展開されます。
変換方法は次の優先順位で決まります。
図を読み込み中…
CastType() の対応:
| DB 型名 | C# 型分類 | キャスト式 |
|---|---|---|
nvarchar, varchar, char など | CsString | .ToString() |
int | CsInt | .ToInt() |
bigint | CsLong | .ToLong() |
decimal, money など | CsDecimal | .ToDecimal() |
float | CsSingle | .ToSingle() |
real | CsDouble | .ToDouble() |
datetime | CsDateTime | .ToDateTime() |
bit | CsBool | .ToBool() |
TypeCs が設定されたカラムは、単純なキャストではなくオブジェクト生成になります。
| TypeCs | 変換元 | 生成されるコード |
|---|---|---|
Title | ByForm | new Title(value.ToString()) |
Time | ByForm | new Time(context, value.ToDateTime(), byForm: true) |
Status | ByApi | new Status(data.Status.ToInt()) |
CompletionTime | ByDataRow | new CompletionTime(context, dataRow, column.ColumnName) |
| その他 | - | value as TypeCs ?? defaultValue |
また、Issues と Results の Body カラムの ByForm には、BinaryUtilities.NormalizeFormBinaryPath(context, value.ToString()) によるバイナリパス正規化が入ります。
DefaultData / InitialValue
| 型分類 | デフォルト値 |
|---|---|
| 文字列 | string.Empty |
文字列(RecordingData が .ToJson() / .RecordingJson()) | TypeCs が List<long> / Attachments なら "[]"、それ以外は "{}" |
| 数値 | 0 |
| 日時 | 0.ToDateTime() |
| ブール | false(Default="1" のとき true) |
| バイナリ | null |
カラム定義に DefaultCs があればそれが最優先、次に Default、どちらもなければ上表の値が使われます。
予約語エスケープと識別子化(ReservedWords.cs)
Implem.Libraries/Utilities/ReservedWords.cs#L4-L48
EscapeReservedWord(): C# の予約語(class、intなど)とコンテキストキーワード(add、dynamic、get、set、value、var、where、yieldなど)に一致すると、既定でアンダースコアを前置します(class→_class)。ValidName(): 定義 ID の特殊文字を C# 識別子として有効な文字列に変換します。DefinitionRowハンドラで使われます。
| 元の文字 | 変換後 |
|---|---|
| 空白 | _space_ |
. | _dot_ |
# | _sharp_ |
, | _comma_ |
: | _colon_ |
> [ ] ( ) - | _ |
+ | _plus_ |
= | _equal_ |
^ | _caret_ |
" | _yen_ |
* | _asterisk_ |
@ | _atmark_ |
置換のレベルと実行順序
図を読み込み中…
実行順序は次のとおりです(図のレベル番号と実行順序は一致しません)。
| 順序 | レベル | 対象 | 実施箇所 |
|---|---|---|---|
| 1 | 構造的プレースホルダー | <!--子定義ID--> | Creators.Create() で再帰展開 |
| 2 | ハンドラレベルの値置換 | Table: #ModelName#, #TableName# など / Column: #ColumnName#, #Type# など / Join: #JoinTableName# など / 型変換: #ByForm#() など / DefinitionColumn: #DefColumnName# など | 各ハンドラの ReplaceCode() |
| 3 | Creators レベルの値置換 | #IdType#(ID 列の C# 型)、#IdTypeDefault#(ID 列のデフォルト値)、#CastIdType#(ID 列のキャスト式) | Creators.ReplaceCode() |
| 4 | MvcCreator レベルの置換 | #ServiceName#、#TableName#、#tableName#、#ModelName#、#modelName# | MvcCreator.ReplacePlaceholder() |
レベル 4 の置換は出力ファイルのパスにも適用されます。たとえば OutputPath が Models\#ModelName#\#ModelName#Model.cs なら Models\Issue\IssueModel.cs に展開されます。
コードマージ(Merger.cs / Parser.cs)
CodeDefiner は定義の変更のたびにコードを再生成しますが、生成コードへの手動修正(固有のビジネスロジック、クエリ最適化、例外処理など)が失われないよう、既存ファイルとマージします。
Merger.Merge()
図を読み込み中…
マージ処理が行われるのは C# ファイルだけで、それ以外は単純に上書きされます。
MergeToExisting でマージの方向(ベースとなるコード)が決まります。
| MergeToExisting | ベース | マージされる側 | ユースケース |
|---|---|---|---|
true | 既存コード | 新しい生成コード | 既存の手動修正を保持しつつ新機能を追加 |
false | 新しい生成コード | 既存コード | 生成コードを優先しつつ手動修正を取り込み |
Parser:C# コードの構造解析
Parser は C# ソースを 名前空間 → クラス → メソッド の階層(CodeTypes: Namespace / Class / Method)に分解します。各ノードはシグネチャ(Id)、名前、XML ドキュメントコメント(Description)、Fixed / NotMerge マーカー、テキスト片(TextCollection)と子メンバー(MemberCollection)を持ち、子メンバーごとに Parser が再帰生成されます。
図を読み込み中…
たとえば、クラス UserModel にプロパティ UserId、/// Fixed: 付きメソッド CustomMethod()、自動生成メソッド GeneratedMethod() がある場合、次のように分解されます。
図を読み込み中…
/// Fixed: と /// NotMerge:
| マーカー | 効果 |
|---|---|
/// Fixed: | 再生成時に既存の内容が保持される(新しい生成コードで上書きしない) |
/// NotMerge: | マージ処理から完全に除外される |
/// Fixed:
public string CustomMethod()
{
return "この内容は再生成時に保持される";
}検出ロジックは次のとおりです。
private void SetFixed()
{
Fixed = Description.IndexOf("/// Fixed:") != -1;
}
private void SetNotMerge()
{
NotMerge = Description
.RegexExists("^ {" + Indent + "}/// NotMerge:.*", RegexOptions.Multiline);
}MergeCode() の判定
private void MergeCode(
Parser baseCsParent, Parser baseCs, Parser margeCs, bool margeToExisting)
{
// ステップ1: Fixed メンバーの処理
if (margeCs.Fixed)
{
MergeFixedCode(baseCsParent, baseCs, margeCs);
}
// ステップ2: MergeToExisting フラグに基づくマージ
if (margeToExisting)
{
MergeToExisting(baseCsParent, baseCs, margeCs);
}
// ステップ3: 子メンバーに対して再帰的にマージ
margeCs.MemberCollection.Select(o => o.Value).ForEach(memberOld =>
MergeCode(baseCs, SameMember(baseCs, memberOld), memberOld, margeToExisting));
}図を読み込み中…
メンバーの同一性は Id(アクセス修飾子や引数リストを含むシグネチャ全体)で判定されるため、オーバーロードも区別されます。マージ後は Code() が TextCollection と MemberCollection をインデックス順に結合し、メンバーの出現順序を維持します。
マージの例(MergeToExisting = false)
既存ファイルに /// Fixed: 付きの ValidateCustomRule() があり、新しい生成コードで Name プロパティが追加・Update() が変更された場合、結果は次のようになります。
namespace Implem.Pleasanter.Models
{
public class UserModel : _BaseModel
{
public int UserId { get; set; }
public string LoginId { get; set; }
public string Name { get; set; } // 新カラムが反映
/// Fixed:
public bool ValidateCustomRule()
{
// 開発者が手動で追加したバリデーション(保持される!)
return LoginId.Length >= 3;
}
public void Update()
{
// 自動生成された更新処理(最新版に更新)
}
}
}CodeWriter:差分がある場合のみ書き込み
Utilities/CodeWriter.cs#L7-L21
既存コードと新コードが完全に同じなら書き込みをスキップしてコンソールに - を表示し、差分がある場合だけ書き込んで変更されたファイルパスを表示します。不要なタイムスタンプ更新や Git の差分ノイズを防ぐためです。
データベースの作成・更新(_rds / rds)
プリザンターは SQL Server、PostgreSQL、MySQL に対応しており、CodeDefiner は RDBMS ごとの違いを吸収しながらデータベース・ユーザー・テーブルを作成・更新します。以下は 1.5.8.1 のソースで確認した内容です。
処理の順序
ConfigureDatabase() は、まず SA の接続文字列で DB に接続できるかを確かめ、Configurator.Configure() を呼びます(Starter.cs#L398-L436、Configurator.cs#L18-L48)。
図を読み込み中…
yの確認は 5 のあとに出ます。確認より前に 1〜4(セッション切断・DB とユーザーとスキーマの作成 / 更新)はもう実行されています。 確認でy以外を入力して中止しても、1〜4 は取り消されません。入力は大文字小文字を区別せず、yとyesを受け付けます(Configurator.cs#L89-L139、Configurator.cs#L346-L350)。- 5 では Issues と Results について、DB にあって定義に無い列を探します。見つかると列名を表示し、
/fが無ければ中止します。拡張列の数を減らした場合などに、列ごとデータを失うのを防ぐためのチェックです。 ProviderがAzureのときは 1〜4 と 7 を飛ばします(Pleasanter Setup と Rds.json の Provider)。
接続は 2 種類を使い分けます。DB・ユーザーの作成と権限付与は SA の接続(Def.SqlIoBySa()、SaConnectionString)、テーブルの作成・移行は Owner の接続(Def.SqlIoByAdmin()、OwnerConnectionString)です。
| 段階 | SQL Server | PostgreSQL | MySQL |
|---|---|---|---|
| DB の作成 | create database ... collate japanese_90_ci_as_ks | CreateDatabase はダミー(select 1)で、CreateUserForPostgres / CreateDatabaseForPostgres で作成 | create database ... collate utf8mb4_general_ci |
| 新規作成時のスキーマ | - | サービス名のスキーマを Owner の所有で作成し、pg_trgm・pgcrypto 拡張を入れる | - |
| Owner の権限 | db_owner ロール | alter role のみ(テーブルの所有者になるため) | create, alter, index, drop と select, insert, update, delete, create routine, alter routine ... with grant option |
| User の権限 | db_datareader・db_datawriter ロール | スキーマ内の Owner 所有の全テーブルに select, insert, update, delete | select, insert, update, delete, create routine, alter routine |
MySQL の権限付与は、MySqlConnectingHost にカンマ区切りで書いたホストごとに行います(PrivilegeConfigurator.cs#L11-L27)。
PostgreSQL で使うスキーマは次のように決まります。DB がまだ無ければサービス名のスキーマを作ります。DB がすでにあれば、サービス名のスキーマがあればそれを、無ければ public を使います(RdsConfigurator.cs#L79-L96、SchemaConfigurator.cs#L9-L36)。
対象のテーブル
管理するテーブルの一覧はコードに書かれておらず、Def.TableNameCollection() がカラム定義(Definition_Column/*.json)の TableName から導きます(_Base で始まるモデルは除く)。定義ファイルを足せばテーブルが増え、消せば管理対象から外れます(Def.cs#L106-L116)。
テーブルは削除されない
CodeDefiner にはテーブルを削除する処理がありません。定義から消したテーブルも、後述の移行で退避された旧テーブル(_Migrated_...)も DB に残ります。
1 つのテーブル定義から、最大 3 つの物理テーブルを作ります(TablesConfigurator.cs#L151-L214)。
| 物理テーブル | 作る条件 | 列 |
|---|---|---|
{テーブル名} | 常に | 対象の列すべて |
{テーブル名}_deleted | History が 1 以上の列があるとき | 通常テーブルと同じ |
{テーブル名}_history | 同上 | History が 1 以上の列だけ(History の順) |
_deleted / _history の列には IDENTITY を付けません。対象の列からは、NotUpdate の列、JoinTableName のある列(結合用)、Calc のある列(計算列)、SysLogs の列のうち SchemaVersion が Rds.json の SysLogsSchemaVersion より大きいもの、テーブル固有の列がすべて ExcludeBaseColumns のときの共通列(_Bases / _BaseItems 由来)を除きます。
Quartz.NET のテーブル(Quartz.json の Clustering.TablePrefix で始まるもの。既定 QRTZ_)は、Clustering.Enabled が true のときだけ作ります。PostgreSQL では Quartz のテーブル名・列名を小文字にします(TablesConfigurator.cs#L15-L34)。
全テーブルのあと、全文検索のインデックスを作ります。SQL Server は ftx の全文カタログと Items・Binaries の全文インデックス、PostgreSQL は DB を新規作成したときだけ Items の FullText に gin_trgm_ops の GIN インデックス、MySQL は Items の FullText に ngram パーサーの全文インデックスです(TablesConfigurator.cs#L59-L149)。
作り直すかどうかの判定
テーブルごとに、無ければ作成、あれば Tables.HasChanges() で定義と DB を比べ、違いがあれば移行(作り直し)します(TablesConfigurator.cs#L216-L267、Tables.cs#L165-L199)。次のどれか 1 つでも違えば移行します。
| 比較 | 内容 |
|---|---|
| 列の数 | 定義の列数と DB の列数 |
| 列ごと(定義の順に比較) | 列名、型(DB の型名を SQL Server の型名に戻して比較)、サイズ、NULL 許可、IDENTITY(_history / _deleted は比較しない) |
| 既定値 | Default のある列を列名順に 列名,既定値 で並べた文字列の一致(_history の Ver は除く) |
| インデックス | 定義から作るインデックス名の一覧と DB のインデックス名の一覧(PostgreSQL は ftx を除く。MySQL は主キーの列構成とそれ以外のインデックス名を別々に比較) |
- 列はテーブル上の並び順で比べるので、同じ列でも順番が違えば移行になります(Columns.cs#L25-L95)。
- サイズは
char/varcharならMaxLengthと、nchar/nvarcharならMaxLength × 係数(SQL Server 2、PostgreSQL・MySQL 4)と比べます。decimalはSize(18,4など)の文字列、max(-1)どうしは同じとみなします(ColumnSize.cs)。 Rds.jsonのDisableIndexChangeDetectionがtrueのときはインデックスを比べません。配布時のRds.jsonはtrueなので、インデックスの違いだけでは移行しません(Indexes.cs#L253-L279、Rds.json#L11)。
移行(作り直し)のしかた
移行は ALTER TABLE ではなく、新しい構造のテーブルを作ってデータを入れ直し、名前を入れ替える方式です(Tables.cs#L41-L147)。
図を読み込み中…
IDENTITY 列のあるテーブル(_history / _deleted を除く)は、SQL Server では set identity_insert ... on/off で囲み、PostgreSQL では最後に setval(pg_get_serial_sequence(...), max(...)) で連番を合わせます。MySQL は通常と同じです(Sqls/SQLServer/MigrateTableWithIdentity.sql、Sqls/PostgreSQL/MigrateTableWithIdentity.sql)。
データを入れ直すときの列の対応は次のとおりです。
| 条件 | 新テーブルに入れる値 |
|---|---|
| 旧テーブルに同じ名前の列がある | その列の値 |
旧テーブルに無く、定義に OldColumnName がある | OldColumnName の列の値(列名の変更) |
旧テーブルに無く、Default が無く NOT NULL | 型に応じた値(文字列は ''、日時は現在日時、真偽は既定の真偽値、ほかは 0) |
| 上記以外 | 入れない(NULL または既定値になる) |
移行は全件をコピーするので、データの多いテーブルでは時間がかかります。旧テーブルは _Migrated_ 付きの名前で残るため、DB の容量も増えます。
確認だけ行う(/c)
_rds /c は DB を変更しません。Local のときは DB が無いかを確認して DB 名・Owner / User のユーザー名・スキーマ名を表示し、DB が無ければそう表示して終わります。DB があれば全テーブルを比較します。テーブルごとの行に加えて、作成・移行が必要なテーブルには CreateTable / MigrateTable の行が出ますが、SQL は実行しません。変更が無ければそのことを表示します(Configurator.cs#L49-L86)。バージョンアップの前に、どのテーブルが作り直されるかを見るのに使えます。
画面とログへの出力
CodeDefiner の出力は Consoles.Write() から Trace に書かれ、Trace にはログファイル logs/Implem.CodeDefiner_yyyyMMdd_HHmmss.log とコンソールの両方が登録されています。そのため、ログに出る行はコンソールにも同じく出ます(Starter.cs#L28-L35、Consoles.cs#L18-L45)。
<INFO> TablesConfigurator.ConfigureTableSet: Users
<INFO> Tables.MigrateTable: Users
<INFO> Tables.CreateTable: Users<種類> クラス名.メソッド名: 内容 の形式です。テーブルの処理では、テーブルごとに ConfigureTableSet の行が出て、作成・移行するときは CreateTable / MigrateTable の行が続きます(移行は内部で新テーブルを作るので CreateTable の行も出ます)。1 行出たあと次の行が出るまでは、そのテーブルの処理(移行ならデータのコピー)が続いている状態です。データの多いテーブルの移行中は、画面が長く止まって見えます。
テーブル単位のエラーは <ERROR> の行として出て、次のテーブルに進みます。最後にエラーの件数とログファイルのパスが表示されます(エラーが無ければ完了のメッセージ)。DB に接続できないときなど abort のエラーは、その場で終了します。Pleasanter Setup から実行したときにログで進み具合を見る方法は Pleasanter Setup と Rds.json の Provider を参照してください。
SQL テンプレート
CodeDefiner が実行する SQL は、1.5.8.1 では App_Data/Definitions/Sqls/{SQLServer|PostgreSQL|MySQL}/*.sql(各 55 ファイル、同じファイル名)から読み込まれ、Def.Sql のフィールドに入ります。RDBMS は Rds.json の Dbms で選ばれます(Def.cs#L5961-L5985、Directories.cs#L63-L66)。#TableName# などのプレースホルダーを Replace() で置き換えて実行します。
テーブル作成では、CreateTable.sql に対して次の置換を行います(Tables.cs#L13-L39)。
| プレースホルダー | 置き換えられる内容 | 作るメソッド |
|---|---|---|
#Columns# | 列の定義(型、サイズ、IDENTITY、NULL 制約) | CreateColumn() |
#Pks# | 主キー制約 | CreatePk() |
| (主キーの後ろ) | インデックス定義 | CreateIx() |
#Defaults#(SQL Server / PostgreSQL)/ #ModifyColumn#(MySQL) | 既定値 | CreateDefault() |
#DropConstraint# | 既存の制約の削除 | DropConstraint() |
#TableName# | テーブル名(移行時は一時テーブル名) | - |
ファクトリによる RDBMS 切り替え
RDBMS 固有の処理は、RdsFactory.Create(Parameters.Rds.Dbms) が返す ISqlObjectFactory(SqlServerObjectFactory / PostgreSqlObjectFactory / MySqlObjectFactory)で切り替えます。SQL の方言は ISqls、型の変換は ISqlDataType、RDBMS ごとの設定値は ISqlDefinitionSetting です。SQL テンプレートで吸収できない違い(列の作成、MySQL のインデックスと既定値、サイズの比較)は、C# の switch (Parameters.Rds.Dbms) で分けています。
Implem.CodeDefiner/Starter.cs#L66
| 項目 | プロパティ | SQL Server | PostgreSQL | MySQL |
|---|---|---|---|---|
| 自動連番 | GenerateIdentity | identity({0}, 1) | generated by default as identity (start with {0} increment by 1) | 空文字(主キー制約追加後に別途 auto_increment を設定) |
| 現在日時 | CurrentDateTime | getdate() | CURRENT_TIMESTAMP | CURRENT_TIMESTAMP(3) |
| NULL 置換 | IsNull | isnull | coalesce | ifnull |
| LIKE | Like | like | ilike(大文字小文字を区別しない) | like |
| 真 / 偽 | TrueString / FalseString | 1 / 0 | true / false | 1 / 0 |
1.5.8.1 のソースで確認した値です(SqlServerSqls.cs#L9-L42、PostgreSqlSqls.cs#L10-L42、MySqlSqls.cs#L11-L43)。IDENTITY の開始値は定義の Seed で、0 のときは 1 になります(Columns.cs#L156-L159)。
設定値(ISqlDefinitionSetting) | SQL Server | PostgreSQL | MySQL |
|---|---|---|---|
IdentifierPostfixLength | 64 | 32 | 32 |
NationalCharacterStoredSizeCoefficient | 2 | 4 | 4 |
ReducedVarcharLength | 0 | 0 | 760 |
SchemaName | 空(dbo) | 上記のとおり決まる | 空 |
データ型の自動変換
カラム定義の型名は SQL Server の型名で書かれており、PostgreSQL と MySQL では作成時に Convert() で置き換え、DB と比べるときは ConvertBack() で SQL Server の型名に戻します(PostgreSqlDataTypes.cs#L8-L45、MySqlDataTypes.cs#L7-L41)。
| 定義上の型 | PostgreSQL | MySQL |
|---|---|---|
nchar | char | char |
nvarchar(max) | text | longtext |
nvarchar | varchar | varchar(1024 以上は下記) |
bit | boolean | tinyint(1) |
varbinary | bytea | blob |
image | bytea | longblob |
datetime | timestamp(3) | datetime(3) |
MySQL では、MaxLength が 1024 以上の nvarchar を次のように作ります(MySqlColumns.cs#L9-L79)。
| 条件 | 作る型 | 理由 |
|---|---|---|
既定値(Default)がある | varchar(760)(ReducedVarcharLength) | text 型には既定値を設定できない |
| インデックスの列になっている | varchar(760) | text 型のインデックスにはサイズ制限がある |
| 上記以外 | text | より大きなデータを格納できる |
パラメータの引き継ぎ(merge)
merge は、バージョンアップのときに旧環境の App_Data/Parameters を新しいバージョンの形に合わせて引き継ぐコマンドです。1.5.8.1 のソースでの動きは次のとおりです(Starter.cs#L240-L347、PatchParameters.cs#L15-L60)。
dotnet Implem.CodeDefiner.dll merge /b <旧環境のフォルダ> [/i <新環境のフォルダ>]図を読み込み中…
/iを省略すると、新環境は Windows ならC:\web\pleasanter、それ以外は/web/pleasanter、パッチは そのフォルダのParametersPatch.zipになります。/iを指定したときも、パッチは/iのフォルダのParametersPatch.zipです(DefaultParameters.cs)。zip が無ければFileNotFoundExceptionです。- バージョンは各桁を 2 桁にそろえた文字列(
1.5.8.1→01.05.08.01)にして、zip 内のParametersPatch/{バージョン}フォルダ名と突き合わせます。フォルダは名前の昇順に並べ、旧バージョンのフォルダの次から新バージョンのフォルダまでを順に適用します。ダウングレードはできず、zip に旧バージョンのフォルダが無いバージョン(開発版など)からは引き継げません。 - パッチは JSON の差分で、
JsonDiffPatch.Net(2.5.0)の差分形式です(RFC 6902 の JSON Patch ではありません)。Parameters 配下(サブフォルダを含む)の各ファイルに、パッチフォルダ内のファイル名が同じファイルを当てます。フォルダの位置は見ないので、サブフォルダに同じ名前のファイルがあればそれにも当たります。 - 適用後のファイルは 4 スペースのインデントで書き直されます。すべて終わると
Patch application was successful.と新バージョンが表示されます。
途中で失敗すると中途半端な状態で残る
JToken.Parse() とパッチの適用に例外処理が無く、ロールバックもありません。Parameters の JSON にコメントや末尾のカンマなど JToken.Parse() が受け付けない書き方があると、そのファイルで例外になり、それまでに書き込んだファイルだけがパッチ済みの状態で残ります。配列の要素を追加・削除して使っている項目では、差分のインデックスがずれて意図しない結果になることがあります。merge の前に新環境の Parameters を残しておき、終わったら差分を確認してください。
引き継ぎのあとで起動するとき、必須のパラメータ(required: true で読むもの)が JSON として読めなければ、起動時に ParametersIllegalSyntaxException になります。一方、Quartz・BackgroundJobs など required: false で読むパラメータは、読めなくても例外にならず null(一部は既定のインスタンス)で動きます(Initializer.cs#L339-L354、Jsons.cs#L22-L32)。
バージョンごとのパッチに頼らない引き継ぎ方の設計は パラメータを差分の JSON だけで持つ を参照してください。
実践的なポイント
- 新しいカラムを追加する:
Definition_Column/に JSON ファイルを追加し、CodeDefiner のdefコマンドを実行します。 - 手動修正を保護する: メソッドの直前に
/// Fixed:コメントを付けます。マージ対象から完全に外すなら/// NotMerge:です。 - 特定テーブルだけにコードを生成する: 定義 JSON に
Include/Excludeを指定します。 - 生成をデバッグする:
/tオプションで特定の定義 ID だけを再生成して確認します。 - テンプレートで新しいカラム属性を使う: カラム定義のプロパティ名がそのまま動的プレースホルダー(
#プロパティ名#)として使えます。 - 生成物の差分を確認する: 変更のないファイルは書き込まれず、コンソールに
-と表示されます。 - バージョンアップ前に作り直されるテーブルを見る:
_rds /cで、DB を変えずに作成・移行の対象を確認できます。 - 移行後の旧テーブルに注意する: 移行で退避された
_Migrated_で始まるテーブルは自動では削除されず、DB に残り続けます。