選択行の削除

タグの編集
投稿者 ソッケル  (社会人) 投稿日時 2017/7/31 10:22:59
商品登録プログラムを作成しており、データベースと繋いでテーブルをデータグリットビューに表示しています。
そこで、削除ボタンを設置してそのボタンをクリックしたらデータグリットビューに表示しているテーブルのデータを削除させたいです。
 現在選択している行を削除させるようにしたいのですが、選択行を取得する方法がわかりません。
また、DELETE文でその行を削除し、データベースの方にも反映させるようにしたいです。
投稿者 ソッケル  (社会人) 投稿日時 2017/7/31 10:28:17
現在のソースはこんな感じです。
 '消去ボタン
    Private Sub Button3_Click(sender As Object, e As EventArgs) Handles Button3.Click
        Dim conn As New MySqlConnection
        Dim myconnectionstring As String = Module1.SQL
        Dim table As New DataTable

        conn.ConnectionString = myconnectionstring
        Try
            '接続 
            conn.Open()
            table = DataGridView1.DataSource
            Dim delete As String = "DELETE FROM T_ITEM WHERE ITEM_ID ='" & DataGridView1.CurrentCell.RowIndex & "'"
            Dim mycomand As New MySqlCommand(delete, conn)

            ' データを取得
            Dim Ds As New DataSet
            Dim result As DialogResult = MessageBox.Show("消去してもよろしいですか?", _
                                                         "質問", _
                                                         MessageBoxButtons.YesNo, _
                                                         MessageBoxIcon.Exclamation, _
                                                         MessageBoxDefaultButton.Button2)

            '何が選択されたか調べる 
            If result = DialogResult.Yes Then
                '「はい」が選択されたら
                mycomand.ExecuteNonQuery()
            End If
        Catch ex As Exception
            MsgBox(ex.Message)
        Finally
            '閉じる
            conn.Close()
        End Try

    End Sub
投稿者 魔界の仮面弁士  (社会人) 投稿日時 2017/7/31 15:01:00
> 現在選択している行を削除させるようにしたいのですが、選択行を取得する方法がわかりません。

行全体を選択していれば、そのまま Delete キーを押すことで
該当行が削除状態にマークされますが、それでは駄目でしょうか。


削除マークの付いた行(RowState プロパティが Deleted な行)というものは、
バインドしている DataGridView 上では表示されなくなりますし、
その DataTable を MySqlDataAdapter に渡せば、削除マークの付いた行に対する
DELETE SQL が自動的に呼び出せれ、実際にデータベース上からも削除されます。


DataGridView で「行全体を選択」するには:
 SelectionMode プロパティを FullRowSelect にしておき
 任意のセルをマウスもしくはキーボード操作で選択する
もしくは:
 SelectionMode プロパティを RowHeaderSelect にしておき
 RowHeaderVisible = True にした状態で、
 左端の ► のセルを選択する
もしくは:
 SelectionMode プロパティを RowHeaderSelect にしておき
 [Shift]+[Space] を押下する
のいずれかの操作で行います。

Ctrl キーと併用すれば、複数の行を同時に選択することもできます。


またユーザー操作ではなく、プログラムから削除マークを付けたいのであれば、
前回の回答のように、選択行に対して個別に Delete メソッドを呼び出していく必要があります。


【行全体が選択されているものを対象とする場合】
DataGridView1.EndEdit()
For Each gridRow As DataGridViewRow In DataGridView1.SelectedRows
    If Not gridRow.IsNewRow Then   'IsNewRow 判定するかは御好みで 
        DirectCast(gridRow.DataBoundItem, DataRowView).Delete()
    End If
Next



【セルの選択状態とは関係なく、現在のカーソル行を対象とする場合】
Dim bnd = Me.BindingContext(Me.DataGridView1.DataSource, Me.DataGridView1.DataMember)
Dim rowView = DirectCast(bnd.Current, DataRowView)
If rowView IsNot Nothing Then
    rowView.Delete()
End If
もしくは
Dim gridRow = DataGridView1.CurrentRow
If gridRow IsNot Nothing Then
  Dim rowView = TryCast(gridRow.DataBoundItem, DataRowView)
  If rowView IsNot Nothing Then
      rowView.Delete()
  End If
End If



【現在のカーソル行とは関係なく、選択されているセルの行を対象とする場合】
Dim rows = From cell As DataGridViewCell In DataGridView1.SelectedCells
           Let rowView = DirectCast(cell.OwningRow.DataBoundItem, DataRowView)
           Where rowView IsNot Nothing
           Select rowView Distinct

For Each rowView In rows
    rowView.Delete()
Next




> DELETE文でその行を削除し、データベースの方にも反映させるようにしたいです。

削除マークの付いた行に対する DELETE 文を手動で生成する場合は、こんな感じです。

'パラメーター化された DELETE 文を構築 
Dim sql As String = "DELETE FROM T_ITEM WHERE ITEM_ID = @OLD_ITEM_ID"
Dim delCmd As New OleDb.OleDbCommand(sql, conn)

'列の型に合わせること 
Dim oldId As MySqlParameter = delCmd.Parameters.Add("@OLD_ITEM_ID", MySqlDbType.VarChar, 8)

'DataViewRowState を指定して、削除マークの付いた行を取り出す 
Dim deletedRows As New DataView(dt, """", DataViewRowState.Deleted)
For Each rowView As DataRowView In deletedRows
    Dim row As DataRow = rowView.Row

    'DataRowVersion を指定して、該当行の削除前の値を取り出す 
    Dim OLD_ITEM_ID As Object = row("ITEM_ID", DataRowVersion.Original)

    '削除前の主キー値を使って DELETE 文を実行 
    oldId.Value = OLD_ITEM_ID
    delCmd.ExecuteNonQuery()
Next
投稿者 ソッケル  (社会人) 投稿日時 2017/7/31 15:51:17
回答ありがとうございます。

>行全体を選択していれば、そのまま Delete キーを押すことで
該当行が削除状態にマークされますが、それでは駄目でしょうか

ユーザーが行を選択して消去ボタンを押下したらデータを消せるという仕様にしたいです。。。

今のソースでWHERE句で自分で条件を指定すればDBの方のデータも消えるのですが、あくまで選択された行を削除したいのですが、どこを修正すれば選択行をWHERE句に指定することができますでしょうか?
投稿者 魔界の仮面弁士  (社会人) 投稿日時 2017/7/31 19:19:09
削除手順については回答済みなので、あとは御自信で調べて
実装するフェーズだと思っています。

データベース上のデータを削除する方法も、「MySqlDataAdapter.Update を呼び出す方法」と、
「MySqlParameter を指定して、MySqlCommand.ExecuteNonQuery を呼び出す方法」の
2 パターンを回答していますね。


> あくまで選択された行を削除したいのですが、

こちらは、何をもって「行を選択したと看做すのか」次第ですね。

そのため先のサンプルでは、ユーザーに Delete キーを押させるのではなく、
『ユーザー操作ではなく、プログラムから削除』のための手順として、
>> 【行全体が選択されているものを対象とする場合】
>> 【セルの選択状態とは関係なく、現在のカーソル行を対象とする場合】
>> 【現在のカーソル行とは関係なく、選択されているセルの行を対象とする場合】
の 3 パターンを用意しています。

それぞれどういう違いがあるのかを実際に試してみた上で、
実際の要件に近いものを選んで、それを消去ボタンの Click イベントに
記述すれば良いのでは無いでしょうか。

あとは、DataGridView 上に チェックボックス列を追加しておき
☑の有無によって、削除対象行を判断すると言う手もありますね。
(この場合、DataTable 上に Boolean 型の列を追加するのが手っ取り早いです)


> どこを修正すれば選択行をWHERE句に指定することができますでしょうか? 
それだと質問ではなく、もはや作成依頼になってしまいますね…。(^^;

継続質問ということであれば一向に構わないのですが、
作成依頼となってしまうようなら、申し訳ないですが私は手を引かせてもらいます。


一応、選択行の DataRowView あるいは DataRow を得る方法は示しているので、
そこから各列の値を取得することはできるかと思いますが、如何でしょうか。

あるいは、先の回答のどの部分の意味が分からないかを伝えて頂ければ、
その点について追加説明いたしますよ。


なお、先の回答の『削除状態』の話の意味が分からない場合には、先に
下記の「行の状態とバージョン」の解説に目を通しておいた方が良いかもしれません。
https://msdn.microsoft.com/ja-jp/library/ww3k31w0.aspx
投稿者 ソッケル  (社会人) 投稿日時 2017/8/1 15:17:22
教えていただいた内容と違うやり方で解決できたのですが、新たに疑問が出てきたので質問させてください。


    '消去ボタン
    Private Sub Button3_Click(sender As Object, e As EventArgs) Handles btn_Delete.Click
        Dim conn As New MySqlConnection(Module1.SQL)
        Dim table As New DataTable
        
        Try

            'DB接続 
            conn.Open()
            Dim delete As String = "DELETE FROM T_ITEM WHERE NUMBER =  '" & DataGridView1.CurrentRow.Cells("注文番号").Value & "'"
            Dim mycomand As New MySqlCommand(delete, conn)

            Dim result As DialogResult = MessageBox.Show("消去してもよろしいですか?", _
                                                         "質問", _
                                                         MessageBoxButtons.YesNo, _
                                                         MessageBoxIcon.Exclamation, _
                                                         MessageBoxDefaultButton.Button2)

            '何が選択されたか調べる 
            If result = DialogResult.Yes Then
                '「はい」が選択された場合
                DataGridView1.Rows.RemoveAt(DataGridView1.CurrentCell.RowIndex)
                mycomand.ExecuteNonQuery()
            End If

            '合計の表示
            sum()

        Catch ex As Exception
            MsgBox(ex.Message)
        Finally
            '閉じる
            conn.Close()
        End Try

    End Sub

これで選択した行を削除できるようになったのですが、複数行選択された状態で削除ボタンが押下された場合、1行しか削除されませんでした。
複数行でも削除できるようにするにはfor文などを使用するのが正しいですか?
投稿者 (削除されました)  () 投稿日時 2017/8/1 19:05:48
(削除されました)
投稿者 魔界の仮面弁士  (社会人) 投稿日時 2017/8/1 19:13:46
最初の質問では WHERE ITEM_ID = ~ だったのに
今回のコードは WHERE NUMBER = ~ ですね。
どちらが正しいのでしょうか。


> 1行しか削除されませんでした。
それは、CurrentCell や CurrentRow を使っているからですね。


> 複数行でも削除できるようにするにはfor文などを使用するのが正しいですか? 
DataGridView 上の複数行を取得する方法は、既に具体例付きで回答しています。
すなわち
 【行全体が選択されているものを対象とする場合】
 【現在のカーソル行とは関係なく、選択されているセルの行を対象とする場合】
の 2 つのコードの事です。For Each のループ処理が書かれていましたよね?


> 複数行選択された状態で削除ボタンが押下された場合、
そもそも「選択された行」と「現在の行」は無関係です。
たとえば下記の図の場合、このような状態を意味します。
 CurrentCell   は 1 件。[行1列0]
 CurrentRow    は 1 件。[行1]
 SelectedRows  は 2 件。[行3],[行4]
 SelectedCells は 5 件。[行3列0],[行3列1],[行4列0],[行4列1],[行6列1]



さてこの場合、削除したいのは以下のいずれでしょうか?
 (A) 1 件削除。行1 のみ。CurrentRow を使う。(現状がこれですよね)
 (B) 2 件削除。行3 と 行4。SelectedRows を使います。
 (C) 3 件削除。行3 と 行4 と 行6。SelectedCells を集約してから使います。

これらの 3 パターンの行取得の手順は、先の投稿を参照してください。

A ならば【現在のカーソル行を対象とする場合】の手法です。
B ならば【行全体が選択されているものを対象とする場合】です。
C ならば【選択されているセルの行を対象とする場合】です。

SelectionMode に FullRowSelect を指定している場合は、
C は使わないので B 案になるでしょう。


先の例においては、削除のために「Delete メソッド」を呼び出していたわけですが、
今回は Delete メソッドを呼ぶ代わりに、ソッケルさんの「DELETE SQL を呼ぶ」コードを
使うことで、ひとまずの目的を達することができるでしょう。
(B あるいは C の For Each の中の処理を書き換える、ということです)


もう少し書いておくと……ソッケルさんの
> DataGridView1.CurrentRow.Cells("注文番号").Value
の場合、「DataGridView1.CurrentRow」が DataGridViewRow 型を表しますよね。

一方、先の例の
> For Each gridRow As DataGridViewRow In DataGridView1.SelectedRows
の場合、「gridRow」が DataGridViewRow 型で得られますので、
 gridRow.Cells("注文番号").Value
のようにして、各行の注文番号を得られることになるわけです。
あとは同じように書き換えられますよね。



さて、これで複数件の削除はひとまず行えるのですが、
さらに追記しておくべきことがあります。

> Dim delete As String = "DELETE FROM T_ITEM WHERE NUMBER =  '" & DataGridView1.CurrentRow.Cells("注文番号").Value & "'"

残念ながら、このコードには 2 つの問題があります。

一つは、データがゼロ件だった場合などのように、
CurrentRow が Nothing を返してくる場合の対処が漏れていること。

もう一つは、SQL インジェクションが考慮されていない点です。
後者は特に危険なので、是正するべきです。

たとえば提示頂いた例で言えば、注文番号欄に
「'--」あるいは「' OR ''='」が入力されることで
T_ITEM 上の全データが削除される危険性を含んでしまいます。


どうするべきかというと、値を埋め込んだ SQL コマンドを直接作るのではなく、
前回の最後に書いた【DELETE 文を手動で生成する場合】のサンプルコードのように、
パラメーター付きの SQL にするということです。

MySQL の場合は、「WHERE NUMBER = ?」の無名パラメーターと
「WHERE NUMBER = @PARAM1」などの名前付きパラメーターの
両方の構文に対応しています。あとは、そのパラメーターに対応させて
MySqlParameter オブジェクトを用意して渡すようにすれば、
『'』が含まれるような文字列であっても、安全に引き渡すことができます。

もし、どうしても値を直接 SQL に埋め込まなくてはならないのなら、
データ内容が安全かを事前にチェックするコードを設けておくか、
あるいは、必ずサニタイジングを施す必要があります。たとえば、
文字列中の「'」や「"」を、Replace メソッドを使うなどして
「\'」や「\"」に置き換えるといった手法です。
https://dev.mysql.com/doc/refman/5.6/ja/string-literals.html


とはいえ、このような手動サニタイズは作業漏れを引き起こしやすいので、
可能な限り、MySqlParameter あるいは MySqlDataAdapter を
使うことを心がけておいたほうが安全です。


> Dim mycomand As New MySqlCommand(delete, conn)
mycomand ではなく
mycommand かな…。


> DataGridView1.Rows.RemoveAt(DataGridView1.CurrentCell.RowIndex)
わざわざ行番号に戻さずとも、RemoveAt の代わりに Remove を使うようにすれば
 DataGridView1.Rows.Remove(DataGridView1.CurrentRow)
のように、行オブジェクトをそのまま渡せますよ。


> Catch ex As Exception
>   MsgBox(ex.Message)
確認では MessageBox.Show を用いていたのに、
エラー表示は MsgBox なんですね。
何か理由があって使い分けているのでしょうか?



> '合計の表示
> sum()
以下、蛇足情報として。

上記がどのような処理で実装されているか分かりませんが、
現在表示されている内容の合計を求めるのであれば、
DataTable の Compute メソッドを使うと、
ループ処理を使わずに、合計を簡単に求められます。

あるいは VB の Aggregate 句を使う方法などもあります。


蛇足ついでにもう一つ。ちょっと難しい内容かもしれませんが、
データ量が多くなってきた場合に必要となる情報を紹介しておきます。

[Windows フォーム DataGridView コントロールを拡張するための推奨される手順]
https://msdn.microsoft.com/ja-jp/library/ha5xt0d9.aspx
投稿者 ソッケル  (社会人) 投稿日時 2017/8/2 09:01:51
ご丁寧にありがとうございます。とても勉強になります。

まずはプログラムを正常に動かすのが第一なのでこちらの箇所だけ抜粋します。

>gridRow.Cells("注文番号").Value
と書くとgridRowは宣言されていません。
構文エラーですとエラーがでてしまいました。
投稿者 魔界の仮面弁士  (社会人) 投稿日時 2017/8/2 15:01:16
> まずはプログラムを正常に動かすのが第一なのでこちらの箇所だけ抜粋します。

「動作しないコード」の『一部』だけを抜粋されても答えにくいです…。
質問が断片的なため、こちらとしては幾つかのパターンを想定せねばならず、
回答が断片的で雑多になりがちですが、そこは御容赦を。


そちらの状況が分からないため、こちらから幾つかの逆質問をしていますが、
それらに答えていただけると、回答がしやすくなります。
ここでいう確認事項というのは、たとえば
 「この場合、削除したいのは以下のいずれでしょうか?」
などのことです。
(他にも確認していた点がありますが、さしあたって重要なのは上記です)


>> gridRow.Cells("注文番号").Value
> と書くとgridRowは宣言されていません。

そのコードは、どこに書いたのでしょうか?


先の回答では、
>> 一方、先の例の
>>> For Each gridRow As DataGridViewRow In DataGridView1.SelectedRows
>> の場合、「gridRow」が DataGridViewRow 型で得られますので、
と書いていたと思います。
上記では gridRow が宣言されていますので、これの For Each ~ Next の
ブロック内を書き換えているのであれば、未宣言エラーにはなっていないと思います。


もし、【行全体が選択されているものを対象とする場合】ではなく、
【選択されているセルの行を対象とする場合】を参考にされた場合、
> For Each rowView In rows
になっていたと思います。この場合の rowView は DataGridViewRow 型ではありません。
ここでは As 句が省略されていますが、実際には As DataRowView に相当しますので、
As DataGridViewRow の場合とは、若干異なるコードになります。


このパターンを採用していたのであれば、
 Dim o As Object = rowView("注文番号")
などとすることで、各行の注文番号を取り出せます。

ただしそれは、DataGridView が ReadOnly の場合の話です。

DataGridView を編集可能にしていた場合は、注文番号欄が
ユーザーに修正される可能性がありますので、
現在の表示値ではなく、修正前の値で WHERE 文を
組み立てる必要があります。


それには、DataRow 型のオブジェクトを取得することになります。


もしも、【選択されているセルの行を対象とする場合】において、編集前の値を
取り出す必要がある場合には、【DELETE 文を手動で生成する場合】の例で述べた
> For Each rowView As DataRowView In ……
>  Dim row As DataRow = rowView.Row
のようにして、DataRowView の元となっている DataRow を取り出す必要があります。

つまりこういうことですね。
Dim rows = From cell As DataGridViewCell In DataGridView1.SelectedCells
           Let rowView = DirectCast(cell.OwningRow.DataBoundItem, DataRowView)
           Where rowView IsNot Nothing
           Select rowView Distinct
For Each rowView As DataRowView In rows
    Dim row As DataRow = rowView.Row
    ': 
    ': 
Next

あるいはその直前の LINQ 式を修正して、これでも OK です。
Dim rows = From cell As DataGridViewCell In DataGridView1.SelectedCells
           Let rowView = DirectCast(cell.OwningRow.DataBoundItem, DataRowView)
           Where rowView IsNot Nothing
           Select rowView.Row Distinct
For Each row As DataRow In rows
    ': 
    ': 
Next


これで DataRow 型のオブジェクトが取り出せました。


あるいは、【行全体が選択されているものを対象とする場合】のように、
As DataGridViewRow なオブジェクトから辿る場合は、先のサンプルにもあるように
Dim rowView = TryCast(gridRow.DataBoundItem, DataRowView)
とすれば、DataRowView が得られますので、そこから先述の方法で、
Dim row As DataRow = rowView.Row
で DataRow が取り出せます。


これらの方法で DataRow 型のオブジェクトを取得した後は、
下記を使い分けることで、編集前バージョンもしくは編集後バージョンの値を取得できます。
Dim o0 As Object = row("注文番号")
Dim o1 As Object = row("注文番号", DataRowVersion.Current)
Dim o2 As Object = row("注文番号", DataRowVersion.Original)
Dim o3 As Object = row("注文番号", DataRowVersion.Proposed)
Dim o4 As Object = row("注文番号", DataRowVersion.Default)



(1) o0 と o4 は同じ意味です。DataRowVersion を指定しない場合は Default 扱いです。

(2) o1 の Current は「現在の値」を示します。DataGridView に表示されている値がこれです。
 削除マークの付いた行(RowState = Deleted)で呼び出すと VersionNotFoundException の例外となります。

(3) o2 の Original は「元の値」を示します。DELETE 文の WHERE に渡すときに使えます。
 新規追加したばかりの行(RowState = Added)で呼び出すと VersionNotFoundException の例外となります。

(4) o3 の Proposed は「提案値」を示します。
 今回のケースで呼び出すと VersionNotFoundException の例外となります。
 これはたとえば、DataGridView 上で編集されている最中で、まだその値が
 DataRow 側に反映される前といった、一時的な状態を指し示します。

(5) o4 の Default は「既定値」です。省略して o0 表記でも書けます。
 これは該当行の RowState プロパティに応じて o1~o3 のいずれかを意味します。
 RowState が 追加(Added)/修正済み(Modified)/未編集(Unchanged)の
 いずれかの場合は o1 (Current) 相当の動作です。
 RowState が 削除済み(Deleted)なら o2 (Original) 相当の動作です。
 RowState が未割り当て(Detached)なら o3 (Propsed) 相当の動作です。
投稿者 魔界の仮面弁士  (社会人) 投稿日時 2017/8/2 15:07:56
ちなみに、今回は採用されなかった Delete メソッド を使ったパターンの場合はこんな感じです。

「'」を含んだデータなどでも処理できますし、SQL の構築も最小限で済むので、
基本的にはこちらの方法を推奨しておきます。


【データ取得処理】
table = New DataTable()
'DataAdapter は、実データベースと DataTable とを中継するクラスです 
adp = New MySqlDataAdapter("SELECT … FROM …", conn)
'CommandBuilder に DataAdapter を渡すと、SELECT の情報を元にして、 
'対応する INSERT/UPDATE/DELETE コマンドが自動生成されます 
Dim cb As New MySqlCommandBuilder(adp)
'DataTable にデータが取り込まれます。(全ての行が Unchanged な状態) 
adp.Fill(table)
DataGridView1.DataSource = table


この後、ユーザーが DataGridView を通じて追加・更新・削除するか、
プログラム側で .Rows.Add や Delete メソッドなどを通じて加工します。
これにより、DataTable の各行の RowState が、
Unchanged から Added/Modified/Deleted に変化します

【データ更新処理】
'DataTable の各行の Added/Modified/Deleted 状態を元に、 
'INSERT/UPDATE/DELETE コマンドに渡され、MySQL 上に一括反映されます 
adp.Update(table)
'AcceptChanges メソッドを呼び出して、更新完了を DataTable 側に反映させます。 
'これにより、すべての行が Unchanged な状態と戻ります。 
'(Added/Modified だった行は Unchanged となります) 
'(Deleted だった行は DataTable から Remove されます) 
table.AcceptChanges()



なお、外部結合したテーブルなどの場合は、MySqlCommandBuilder からでは
更新用の SQL が自動生成できませんので、自前で更新用の DELETE 文を
組まねばならないこともあります。

ただしその場合でも、先に述べた理由により、MySqlParameter を利用して
パラメーターを渡すことが望ましいです。
投稿者 ソッケル  (社会人) 投稿日時 2017/8/2 16:50:39
返信ありがとうございました。

delete文をfor eachの中に持ってくことで複数削除できるようになりました。
おいおい、Parameterを使用したやり方に変更したいと思います。

また、教えてくださったことを元にこれから勉強していきます。
また何かありましたらお願いします。