型無し DataRow を、データクラスに変換して使いたい
DataRow 使いやすくなるといいね。
目次
- 目次
- 問題提起
- サンプルデータ
- 案1、匿名型のデータクラスに変換して使う
- 案2、あらかじめ型を定義しておいて、その型にデータをセットして使う
- Integer が DBNull というか Nothing だったら 0 扱いしたくないの
- 別案
問題提起
型無し DataRow のまま、型付 DataRow みたいに扱いたい!以下なんか考えたくない!という問題に対する妥当解はあるのかを追い求めています。
- ダブルコーテーションがキー入力しづらい、指がつってしまう
- フィールド名なんて覚えていない、長いやつある、インテリセンスはどこ?
- わざわざ Object 型から任意の該当型に型変換したくない
サンプルデータ
こういう感じの DB にあるテーブルから、データを取得してきたとします。偶数だったらちゃんとしたデータ、奇数だったら DBNull が入ったデータです。
Dim dt As New DataTable("Member") dt.Columns.Add("ID", GetType(Integer)) dt.Columns.Add("Name", GetType(String)) dt.Columns.Add("Age", GetType(Integer)) dt.PrimaryKey = New DataColumn() {dt.Columns("ID")} ' データ登録 For i As Integer = 0 To 100 If i Mod 2 = 0 Then dt.Rows.Add(New Object() {i, $"taro{i}", i + 200}) Else dt.Rows.Add(New Object() {i, DBNull.Value, DBNull.Value}) End If Next
案1、匿名型のデータクラスに変換して使う
その場で匿名型を作って使う案です。でもこの案はダメです。匿名型を作る
ところで、いつもの DataRow だからです。
取得と同時に型変換するための Field 拡張メソッド、その中で DBNull に対応するための Null 許容型までは百歩譲ったとして、結局フィールド名を文字列指定してるやないかい!という点がアウトでした。
' データ取得 For Each row As DataRow In dt.Rows Dim user = New With { .ID = row.Field(Of Integer)("ID"), .Name = row.Field(Of String)("Name"), .Age = row.Field(Of Integer?)("Age") } ' DBNull の場合、String -> Nothing(=""), Integer?(Nullable(Of Integer)のこと) -> 0 Dim result As String = $"ID = {user.ID}, Name = {user.Name}, Age = {user.Age.GetValueOrDefault()}" Console.WriteLine(result) Next
出力結果
ID = 0, Name = taro0, Age = 200 ID = 1, Name = , Age = 0 ID = 2, Name = taro2, Age = 202 ID = 3, Name = , Age = 0 ID = 4, Name = taro4, Age = 204 ・・・ ・・・ ・・・
案2、あらかじめ型を定義しておいて、その型にデータをセットして使う
やっぱり無からインテリセンス対応するのは無理ゲーだったという結果です。で、アンチ型付DataSet派の自作ORマッピング - レベルエンター山本大のブログ という記事を見つけてこれやな!と思った次第です。自作拡張メソッド&クラスの定義するパターンです。コード見たほうが早いかもね。
拡張メソッドの準備
Imports System.Runtime.CompilerServices Imports System.Reflection Module DataRowExtensions <Extension()> Public Function GetRowData(Of TClass As New)(ByVal self As DataRow) As TClass Dim resultData As New TClass Dim t As Type = resultData.GetType Dim pis() As PropertyInfo = t.GetProperties For Each pi As PropertyInfo In pis If self(pi.Name) Is DBNull.Value Then pi.SetValue(resultData, Nothing) Else pi.SetValue(resultData, self(pi.Name)) End If Next Return resultData End Function End Module
ジェネリック指定している T 型のクラス(TClass)は、制約としてインスタンス生成できることを条件に指定しています。これを指定しないと戻り値となるデータクラスがインスタンス生成できません。
処理内容としては以下の通りです。
- データクラスをインスタンス生成
- データクラスのプロパティメンバー名を知りたいため(後で使う)、リフレクションして全部取得
- プロパティメンバー数分、ループ(1つ1つのプロパティに対して、値をセットしていく。DataRow からデータクラスへの入れ替え作業)
- あるプロパティの値が DBNull の場合、Nothing をセットする。クラスの場合、Nothing は規定値を返してくれるので、String → Nothing, Integer → 0, Integer? → Nothing みたいな感じで規定値がセットされる。逆に値がある場合は、そのまま値がセットされる。
DB テーブルに合わせたクラスの定義
Class MemberData Public Property ID As Integer = 0 Public Property Name As String = String.Empty Public Property Age As Integer? = 0 End Class
そしてデータ取得
' データ取得 For Each row As DataRow In dt.Rows Dim user As MemberData = row.GetRowData(Of MemberData)() ' DBNull の場合、String -> Nothing(=""), Integer?(Nullable(Of Integer)のこと) -> 0 Dim result As String = $"ID = {user.ID}, Name = {user.Name}, Age = {user.Age.GetValueOrDefault()}" Console.WriteLine(result) Next
出力結果
ID = 0, Name = taro0, Age = 200 ID = 1, Name = , Age = 0 ID = 2, Name = taro2, Age = 202 ID = 3, Name = , Age = 0 ID = 4, Name = taro4, Age = 204 ・・・ ・・・ ・・・
Integer が DBNull というか Nothing だったら 0 扱いしたくないの
という場合は、こんな感じで好きなように判定します。
' 値が無い場合 If Not user.Age.HasValue Then End If
別案
元も子もないですが、列名を文字列指定していい場合は、DataRow の拡張メソッドを作った方が早いです。
DataRow の拡張モジュール
Imports System.Runtime.CompilerServices Public Module DataRowExtensions ' DBNull と値を分けて扱いたい場合向け <Extension()> Public Function GetInteger(ByVal self As DataRow, ByVal columnName As String) As Integer? Dim result As Integer? = self.Field(Of Integer?)(columnName) Return result End Function ' DBNull を考慮したくない、しなくてもいい設計の場合向け <Extension()> Public Function GetIntegerOrDefault(ByVal self As DataRow, ByVal columnName As String) As Integer Dim result As Integer? = self.Field(Of Integer?)(columnName) Return result.GetValueOrDefault() End Function ' ... <Extension()> Public Function GetStringOrDefault(ByVal self As DataRow, ByVal columnName As String) As String ' DBNull の場合、Nothing になる Dim result As String = self.Field(Of String)(columnName) If result Is Nothing Then result = String.Empty End If Return result End Function <Extension()> Public Function GetBooleanOrDefault(ByVal self As DataRow, ByVal columnName As String) As Boolean Dim result As Boolean? = self.Field(Of Boolean?)(columnName) Return result.GetValueOrDefault() End Function <Extension()> Public Function GetDateTimeOrDefault(ByVal self As DataRow, ByVal columnName As String) As DateTime Dim result As DateTime? = self.Field(Of DateTime?)(columnName) Return result.GetValueOrDefault() End Function End Module
使う側
Module Module1 Sub Main() ' 列名とデータ登録 Dim table As New DataTable table.Columns.Add("Id", GetType(Integer)) table.Columns.Add("Name", GetType(String)) table.Columns.Add("IsMale", GetType(Boolean)) table.Columns.Add("CreatedTime", GetType(DateTime)) table.Columns.Add("UpdatedTime", GetType(DateTime)) Dim items = Enumerable.Range(1, 5) For Each item In items table.Rows.Add(New Object() { item, If(item Mod 2 = 0, $"taro{item}", $"hanako{item}"), If(item Mod 2 = 0, True, False), DateTime.Now, DateTime.Now }) Next ' 取得 For Each row As DataRow In table.Rows Dim id As Integer = row.GetIntegerOrDefault("Id") Dim name As String = row.GetStringOrDefault("Name") Dim isMale As Boolean = row.GetBooleanOrDefault("IsMale") Dim createdTime As DateTime = row.GetDateTimeOrDefault("CreatedTime") Dim updatedTime As DateTime = row.GetDateTimeOrDefault("UpdatedTime") Console.WriteLine($"Id = {id}, Name = {name,-10}, IsMale = {isMale}") Next Console.ReadKey() End Sub End Module