注意
※この記事は2018年に書いた記事です。
2020年の段階では、言語の発展終了が発表されています。
(サポートの終了ではないですが、いつサポート終了が発表されるか予測はできません。)
選択が可能であればC#.Net Coreの選択のほうが良いと思います。
以下過去に書いた記事
2018年の今、VB6をVB.Netにコンバートをしている(しようとしている)人は一定数いると思います。
私も2年ほど前にVB6のコンバートを行い、現在は.Netを主に書いています。2年前に知っておきたかったことをこれから今山場を迎える人に向けて紹介したいと思います。
本記事で対象とする方
- ORM(O/R Mapper)を知らない(使っていない)
- LINQを使ったことない
- これからVB6からコンバートをする予定
注意 以下に記述しているコードは、開発した環境のコードを思い出しながら、動作環境がない状態で記述をしているので、適切に動かない場合があります。また、例外処理は適時行ってください。
ORM(O/R Mapper)とは
簡単に説明すれば、DBとPG内のデータ構造を紐付け、データ取得・編集を容易にするものです。この記事を見ている方には、コンバートだけをしているだけだから、入れるの大変そう、覚えるの大変そう。などあると思います。(私も当時同じ考えでした)
そんな方々におすすめをしたいのは、Dapperです。
Dapperを理解する際に役立ったまとめ
https://qiita.com/araiman/items/36c3034464e42704bb21
Dapper のクエリ
https://qiita.com/masakura/items/3409a766e46580a5ad99
Dapperは、Micro ORMと言われるもので、EntityFrameworkなどと比べて薄い機能しかありません。しかし既存のソースコードをコンバートし、動くように手を加えている場合や軽い開発には非常に便利です。
ORMを使わない普通の場合のデータのとり方
ORMを使っていない場合は、主にDataReaderを回すか、DataSet→DataTableを使うかの2パターンだと思います。
こんな感じに書くはずという"イメージ"を以下に記述します。
'DataReaderの場合
Dim connection As IDbConnection '環境に応じて
Dim command As IDbCommand '環境に応じて
command.Connection = connection
command.CommandText = "select a, b, c from table"
Dim reader As IDataReader = command.ExecuteReader
Do While reader.Read
Dim a = reader.GetValue("a")
Dim b = reader.GetValue("b")
Dim c = reader.GetValue("c")
'~
Loop
'DataSetの場合
Dim connection As IDbConnection '環境に応じて
Dim command As IDbCommand '環境に応じて
command.Connection = connection
command.CommandText = "select a, b, c from table"
Dim adapter As IDbDataAdapter = New ~Adapter() '環境に応じて
adapter.SelectCommand = command
Dim ds As New DataSet
adapter.Fill(ds)
Dim dt As DataTable = ds.Tables(0)
For Each row As DataRow In dt.Rows
Dim a As String = row.Item("a")
Dim b As String = row.Item("B")
Dim c As String = row.Item("c")
'~
Next
最終的に取得した値は、"構造体なり変数に"入れていると思います。
Dapperを使用すると、構造体に一個一個設定する手間が省けます。
ちなみにDapperは、NugetでDapperと検索しインストールすることで、プロジェクトに追加されます。
その後、Dapperをインポートしてください。
'データを設定するクラス(構造体)の定義
Public Class Table
Public Property a As String
Public Property b As Integer
Public Property c As Decimal
End Class
'クラスを定義時
Dim list As List(Of Table)
Using connection As IDbConnection = testConection '環境に応じて
list = connection.Query(Of Table)("select a, b, c from table")
End Using
'クラスを定義しない時
Dim list As List(Of Object)
Using connection As IDbConnection = testConection '環境に応じて
list = connection.Query("select a, b, c from table")
'例 1行目のa列を取りたいとき
list.FirstOrDefaut.a
End Using
事前にデータを設定するクラスを定義しても、定義しなくてもリストに設定された状態のものが間違いなく作成されるというのは、開発する上で非常に手間を短縮することができます。
クラスを定義する場合は、テーブルの項目数が増えても対応しなくても動く、対応してもリコンパイルで大丈夫というメリットがあります。(保証するものではありません)
クラスを定義しないパターンについては、少ない項目数を取得する場合や、集計を行う場合が向いていると思います。
パッケージを使えない開発もあると思います。その場合は、以下のような記述を行うことでクラスを定義しないパターンの対応が可能です。
※.Net4.0以降での動作となります。
'DataReaderを取得したあと
Dim list As New List(Of Object)
Do While reader.Read
'設定する変数の定義(動的拡張に対応)
Dim obj As Object = Dynamic.ExpandoObject
Dim dic As IDictionary(Of String, Object) = obj
'データの設定
For index As Integer = 0 To reader.FieldCount - 1
If Not reader.IsDBNull(index) Then
dic.Add(reader.GetName(index), reader.GetValue(index))
Else
dic.Add(reader.GetName(index), Nothing)
End If
Next
list.Add(obj)
Loop
ExpandoObjectを使うことで、list.FirstOrDefaut.aのような取得が行えるようになります。
クラスに設定した状態で取得したい場合は、以下のように記述することで取得が行なえます。
※4.0ではpi.SetValueにもう一つ引数が必要で、Nothingで大丈夫なはずです。
Public Function Query(Of T)(reader As IDataReader) As List(Of T)
'Tクラスのインスタンスを作成
Dim t_instance As T = Activator.CreateInstance(Of T)
'Tクラスのプロパティを取得
Dim propertyinfos As PropertyInfo() = t_instance.GetType.GetProperties
Dim columns As New Dictionary(Of Integer, PropertyInfo)
'DBのフィールドの列IndexとTクラスのプロパティ名を一致させる
For Each propertyinfo As PropertyInfo In propertyinfos
Try
'GetOrdinalで列の列Indexを取得する
Dim index As Integer = reader.GetOrdinal(propertyinfo.Name)
columns.Add(index, propertyinfo)
Catch ex As Exception
'フィールドになければ例外に入るはず
End Try
Next
Dim list As New List(Of T)
Do While reader.Read
Dim obj As T = Activator.CreateInstance(Of T)
'列情報を、Tクラスのインスタンスに設定する
For Each column As KeyValuePair(Of Integer, PropertyInfo) In columns
Dim pi As PropertyInfo = column.Value
Dim value As Object = reader.GetValue(column.Key)
'DBNull以外の場合は、対象のプロパティから一致する列情報を設定
If Not value Is DBNull.Value Then
pi.SetValue(obj, Convert.ChangeType(reader.GetValue(column.Key), pi.GetType))
End If
Next
list.Add(obj)
Loop
Return list
End Function
サンプルを提供しても意味不明と言われて許されない場合は、エクソダスしましょう…
IDB~やらIData~でコーディングを行うことで、OracleやPostgreSQLなどDBが変わっても簡単な修正で対応が可能となります。
今回はDBの取得のみを記述しましたが、PropertyInfoの中身をうまく回すことで、違う項目数のクラスの同じプロパティ名の中身をコピーという処理を書くこともできます。
そのような機能をもったパッケージもすでに提供されています。
補足 Query(Of T) のTってなんだよという人がいると思います。(2年前の私
Tの部分に設定したクラス(TypeProperty)が、処理内のTと宣言した箇所で適用されます。
List(Of String)と宣言すれば、Stringのリストがされるのと同様に、任意のクラスでも同じようなことができます。
設定されるクラスが違うけど、処理したい内容は一緒という場合などに有効です。
LINQについて
Qiita他の記事を見てもそうですが、LINQ取っ付きにくかったけど、使ってみるとすごい便利という投稿が結構あります。私も最初は大変でしたが、実際に使うと非常に便利でした。
上記のORMと組み合わせて使用すると、非常に便利です。
はじめての LINQ
https://qiita.com/nskydiving/items/c9c47c1e48ea365f8995
'サンプル
Public Class Table
Public Property a As String
Public Property b As Integer
Public Property c As Decimal
End Class
'例 対象行をすべて取得し、特定条件列の合計も計算したい
Dim list = conn.Query(Of Table)("select a,b,c from table where~")
'LINQを使わない場合
Dim b_sum As Integer = 0
For Each item As Table in list
'a列の先頭がaで始まる場合
If item.a.StartWith("a") Then
b_sum += item.b
End If
Next
'LINQを使う場合
'a列の先頭がaで始まる場合、bの合計を求める①
Dim b_sum As Integer = 0
For Each item in list.Where(Function(m) m.a.StartWith("a"))
b_sum += item.b
Next
'a列の先頭がaで始まる場合、bの合計を求める②
Dim b_sum As Integer = list.Where(Function(m) m.a.StartWith("a")).Sum(Function(m) m.b)
'Function(m)と書いていますが、mの部分はなんでも大丈夫です。
LINQでToListなどを行う場合など、適切に処理を記述しないと速度が出ない場合があります。
(もちろん適切に書いても普通のForが早いこともあります)
最初のうちは、すでに記述済みの場所をコメントアウトなどして置き換えてみるという方法でもいいと思います。
最後に
パッケージを使える環境の場合は、Excelの場合はClosedXML、ログ関連ならLog4Netなどがあります。
そういうパッケージをうまく探して使っていったほうがいいと思います。
LINQ以外にも.Netでは便利な記述方法がたくさんあります。
以下に自分が役立ったものをリストアップします。最初は難しいとは思いますが、使い慣れると便利なので、ぜひ覚えてください。
- ヘッダーコメント (http://kokorona.com/wp/it/vb-dot-net-header-comment/)
- Dim obj = New With {.a = "a", .b = 2} のような記述が可能(Dapperで結構使います)
- 短絡評価(AndAlso、OrElse)
- Task、Parallel.For/ForEach
- ラムダ式(無名関数)
- String.IsNullOrEmpty/IsNullOrWhiteSpace
- String.Join (主にCSV)
- 文字列の連結の簡略化 $"日時:{Now}" のように&でくっつけなくても良い
もし上記以外で非常に役立った書き方や、パッケージなどがあればコメントにお願いします。
またもっと簡略化できるという方もコメントで教えてください。