Entity Frameworkって便利ですよね
存在を知って、初めて使ったときの感動は今でも忘れられません。
そんなEntity Frameworkを使うためにはEntity Data Modelクラスが必要です。
これもまた便利なもので、既存のDBをウィザードに従って設定するだけでVisual Studioが勝手にModelクラスを生成してくれます。
カラムの変更などの対応も簡単です。
趣旨
便利は便利なのですが、ふと疑問に思いました。
「これってどうやって生成しているんだろう」
「C#で実装してるのかな?それともC++とか?」
「よし、クラスファイルと言ってもテキストファイルなんだからC#で作ってみよう」
というのが今回の趣旨です。
環境
- OS: Windows 7 64bit
- IDE: Visual Studio 2015 Community
- DB: SQLite
そもそも可能なの?
Google先生に聞いてみました。
どうやらCodeDOM(Code Document Object Model)を使用することで実現できそうだということがわかりました。
「もしかしてString結合でやらなきゃできないのかな...」という不安はひとまず解消
手段はわかったので実装してみます。
実装
凝ったモノを作るつもりではないので、コンソールアプリで作成
DBはお手軽なSQLiteを使用します。
何はともあれNuGet
SQLはベタ書きするのでSQLite.Core
で十分です。
PM> Install-Package System.Data.SQLite.Core
DB名はsample.db
とします。
まずは以下のようなテーブル作成メソッドを作成します。
/// <summary>
/// employeeテーブルを作成
/// </summary>
/// <param name="conn"></param>
static void CreateTable(SQLiteConnection conn) {
using (var command = conn.CreateCommand()) {
command.CommandText = @"
CREATE TABLE IF NOT EXISTS employee (
id INTEGER PRIMARY KEY AUTOINCREMENT
,name TEXT
,age INTEGER
,height REAL
,weight REAL
)";
command.ExecuteNonQuery();
}
}
次にテーブルのカラム情報を格納するクラスを作成します。
とは言っても、必要な情報はカラム名とデータ型のみです。
/// <summary>
/// テーブル情報格納クラス
/// </summary>
class TableInfo {
public string ColumnName { get; set; }
public string DataType { get; set; }
}
テーブル作成、カラム情報格納クラスができたところでテーブルの情報を取得します。
恥ずかしながら、SQLiteのテーブル情報取得方法は知らなかったので、こちらの記事を参考にさせていただきました。
PRAGMA TABLE_INFO(TABLE_NAME);
で取得できるようです。
取得できる内容は以下
カラム名 | |
---|---|
cid | カラムインデックス |
column | カラム名 |
type | データ型 |
notnull | NOT NULLなら「1」、NULL許容なら「0」 |
dflt_value | 初期値 |
pk | 主キーなら「1」、それ以外は「0」 |
今回は上記記事を参考にcolumn とtype だけ取得します。 |
|
先程作成したTableInfo 型のListを作成します。 |
/// <summary>
/// employeeテーブルの列情報を取得
/// </summary>
/// <param name="conn"></param>
/// <returns></returns>
static List<TableInfo> GetTableInfo(SQLiteConnection conn) {
using (var command = conn.CreateCommand()) {
//employeeテーブル情報取得
command.CommandText = "PRAGMA TABLE_INFO(employee);";
var result = new List<TableInfo>();
using (var reader = command.ExecuteReader()) {
while (reader.Read()) {
//name: カラム名, type: データ型
var tableInfo = new TableInfo {
ColumnName = reader["name"].ToString(),
DataType = reader["type"].ToString()
};
result.Add(tableInfo);
}
}
return result;
}
}
ひとまずテーブルが正しく作成され、テーブル情報が取得できているか確認してみましょう。
static void Main(string[] args) {
List<TableInfo> tableInfo;
using (var conn = new SQLiteConnection("Data Source=sample.db")) {
conn.Open();
CreateTable(conn);
tableInfo = GetTableInfo(conn);
conn.Close();
}
foreach (var t in tableInfo) {
Console.WriteLine($"column_name: { t.ColumnName }, data_type: { t.DataType }");
}
}
では今回のメインロジックを組んでいきます。
まずはSQLiteのデータ型をC#のデータ型に変換するメソッドを作成
/// <summary>
/// SQLiteのデータ型に合った型を返す
/// </summary>
/// <param name="dataType"></param>
/// <returns></returns>
private Type GetPropertyDataType(string dataType) {
//今回はBLOB型を無視します。
switch (dataType) {
case "TEXT":
return typeof(string);
case "INTEGER":
return typeof(int);
case "REAL":
return typeof(double);
default:
throw new ArgumentException($"型が不明です: { dataType }");
}
}
BLOB型については、どう扱うべきか迷ったので面倒くさいし無視することにしました。
次は実際にEntityクラスを生成するメソッドを実装します。
CodeCompileUnit
クラス等を使用して実装します。
最終的にはStreamWriter
でファイル出力します。
/// <summary>
/// テーブルに対応するクラスを作成する
/// </summary>
/// <param name="nameSpace"></param>
/// <param name="className"></param>
/// <param name="tableInfo"></param>
public void GenerateEntity(string nameSpace, string className, List<TableInfo> tableInfo) {
var compileUnit = new CodeCompileUnit();
//名前空間を設定
var name = new CodeNamespace(nameSpace);
compileUnit.Namespaces.Add(name);
//クラス定義 引数にはクラス名を設定
var classType = new CodeTypeDeclaration(className);
foreach (var t in tableInfo) {
//propertyを定義
var field = new CodeMemberField {
Attributes = MemberAttributes.Public | MemberAttributes.Final,
Name = $"{ t.ColumnName } {{ get; set; }}",
Type = new CodeTypeReference(this.GetPropertyDataType(t.DataType)),
};
classType.Members.Add(field);
}
name.Types.Add(classType);
var provider = new CSharpCodeProvider();
//CSharpCodeProvider().FileExtensionで「cs」拡張子を取得できます
var fileName = $"{ classType.Name }.{ provider.FileExtension }";
//Entityクラスを出力
using (var writer = File.CreateText(fileName)) {
provider.GenerateCodeFromCompileUnit(
compileUnit,
writer,
new CodeGeneratorOptions()
);
}
}
このメソッドをProgram.cs
のMain()
で呼んであげます。
static void Main(string[] args) {
List<TableInfo> tableInfo;
using (var conn = new SQLiteConnection("Data Source=sample.db")) {
conn.Open();
CreateTable(conn);
tableInfo = GetTableInfo(conn);
conn.Close();
}
new Generate().GenerateEntity("Namespace1", "EmployeeEntity", tableInfo);
}
「よすよす、これでOK、楽勝だった」と思っていましたが、実際に生成されたEntityクラスはと言うと...
//------------------------------------------------------------------------------
// <auto-generated>
// このコードはツールによって生成されました。
// ランタイム バージョン:4.0.30319.42000
//
// このファイルへの変更は、以下の状況下で不正な動作の原因になったり、
// コードが再生成されるときに損失したりします。
// </auto-generated>
//------------------------------------------------------------------------------
namespace Namespace1 {
public class EmployeeEntity {
public int id { get; set; };
public string name { get; set; };
public int age { get; set; };
public double height { get; set; };
public double weight { get; set; };
}
}
ん?プロパティって最後にセミコロンいらないよね??
Visual Studioで開いてみると、案の定怒られました。
このセミコロンでだいぶハマりました。
ググっても解決策出てこないしどうしよう...
とりあえず力技でなんとかする
最初のほうでも書きましたが、クラスファイルといっても所詮テキストファイルです。
今回のケースであれば、ファイルを読み込んで};
を}
に置換してやればなんとかなります。
そして以下のメソッドを追加
/// <summary>
/// 「public int id { get; set; };」のように末尾にセミコロンが付いてしまうので削除する
/// </summary>
/// <param name="fileName"></param>
private void DeletePropertysEndSemicolon(string fileName) {
//ファイルを読込み、波括弧末尾のセミコロンを削除
string fileDetail = File.ReadAllText(fileName).Replace("};", "}");
//再度ファイルに書き出す
using (var writer = new StreamWriter(fileName)) {
writer.Write(fileDetail);
}
}
我ながらゴリ押しが過ぎる気がしますが、これしか思いつきませんでした。
正しい方法をご存知の方がいらっしゃればご教示頂きたい...
と、とりあえずDeletePropertysEndSemicolon()
メソッドをGenerateEntity()
メソッドの最終行に追加してあげれば完成です(震え声)
/// <summary>
/// テーブルに対応するクラスを作成する
/// </summary>
/// <param name="nameSpace"></param>
/// <param name="className"></param>
/// <param name="tableInfo"></param>
public void GenerateEntity(string nameSpace, string className, List<TableInfo> tableInfo) {
var compileUnit = new CodeCompileUnit();
//名前空間を設定
var name = new CodeNamespace(nameSpace);
compileUnit.Namespaces.Add(name);
//クラス定義 引数にはクラス名を設定
var classType = new CodeTypeDeclaration(className);
foreach (var t in tableInfo) {
//propertyを定義
var field = new CodeMemberField {
Attributes = MemberAttributes.Public | MemberAttributes.Final,
Name = $"{ t.ColumnName } {{ get; set; }}",
Type = new CodeTypeReference(this.GetPropertyDataType(t.DataType)),
};
classType.Members.Add(field);
}
name.Types.Add(classType);
var provider = new CSharpCodeProvider();
//CSharpCodeProvider().FileExtensionで「cs」拡張子を取得できます
var fileName = $"{ classType.Name }.{ provider.FileExtension }";
//Entityクラスを出力
using (var writer = File.CreateText(fileName)) {
provider.GenerateCodeFromCompileUnit(compileUnit, writer, new CodeGeneratorOptions());
}
//各プロパティ末尾のセミコロン削除
this.DeletePropertysEndSemicolon(fileName);
}
実行して生成されたクラスが以下
//------------------------------------------------------------------------------
// <auto-generated>
// このコードはツールによって生成されました。
// ランタイム バージョン:4.0.30319.42000
//
// このファイルへの変更は、以下の状況下で不正な動作の原因になったり、
// コードが再生成されるときに損失したりします。
// </auto-generated>
//------------------------------------------------------------------------------
namespace Namespace1 {
public class EmployeeEntity {
public int id { get; set; }
public string name { get; set; }
public int age { get; set; }
public double height { get; set; }
public double weight { get; set; }
}
}
期待通りの結果です。
ただ、やはりセミコロンの部分が納得いきません...
一応string
で定義する方法もあったのですが、public int id { get; set; }
とベタ書きしなければならないようだったので、今回は避けました。(インデントも自分で入れないといけないし)
おまけ
クラスを生成、ビルドしてexe
ファイルも作れるみたいなのでHello World
とコンソール出力するアプリを生成してみます。
public void GenerateHelloWorldExe() {
var compileUnit = new CodeCompileUnit();
//名前空間を設定
var name = new CodeNamespace("Namespace1");
//Systemをインポート
name.Imports.Add(new CodeNamespaceImport("System"));
//クラス定義 引数にはクラス名を設定
var classType = new CodeTypeDeclaration("HelloWorld");
//public static void Main() を作成
var method = new CodeEntryPointMethod();
//Console.WriteLine("Hello World!"); を定義
var writeLine = new CodeMethodInvokeExpression(
new CodeTypeReferenceExpression("Console"), "WriteLine",
new CodePrimitiveExpression("Hello World!")
);
//Console.ReadKey(); を定義
var readKey = new CodeMethodInvokeExpression(
new CodeTypeReferenceExpression("Console"), "ReadKey"
);
//上記で定義した処理をMain()に追加する
method.Statements.Add(new CodeExpressionStatement(writeLine));
method.Statements.Add(new CodeExpressionStatement(readKey));
classType.Members.Add(method);
name.Types.Add(classType);
compileUnit.Namespaces.Add(name);
var provider = new CSharpCodeProvider();
//確認のため生成コードをコンソールへ出力
provider.GenerateCodeFromCompileUnit(compileUnit, Console.Out, new CodeGeneratorOptions());
//実行ファイル(HelloWorld.exe)を作成
var param = new CompilerParameters { GenerateExecutable = true, OutputAssembly = "HelloWorld.exe" };
CompilerResults result = provider.CompileAssemblyFromDom(param, new CodeCompileUnit[] { compileUnit });
}
HelloWorld.exe
を実行してみます。
うまく動いてくれました。
実際に作ってみて
クラスを生成する手法を知ったからといって、これと言った使いみちが思いつきません。
もう既に比べるのもおこがましい程高機能なツールがあるわけですし、クラスを動的に作ることも極々稀だと思います。
でも、「こんな感じで実装してるのかな?」って考えるのは楽しいですね。
一応GitHubにアップしているので、死ぬほど暇で本当にやることが無い人がいらっしゃったら見てやってください。