DataGridViewにデータバインドして編集・削除するのに最適な方法は?

WindowsFormで業務アプリを開発する場合、DataGridViewの様にデータの表示と編集作業を実際のデータソースの種類を問わずに一括してやってくれるコントロールがあると非常に便利である。

単純な編集作業ならば、これにデータバインドしてちょこちょこっとコードを書いてやるだけで、目的の仕様を達成できるのだから、ありがたくて涙が出てくる。

.NETにはデータベースにアクセスする技術が何種類かあって、どれを選ぶかによってDataGridViewとの連携のし易さもかなり変わってくる。

その辺の違いを調べる為に以下の仕様をいくつかのデータベースアクセス技術で実装して、その違いを比べてみた。

  • 表示するレコードは複数のテーブルをJOINしている(JOINで対応できない場合はビューを使う)。
  • 属性を変更できる。
  • レコードを削除できる。
ER図

ためした技術は以下、

  1. DataSet
  2. 型付けDataSet
  3. LINQ to SQL
  4. ORM(iBATIS.NET)
画面

画面ロード時にレコードを全件検索して、画面を閉じる時にレコードに対する変更をコミットする。

DataSet

まずはDataSetから(コードを書くスタイル)

MainForm.vb

Imports System.Text
Imports System.Data.SqlClient

Public Class MainForm
    Private Const CONNECTION_STRING = "Data Source=localhost\SQLEXPRESS;Initial Catalog=Sample;Persist Security Info=True;User ID=sa;Password=*******"

    Protected Overrides Sub OnLoad(ByVal e As System.EventArgs)
        Dim dataSet = New DataSet()

        Using sqlCon = New SqlConnection() With {.ConnectionString = CONNECTION_STRING}
            sqlCon.Open()

            Dim sql = New StringBuilder()
            sql.AppendLine("SELECT t1.Id,t1.Name,t2.Name AS Organization")
            sql.AppendLine("FROM Person AS t1")
            sql.AppendLine("INNER JOIN Organization AS t2")
            sql.AppendLine("  ON t1.Organization = t2.Id")

            Using sqlDa = New SqlDataAdapter(sql.ToString(), sqlCon)
                sqlDa.Fill(dataSet)
            End Using
        End Using

        DataGridView1.DataSource = dataSet.Tables(0)

        MyBase.OnLoad(e)
    End Sub

    Protected Overrides Sub OnFormClosed(ByVal e As System.Windows.Forms.FormClosedEventArgs)
        Dim dataTable = DirectCast(DataGridView1.DataSource, DataTable).GetChanges()
        ' 変更無し
        If dataTable Is Nothing Then Return

        Using sqlCon = New SqlConnection() With {.ConnectionString = CONNECTION_STRING}
            sqlCon.Open()

            Using sqlDa = New SqlDataAdapter()
                sqlDa.UpdateCommand = New SqlCommand("UPDATE Person SET Name = @Name WHERE Id = @Id", sqlCon)
                sqlDa.UpdateCommand.Parameters.Add("@Name", SqlDbType.VarChar, 50, "Name")
                sqlDa.UpdateCommand.Parameters.Add("@Id", SqlDbType.BigInt, 8, "Id")

                sqlDa.DeleteCommand = New SqlCommand("DELETE Person WHERE Id = @Id", sqlCon)
                sqlDa.DeleteCommand.Parameters.Add( _
                    New SqlParameter("@Id", SqlDbType.BigInt, 8, "Id") With {.SourceVersion = DataRowVersion.Original} _
                )
                sqlDa.Update(dataTable)
            End Using
        End Using

        MyBase.OnFormClosed(e)
    End Sub
End Class

SqlDataAdapterを使い、最低限のコードで済ませているつもり。検索するレコードはJOINするので、SqlCommandBuilderによるUpdate文などの自動生成には対応していない。
なのでUpdateCommandDeleteCommandは独自に設定している。

思っていたよりもコードがすっきりしていて、わかり易い。

型付けDataSet

次、型付けDataSet。こちらはデザイナによるコード生成で全てまかなえるので、コード無し。

SampleDataSet.xsd


GetDataメソッド


UpdatePersonメソッド


DeletePersonメソッド

あとはこいつをデザイナでフォームにぽとぺたして、DataGridViewと関連付けるだけ。

これだけで動く。問題は自分で検索のタイミングや更新のタイミングを自由に制御しづらい点。

LINQ to SQL

DLINQ

まずはLINQデザイナでテーブルをぽとぺたする。

SampleDatabase.dbml

画面側

MainForm.vb

Imports System.Data.SqlClient

Public Class MainForm
    Private _db As SampleDatabaseDataContext = New SampleDatabaseDataContext()

    Protected Overrides Sub OnLoad(ByVal e As System.EventArgs)
        Dim dataSource = From p In _db.PersonView

        BindingSource1.DataSource = dataSource

        MyBase.OnLoad(e)
    End Sub

    Protected Overrides Sub OnFormClosed(ByVal e As FormClosedEventArgs)
        Dim changeSet = _db.GetChangeSet()
        Dim deleteSet = changeSet.Deletes.Select(Function(o As PersonView) o.Id)
        Dim updateSet = changeSet.Updates.Select( _
            Function(o As PersonView) New With {.Id = o.Id, .Name = o.Name} _
        )
        For Each id In deleteSet
            _db.ExecuteCommand("DELETE Person WHERE Id = {0}", id)
        Next

        For Each o In updateSet
            _db.ExecuteCommand("UPDATE Person SET Name = {0} WHERE Id = {1}", o.Name, o.Id)
        Next

        MyBase.OnFormClosed(e)
    End Sub
End Class

更新するところがLINQでなくなっているのが痛いけど、仕方がない。*1

以外とすっきりしている。しかし、DataSetに比べ若干敷居が高いかもしれない。

ORM(iBATIS.NET)

iBATIS.NETを使った場合

まずはPersonクラスを定義する。

Person.vb
Public Class Person

    Private _id As Long
    Public Property Id() As Long
        Get
            Return _id
        End Get
        Set(ByVal value As Long)
            _id = value
        End Set
    End Property

    Private _name As String
    Public Property Name() As String
        Get
            Return _name
        End Get
        Set(ByVal value As String)
            _name = value
        End Set
    End Property

    Private _organization As String
    Public Property Organization() As String
        Get
            Return _organization
        End Get
        Set(ByVal value As String)
            _organization = value
        End Set
    End Property

End Class

マッピングファイル

Person.xml
<?xml version="1.0" encoding="utf-8" ?>
<sqlMap namespace="OrmSample"
        xmlns="http://ibatis.apache.org/mapping"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">

    <resultMaps>
        <resultMap id="Person" class="OrmSample.Person, OrmSample">
            <result column="Id" property="Id" />
            <result column="Name" property="Name" />
            <result column="Organization" property="Organization" />
        </resultMap>
    </resultMaps>
    
    <statements>
        <select id="selectPerson" resultMap="Person">
            SELECT t1.Id, t1.Name, t2.Name AS Organization
            FROM Person AS t1
            INNER JOIN Organization AS t2
              ON t1.Organization = t2.Id
        </select>
    </statements>
</sqlMap>

画面側

MainForm.vb

Imports System.ComponentModel

Imports IBatisNet.DataMapper
Imports IBatisNet.DataMapper.Configuration

Public Class MainForm
    Private _sqlMapper As ISqlMapper

    Protected Overrides Sub OnLoad(ByVal e As System.EventArgs)
        Dim sqlMapBuilder = New DomSqlMapBuilder()
        _sqlMapper = sqlMapBuilder.Configure()

        BindingSource1.DataSource = _sqlMapper.QueryForList(Of Person)("selectPerson", Nothing)

        MyBase.OnLoad(e)
    End Sub

    Protected Overrides Sub OnFormClosed(ByVal e As FormClosedEventArgs)
        ' 面倒くさい

        MyBase.OnFormClosed(e)
    End Sub
End Class

どのオブジェクトが変更されたかを調べるのが面倒くさかったので更新系は実装していない。だいぶ面倒くさいのは確か。

結果をまとめると、

技術 参照系 更新系 総合点
(5点満点)
雑感
DataSet 4 動的なSQLには不向き
型付けDataSet 4 動的なSQLには不向き。
デザイナを使用するため、画面のコードに直接SQLを書き込む必要がある。
LINQ to SQL 3.5 イカしてる。
ORM 2.5 とにかく面倒くさい。
使うならば変更を検知するなんらかの仕組みが必要

参照系ならばどの技術を使っても大差は無い(動的な条件に強いか弱いかぐらい)。

逆に更新系では、変更を検知する仕組みが初めから組み込まれているDataSetやDLINQはかなり強いので、ORMなんかのPONOをエンティティとして使うフレームワークはどうしても弱くなってしまう。

まぁ、DataGridView使って楽したかったらDataSetを使えって事ですね。結論でました。

*1:LINQはビューの更新に対応していない。