DataReaderやDataTableにデータベースから取得したデータを、定義したエンティティクラスのオブジェクトに入れて操作したいことがあります。
今回はReflection等を使用してDataReaderやDataTableからエンティティオブジェクトに入れ替えるサンプルについて記載します。
また、DataAdapterを使用してデータベースのデータを更新できるように、エンティティオブジェクトからDataTableへの入れ替えのサンプルも合わせて記載しておきます。
目次
データからエンティティクラスのオブジェクトへ
DataReader、DataTableからエンティティオブジェクトへのマッピングは、DataReader、DataTableの列名(フィールド名)と、エンティティクラスのプロパティ名またはフィールド名をもとに行います。
エンティティのプロパティとフィールドはpublicで公開されているものに限定して取得します。
DataReader → Entities
以下に記載のソースコードはDataReaderからエンティティオブジェクトへ、データを設定するDataReaderToEntitiesメソッドです。
メソッドは引数にIDataReader型のオブジェクトを取り、指定された型のエンティティのオブジェクトをIEnumerable
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 |
/// <summary> /// DataReader とエンティティクラスをマッピングして DataReader からエンティティオブジェクトに値を設定します。 /// </summary> /// <typeparam name="T">エンティティクラスの型を指定します。</typeparam> /// <param name="dataTable">DataReader を指定します。</param> /// <returns>データが設定された IEnumerable 型のエンティティオブジェクトのコレクションを返します。</returns> public IEnumerable<T> DataReaderToEntities<T>(IDataReader dataReader) where T : class, new() { // エンティティクラスのタイプを取得。 Type type = typeof(T); // エンティティクラスの public プロパティ情報を取得。 PropertyInfo[] properties = type.GetProperties(BindingFlags.Public | BindingFlags.Instance); // エンティティクラスの public フィールド情報を取得。 FieldInfo[] fields = type.GetFields(BindingFlags.Public | BindingFlags.Instance); // エンティティオブジェクトのリストを作成。 var entities = new List<T>(); // DataReader のフィールド名を取得 HashSet<string> names = new HashSet<string>( Enumerable.Range(0, dataReader.FieldCount).Select(dataReader.GetName)); // DataReader を while で読み込む while (dataReader.Read()) { // エンティティオブジェクトのインスタンスを生成 var entity = new T(); // プロパティ情報を foreach で処理 foreach (PropertyInfo property in properties) { // DataReader にプロパティの名前があるか検査 if (names.Contains(property.Name)) { // プロパティの型を取得 Type valueType = property.PropertyType; // DataReader の値を取得 var value = ConvertTo(dataReader[property.Name], valueType); // エンティティオブジェクトのプロパティに値を設定 property.SetValue(entity, value, null); } } // フィールド情報を foreach で処理 foreach (FieldInfo field in fields) { // DataReader にフィールドの名前があるか検査 if (names.Contains(field.Name)) { // フィールドの型を取得 Type valueType = field.FieldType; // DataReader の値を取得 var value = ConvertTo(dataReader[field.Name], valueType); // エンティティオブジェクトのフィールドに値を設定 field.SetValue(entity, value); } } // エンティティオブジェクトのリストに追加 entities.Add(entity); } return entities; } |
上記のメソッドには、型引数にエンティティクラスの型を指定します。
メソッドでは指定された型からReflectionを使用して、publicなプロパティとpublicなフィールドを取得します。
エンティティオブジェクトのコレクション(List<T>)を作成し、DataReader のフィールド名を取得した後、DataReaderをwhileでループして読み込んでいきます。
DataReaderから1レコードずつ読み込みながら、取得したプロパティとフィールドをforeachでループしてDataReaderのフィールド名と一致するものがあるかを検査し、一致していれば値を取得してエンティティオブジェクトのプロパティまたはフィールドに設定しています。
whileのループでは、プロパティとフィールドの全ての値を設定した後、DataReaderの1レコードに対応するエンティティオブジェクトをエンティティオブジェクトのコレクションに追加しています。
53行目で値を取得する際にConvertToというメソッドを呼んでいますが、これは独自に作成したオブジェクトをコンバートするメソッドです。
以下にソースコードを記載します。
オブジェクトのコンバート
DataReader、DataTableの持つ列のデータ型がエンティティクラスのプロパティ、フィールドと一致していれば問題ないですが、念のためコンバートするメソッドを作成して、値を変換できるなら変換して処理します。また、値がDBNullの場合はnullに変換する処理もこのメソッドで行います。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
/// <summary> /// オブジェクトをコンバートする。 /// </summary> /// <param name="sourceObject">変換元のオブジェクト</param> /// <param name="targetType">変換後の型</param> /// <returns>変換が必要であれば変換したオブジェクトを返して必要なければ引数のオブジェクトを返します。</returns> public object ConvertTo(object sourceObject, Type targetType) { if (sourceObject != null) { // 変換前と変換後の型が同一の場合は sourceObject を返す if (sourceObject.GetType() == targetType) { return sourceObject; } // 型が違う場合はコンバート TypeConverter converter = TypeDescriptor.GetConverter(sourceObject); if (converter != null) { // コンバート可能かどうかを検査して可能なら変換 if (converter.CanConvertTo(targetType)) { return converter.ConvertTo(sourceObject, targetType); } } converter = TypeDescriptor.GetConverter(targetType); if (converter != null) { // コンバート可能かどうかを検査して可能なら変換 if (converter.CanConvertFrom(sourceObject.GetType())) { return converter.ConvertFrom(sourceObject); } } // DBNull の場合は null if (sourceObject == DBNull.Value) { return null; } } return sourceObject; } |
上記のメソッドはDBNullの場合にnullを返すので、エンティティクラスのプロパティがintやdecimal、DateTimeなどのNull許容値型になっていない場合はデータを入れることができませんので注意してください。
DataTable → Entities
以下のソースコードはDataTableからエンティティオブジェクトへ、データを設定するDataTableToEntitiesメソッドです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 |
/// <summary> /// DataTable とエンティティクラスをマッピングして DataTable からエンティティオブジェクトに値を設定します。 /// </summary> /// <typeparam name="T">エンティティクラスの型を指定します。</typeparam> /// <param name="dataTable">DataTable を指定します。</param> /// <returns>データが設定された IEnumerable 型のエンティティオブジェクトのコレクションを返します。</returns> public IEnumerable<T> DataTableToEntities<T>(DataTable dataTable) where T : class, new() { // エンティティクラスのタイプを取得 Type type = typeof(T); // エンティティクラスの public プロパティ情報を取得 PropertyInfo[] properties = type.GetProperties(BindingFlags.Public | BindingFlags.Instance); // エンティティクラスの public フィールド情報を取得 FieldInfo[] fields = type.GetFields(BindingFlags.Public | BindingFlags.Instance); // エンティティオブジェクトのリストを作成 var entities = new List<T>(); // DataTable の列名を取得 HashSet<string> names = new HashSet<string>( Enumerable.Range(0, dataTable.Columns.Count).Select(i => dataTable.Columns[i].ColumnName)); // DataTable の行を foreach で処理 foreach (DataRow dataRow in dataTable.Rows) { // エンティティオブジェクトのインスタンスを生成 var entity = new T(); // プロパティ情報を foreach で処理 foreach (PropertyInfo property in properties) { // DataTable の列にプロパティの名前があるか検査 if (names.Contains(property.Name)) { // プロパティの型を取得 Type valueType = property.PropertyType; // DataTable の値を取得 var value = ConvertTo(dataRow[property.Name], valueType); // エンティティオブジェクトのプロパティに値を設定 property.SetValue(entity, value, null); } } // フィールド情報を foreach で処理 foreach (FieldInfo field in fields) { // DataTable の列にフィールドの名前があるか検査 if (names.Contains(field.Name)) { // フィールドの型を取得 Type valueType = field.FieldType; // DataTable の値を取得 var value = ConvertTo(dataRow[field.Name], valueType); // エンティティオブジェクトのフィールドに値を設定 field.SetValue(entity, value); } } // エンティティオブジェクトのリストに追加 entities.Add(entity); } return entities; } |
DataTableとエンティティオブジェクトのデータのマッピングもDataReaderの場合とほぼ同様の処理になります。
DataReaderのデータを設定する際はwhileでDataReaderを読み込んでいましたが、DataTableではRowsプロパティをforeachでDataRowを取得してエンティティに設定します。
Entities → DataTable
上記でDataTableからエンティティにデータを設定しましたが、ここではその逆にエンティティからDataTableにデータを設定するメソッド例を記載します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 |
/// <summary> /// エンティティクラスと DataTable をマッピングしてエンティティオブジェクトから DataTable に値を設定します。 /// </summary> /// <typeparam name="T">エンティティクラスの型を指定します。</typeparam> /// <param name="entities">エンティティオブジェクトのコレクション</param> /// <returns>データが設定された データテーブルを返します。</returns> public DataTable EntitiesToDataTable<T>(IEnumerable<T> entities) where T : class, new() { // エンティティクラスのタイプを取得 Type type = typeof(T); // エンティティクラスの public プロパティ情報を取得 PropertyInfo[] properties = type.GetProperties(BindingFlags.Public | BindingFlags.Instance); // エンティティクラスの public フィールド情報を取得 FieldInfo[] fields = type.GetFields(BindingFlags.Public | BindingFlags.Instance); // DataTable を作成 var dataTable = new DataTable(); foreach (var entity in entities) { DataRow dataRow = dataTable.NewRow(); // プロパティ情報を foreach で処理 foreach (PropertyInfo property in properties) { string name = property.Name; // DataTable に列が存在しない場合は追加 if (!dataTable.Columns.Contains(name)) { dataTable.Columns.Add(name, property.PropertyType); } // DataTable の行に値を設定 dataRow[name] = property.GetValue(entity); } // フィールド情報を foreach で処理 foreach (FieldInfo field in fields) { string name = field.Name; // DataTable に列が存在しない場合は追加 if (!dataTable.Columns.Contains(name)) { dataTable.Columns.Add(name, field.FieldType); } // DataTable の行に値を設定 dataRow[name] = field.GetValue(entity); } // DataTable に行を追加 dataTable.Rows.Add(dataRow); } return dataTable; } |
上記のメソッドはエンティティクラスのプロパティとフィールド情報をもとに、DataTableの列(DataColumn)を作成してデータを設定していきます。
DataTableからエンティティオブジェクトにデータを設定する場合とは逆に、foreachでループしながらエンティティオブジェクトのコレクションを1レコードずつ読み込んでいきます。
foreachでループでは、DataTable行(DataRow)を作成します。
そして、プロパティとフィールドの名前をもとにDataTableの列(DataColumn)を作成し、DataTableの行に値を設定していきます。
すべてのプロパティとフィールドを処理した後、DataTableに行を追加します。
使用例
使用例としてSQLServer簡単なテーブルを作成してデータを取得し、取得したデータをエンティティオブジェクトへ設定する例と、エンティティオブジェクトからDataTableへデータを設定する例を記載しておきます。
使用するテーブル
データを取得するテーブルとしてCustomerというテーブルを作成します。データベースはTestDatabaseという名前のデータベースがすでに作成済みであることとします。
Customer(顧客)テーブル定義
列論理名 | 列物理名 | データ型 | 備考 |
---|---|---|---|
顧客ID | CustomerId | int | PrimaryKey |
氏名(姓) | LastName | varchar (50) | |
氏名(名) | FirstName | varchar (50) | |
登録日時 | RegisterDate | datetime |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
DROP TABLE IF EXISTS Customer GO IF NOT EXISTS(SELECT* FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[Customer]') AND type in (N'U')) BEGIN CREATE TABLE Customer ( CustomerId int NOT NULL, LastName varchar (50) NULL, FirstName varchar (50) NULL, RegisterDate datetime NULL, CONSTRAINT PK_Customer PRIMARY KEY CLUSTERED ( CustomerId ASC ) ON [PRIMARY] ) END GO |
1 2 3 4 5 6 7 8 9 10 11 12 |
insert into Customer ( CustomerId,LastName,FirstName,RegisterDate ) values ( 1, '山田', '太郎', '2001/01/10 10:15:34' ); insert into Customer ( CustomerId,LastName,FirstName,RegisterDate ) values ( 2, '田中', '花子', '2001/02/15 12:03:51' ); insert into Customer ( CustomerId,LastName,FirstName,RegisterDate ) values ( 3, '鈴木', '一郎', '2001/03/20 15:22:06' ); |
エンティティクラス
エンティティクラスは以下のように定義します。
プロパティ名はテーブルの列名を同じにします。
1 2 3 4 5 6 7 8 9 10 11 |
public class Customer { // 顧客ID public int CustomerId { get; set; } // 氏名(姓) public string LastName { get; set; } // 氏名(名) public string FirstName { get; set; } // 登録日時 public DateTime RegisterDate; } |
エンティティクラスは、顧客ID(CustomerId)、氏名(姓)(LastName)、氏名(名)(FirstName)はプロパティとして作成しています。登録日時(RegisterDate)のみフィールドとして作成しています。
Windowsフォームアプリケーションのプロジェクトを作成して、以下のようなDataGridViewを3つ並べ、ボタンを配置したフォームを作成します。
フォームのソースに上記のメソッドDataReaderToEntitiesメソッド、ConvertToメソッド、DataTableToEntitiesメソッド、EntitiesToDataTableメソッドのソースコードを実装します。
そして画面に配置したボタンのクリックイベントに、以下のソースコードを実装します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 |
private void button1_Click(object sender, EventArgs e) { DataTable dataTable = new DataTable(); IEnumerable<Customer> customersFromDataReader; IEnumerable<Customer> customersFromDataTable; // データベースへの接続文字列 string connectionString = @"Server=localhost\SQL2017;Database=TestDatabase;integrated security=True;"; // SqlConnectionのインスタンスを生成 using (var connection = new SqlConnection(connectionString)) { // コネクションを開く connection.Open(); // SqlConnectionからSqlCommandのインスタンスを生成 using (SqlCommand command = connection.CreateCommand()) { // 実行するSQL string sql = @" SELECT CustomerId, LastName, FirstName, RegisterDate FROM Customer ORDER BY CustomerId"; command.CommandText = sql; // DataReaderにデータを取得 using (SqlDataReader reader = command.ExecuteReader()) { // DataReaderからエンティティへ設定 customersFromDataReader = DataReaderToEntities<Customer>(reader); } // SqlDataAdapterのインスタンスを生成 using (SqlDataAdapter adapter = new SqlDataAdapter(command)) { // データテーブルにデータを取得 adapter.Fill(dataTable); // DataReaderからエンティティへ設定 customersFromDataTable = DataTableToEntities<Customer>(dataTable); } } // コネクションを閉じる connection.Close(); } // エンティティからDataTableへ設定 DataTable dataTableFromEntity = EntitiesToDataTable(customersFromDataTable); // DataGridViewに表示 dataGridView1.DataSource = customersFromDataReader; dataGridView2.DataSource = customersFromDataTable; dataGridView3.DataSource = dataTableFromEntity; } |
ここまで実装が終わったら、ビルドして実行(F5でデバッグ実行)します。
フォームが表示されたら、左下のボタン(button1)をクリックします。
上記のようにDataReaderからエンティティ、DataTableからエンティティ、エンティティからDataTableへデータを設定した結果がグリッドに表示されました。
今回はDataReaderやDataTableからエンティティオブジェクトに、データをマッピングして設定する実装例をご紹介しました。
システムのリプレースなどで、旧システムでDataTableやDataSetにデータを取得するライブラリがすでに実装されている場合には使えるかもしれません。
Entity FrameworkやDapperなどのO/Rマッパーで実装しなおすことは、とても手間のかかる作業です。
最近では昔ほどシステムのリプレースに大きな予算を割いてくれるクライアントは多くありません。少しでも工数を減らしてシステムのリプレースを行うための旧資産の活用方法としては、今回のサンプルは使えるのではないかと思います。