VB.NET DataGridViewで選択行の直下に行を挿入する方法:IDENTITY対策とSortOrderで並び順を永続化

VB.NETのDataGridViewで「選択行の直下」に新規行を追加したいのに、IDENTITY主キーの影響で再読み込み後に一番下へ移動してしまう――この手の悩みは、表示順とデータ順の設計が噛み合っていないことが原因です。この記事では「画面上だけで直下に見せる方法」と「SortOrder列で並び順を永続化する方法」を、TableAdapter/SQL Server前提で具体的に整理します。

目次

現象の正体:なぜ新規行が「常に一番下」に見えるのか

DataGridViewに「ビュー(View)+元テーブル」をバインドしていて、ボタン押下で TableAdapter.Insert によりテーブル Cataloog に行を追加する。すると次のようなことが起きます。

  • 主キーがIDENTITY(自動採番)なので、新規行のIDは常に最大値になる
  • ビューやFillクエリが「ID順(または暗黙に取得順)」になっている
  • 再読み込み(Fill)すると、IDの大きい新規行が結果セットの末尾に並ぶ
  • 結果として、DataGridView上でも「新規行=一番下」に見える

ここで重要なのは、ID(IDENTITY)は識別子であって、並び順の仕様ではないという点です。さらに、SQLの世界では ORDER BYがないSELECTの行順は保証されません。たまたま「いつも同じ順」に見えても、将来の実行計画やインデックス、統計情報の変化で順番が変わる可能性があります。

要素役割「直下に挿入」を満たせるか
IDENTITY主キー行を一意に識別する満たせない(挿入位置の概念を持たない)
DataGridViewの行インデックスその時点の表示位置表示だけなら満たせる(ただしソート次第)
並び順専用列(例:SortOrder)永続的な表示順の基準満たせる(データ設計として正攻法)

まず切り分け:あなたが欲しいのは「表示上の直下」か「永続的な直下」か

要望としては「画面でも、データ的にも選択行のすぐ下に挿入したい」「できれば永続的に保ちたい」ですが、実装方針は大きく2つに分かれます。

方針できることできないこと / 弱点おすすめ度
表示上だけ直下に入れる今見えているリストで「直下」に見せる再読み込み・別PC・別ユーザーでも同じ順を保証できない短期の応急処置向け
SortOrder列で永続化するDB上の「順番」を仕様として固定し、常に直下に出せる列追加や更新処理が必要(設計変更)本命(長期運用向け)

解決策:表示上だけ「選択行の直下」に行を挿入する方法

ここでのゴールは「今開いている画面のDataGridView上で、選択行のすぐ下に新規行が見える」ことです。DBの再取得や他画面でも同順にしたい、という話は次の章の「永続化」で扱います。

この方法が向くケース・向かないケース

観点向く向かない
並び替え(ソート)ソートしていない(固定表示)ユーザーが列ヘッダーで頻繁にソートする
データソース編集可能なDataTableを直接バインドしている読み取り専用View、更新不可の結果セットを表示している
要件その場で見えればOK並び順も仕様として永続化したい

実装の考え方:INSERTではなく「まずDataTableに差し込む」

TableAdapter.Insert を先に叩くと、新規行はDBには作られますが、現在表示しているDataGridViewの並びの中に「直下」という概念で入りません。そこで、DataGridViewの元になっているDataTable(またはBindingSourceのリスト)に対して、InsertAtで差し込むのがポイントです。

基本の流れは次の通りです。

  • DataGridViewのDataSource(またはBindingSource)から、元のDataTableを取得
  • NewRow() で新しいDataRowを作る
  • 選択行の「次」の位置に Rows.InsertAt で挿入する
  • TableAdapter.Update(dt) でDBへ反映する
  • 必要なら再読み込み(Fill)して、表示を同期する

サンプル:DataTableにInsertAtしてからUpdateする(VB.NET)

DataSourceがDataTable直結の場合の例です。実際にはBindingSourceを挟んでいることが多いので、次の節でBindingSource経由も紹介します。

' 例:DataGridViewがDataTableを直接表示しているケース
Dim dt As DataTable = TryCast(ParticulierDataGridView.DataSource, DataTable)
If dt Is Nothing Then
    Throw New InvalidOperationException("DataSource が DataTable ではありません。BindingSource 経由で取得してください。")
End If

If ParticulierDataGridView.CurrentRow Is Nothing Then
    Throw New InvalidOperationException("選択行がありません。")
End If

Dim merkVal As String = CStr(ParticulierDataGridView.CurrentRow.Cells("Merk").Value)

' 新しい行を作成
Dim newRow As DataRow = dt.NewRow()
newRow("Merk") = merkVal
' 必要な列を初期化(NULL不可の列は必ず値を入れる)
' newRow("SomeColumn") = ...

' 選択行の直下へ挿入(表示順=未ソートが前提)
Dim insertIndex As Integer = ParticulierDataGridView.CurrentRow.Index + 1
If insertIndex > dt.Rows.Count Then insertIndex = dt.Rows.Count

dt.Rows.InsertAt(newRow, insertIndex)

' DBへ反映(Insert/Update/Delete をまとめて反映)
Me.CataloogTableAdapter.Update(dt)

' 必要なら再読み込み(Viewの更新を含めて同期したい場合)
Me.DataSet.Particulier.Clear()
Me.ParticulierTableAdapter.Fill(Me.DataSet.Particulier)

BindingSource経由でDataTableを取得するパターン

現実には ParticulierDataGridView.DataSource が BindingSource で、その先がDataTableという構成が多いです。その場合は、次のように「どこにDataTableがあるか」を正しく辿ります。

' DataGridView → BindingSource → DataTable のよくある構成
Dim bs As BindingSource = TryCast(ParticulierDataGridView.DataSource, BindingSource)
If bs Is Nothing Then
    Throw New InvalidOperationException("DataSource が BindingSource ではありません。")
End If

Dim dt As DataTable = Nothing

' (1) DataSource が DataTable の場合
dt = TryCast(bs.DataSource, DataTable)

' (2) DataSource が DataSet の場合(DataMemberでテーブル名が指定されている)
If dt Is Nothing Then
    Dim ds As DataSet = TryCast(bs.DataSource, DataSet)
    If ds IsNot Nothing AndAlso Not String.IsNullOrEmpty(bs.DataMember) Then
        dt = ds.Tables(bs.DataMember)
    End If
End If

If dt Is Nothing Then
    Throw New InvalidOperationException("BindingSource から DataTable を特定できません。")
End If

注意点:ソートされていると「InsertAtの位置」は維持されない

DataGridViewが列でソートされている、あるいはBindingSource側で Sort を設定している場合、DataRowをInsertAtしても、表示はソートキーで並び替えられます。つまり、見た目の「直下」を維持したいなら、次のいずれかが必要です。

  • その画面ではソートを禁止する(列の SortMode を NotSortable にする等)
  • 「直下」を決める基準となる列(後述のSortOrder)で常にソートする
  • 再読み込み後にユーザーが納得できる並び(例:Merk順、作成日順)へ寄せる

結論として、「表示だけで直下に入れたい」方法は、ソートや再取得が絡むと限界があるという理解が重要です。

解決策:並び順を「永続的に」固定したい場合の本質的な方法(SortOrder列)

ここからが本命です。IDENTITYは「登録順の結果」であって、「ユーザーが意味を感じる順番」を表しません。選択行のすぐ下へ入れる、並び替えを維持する、再読み込みしても同じ順番にする――これらを成立させるには、並び順専用の列を持ち、そこを唯一の基準として扱うのが定石です。

設計方針:SortOrderを「順番の真実」にする

  • テーブル Cataloog に SortOrder(整数)を追加する
  • 表示・取得は原則 ORDER BY SortOrder に統一する(ビューにも反映)
  • 「直下に挿入」= SortOrderをずらして空きを作り、新規行のSortOrderを割り当てる

この方針にすると、DataGridView側は非常に単純になります。常にSortOrderで表示しておけば、再読み込みしても「直下に入れた」順番が崩れません。

DB側:SortOrder列の追加と既存データへの採番

SQL Server想定の例です。既存データがある場合は、まず列を追加し、既存行に連番を振ります。

-- 1) 列追加(NULL不可にするなら、既定値を付けてから採番)
ALTER TABLE dbo.Cataloog
ADD SortOrder INT NOT NULL CONSTRAINT DF_Cataloog_SortOrder DEFAULT(0);

-- 2) 既存データへ連番を振る(例:ID昇順で1,2,3...)
;WITH x AS (
    SELECT
        ID,
        ROW_NUMBER() OVER (ORDER BY ID) AS rn
    FROM dbo.Cataloog
)
UPDATE c
SET SortOrder = x.rn
FROM dbo.Cataloog c
INNER JOIN x ON x.ID = c.ID;

さらに、検索・ソートが速くなるようにインデックスも検討します。

-- SortOrderで並べるならインデックスを作る(必要に応じて)
CREATE INDEX IX_Cataloog_SortOrder ON dbo.Cataloog(SortOrder);

「絶対に重複させたくない」なら一意制約(UNIQUE)も選択肢ですが、後述するように“ずらし更新”の実装次第では一時的な衝突が起き得ます。運用・同時更新の前提に合わせて選びましょう。

ビュー(View)やFillクエリにORDER BY SortOrderを入れる

DataGridViewがビュー Particulier を表示している場合、表示順の根拠は「ビューのSELECT」または「TableAdapterのSELECT」です。永続順を実現するなら、次のように順序を統一します。

  • ビュー定義(またはTableAdapterのSELECT)で ORDER BY Cataloog.SortOrder を採用する
  • DataGridView側で手動ソートに頼りすぎない(再読み込みのたびに整合を取るため)

画面側で DataGridView.Sort を呼ぶだけでも一見整いますが、長期的には「取得クエリ自体が順序を保証している」方がバグが減ります。

永続化の核心:「後ろをずらす」→「直下のSortOrderでINSERT」

やりたいことを順序列の言葉に翻訳すると、次の操作になります。

  • 選択行のSortOrderを selectedSortOrder とする
  • selectedSortOrder より後ろ(大きいSortOrder)の行を、すべて +1 して後ろにずらす
  • 新規行は SortOrder = selectedSortOrder + 1 として追加する
操作狙い結果
後ろのSortOrderを+1「直下」に入れる空きを作るselected+1 が空く
新規行をselected+1でINSERT選択行のすぐ下に固定再読み込みでも直下に出る

衝突回避のコツ:ずらし更新はトランザクションで一括実行する

複数人が同時に操作する可能性があるなら、「ずらす」+「挿入」を同一トランザクションで実行してください。途中で別の挿入が入ると、順序が崩れたり重複が起きたりします。

また、SortOrderに一意性を持たせる場合、単純に SET SortOrder = SortOrder + 1 をすると一時的に重複が発生してエラーになるケースがあります。その対策として、更新対象を一旦負の値へ退避してから戻す、というテクニックが安全です(値の衝突を避ける)。

-- 例:selectedSortOrder より後ろを「安全に」+1する(衝突回避)
-- (1) まず負の領域へ退避(重複しない形で)
UPDATE dbo.Cataloog
SET SortOrder = -(SortOrder + 1)
WHERE SortOrder > @selectedSortOrder;

-- (2) 退避した負の値を正に戻す
UPDATE dbo.Cataloog
SET SortOrder = -SortOrder
WHERE SortOrder < 0;

VB.NET:SqlTransactionで「ずらす」→「INSERT」をまとめる例

TableAdapterを使い続けても良いですが、順序更新と挿入を確実にまとめるなら、ここは明示的にSQLを実行した方が事故が減ります(もちろん、TableAdapterにカスタムクエリ/ストアドを生やす形でもOKです)。

Imports System.Data.SqlClient

Private Sub InsertBelowSelectedRow()
    If ParticulierDataGridView.CurrentRow Is Nothing Then
        Throw New InvalidOperationException("選択行がありません。")
    End If

    ' 例:ビューにSortOrder列が含まれている前提
    Dim drv As DataRowView = TryCast(ParticulierDataGridView.CurrentRow.DataBoundItem, DataRowView)
    If drv Is Nothing Then
        Throw New InvalidOperationException("DataBoundItem から DataRowView を取得できません。")
    End If

    Dim merkVal As String = CStr(drv("Merk"))
    Dim selectedSortOrder As Integer = CInt(drv("SortOrder"))

    Dim connStr As String = "Your_Connection_String"

    Using conn As New SqlConnection(connStr)
        conn.Open()

        Using tran As SqlTransaction = conn.BeginTransaction()
            Try
                ' (1) 後ろをずらす(衝突回避の2段階更新)
                Using cmd1 As New SqlCommand(
                    "UPDATE dbo.Cataloog SET SortOrder = -(SortOrder + 1) WHERE SortOrder > @s;", conn, tran)
                    cmd1.Parameters.AddWithValue("@s", selectedSortOrder)
                    cmd1.ExecuteNonQuery()
                End Using

                Using cmd2 As New SqlCommand(
                    "UPDATE dbo.Cataloog SET SortOrder = -SortOrder WHERE SortOrder < 0;", conn, tran)
                    cmd2.ExecuteNonQuery()
                End Using

                ' (2) 新規行を直下のSortOrderで追加
                Using cmd3 As New SqlCommand(
                    "INSERT INTO dbo.Cataloog (Merk, SortOrder) VALUES (@merk, @sort);", conn, tran)
                    cmd3.Parameters.AddWithValue("@merk", merkVal)
                    cmd3.Parameters.AddWithValue("@sort", selectedSortOrder + 1)
                    cmd3.ExecuteNonQuery()
                End Using

                tran.Commit()
            Catch
                tran.Rollback()
                Throw
            End Try
        End Using
    End Using

    ' (3) 再読み込みしてSortOrderで表示を確定
    Me.DataSet.Particulier.Clear()
    Me.ParticulierTableAdapter.Fill(Me.DataSet.Particulier)

    ' 可能なら取得クエリ側でORDER BYするのが理想。保険として画面側ソートも可。
    ParticulierDataGridView.Sort(
        ParticulierDataGridView.Columns("SortOrder"),
        System.ComponentModel.ListSortDirection.Ascending
    )
End Sub

上の例では最小限として Merk と SortOrder だけINSERTしています。実際にはNULL不可列や既定値の扱いがあるので、INSERT対象列はテーブル定義に合わせて増やしてください(既定値があるなら列を省略するのも手です)。

TableAdapterでやるなら「ずらし更新」と「SortOrder付きINSERT」をクエリとして追加する

DataSet DesignerでTableAdapterに次の2つのクエリを追加しておくと、呼び出し側がすっきりします。

  • ShiftSortOrder(selectedSortOrder):後ろをずらすUPDATE
  • InsertWithSortOrder(merkVal, ..., newSortOrder):SortOrderを指定してINSERT

そしてVB側は次のような形にまとめられます。

' ① selectedSortOrder を取得
Dim selectedSortOrder As Integer =
    CInt(CType(ParticulierDataGridView.CurrentRow.DataBoundItem, DataRowView)("SortOrder"))

' ② 後ろをずらす
Me.CataloogTableAdapter.ShiftSortOrder(selectedSortOrder)

' ③ 新規行を直下のSortOrderで追加
Dim newSortOrder As Integer = selectedSortOrder + 1
Me.CataloogTableAdapter.InsertWithSortOrder(merkVal, newSortOrder)

' ④ 再読み込みしてSortOrderで表示
Me.DataSet.Particulier.Clear()
Me.ParticulierTableAdapter.Fill(Me.DataSet.Particulier)

トランザクションを確実にしたい場合は、TableAdapterの接続を共有し、Adapter.SelectCommand.Transaction 等へ設定するか、ストアドプロシージャに処理を寄せるのがおすすめです。

実運用で差が出るポイント

「選択行」の取り方を誤ると、意図しない位置に入る

DataGridViewには CurrentRow、SelectedRows など複数の概念があります。複数選択や行ヘッダー選択の設定によっては、CurrentRowが期待通りにならないことがあります。

パターン起きがち対策
複数選択ONSelectedRowsが複数になり「どれの直下?」が曖昧仕様で1行のみ許可(MultiSelect=False)にするか、最後に選択した行を採用
CurrentRowがNothingセル未選択、クリック直後の状態で落ちるガード節で必ずチェックし、ユーザーへ選択を促す
フィルタ適用中画面で見える順とテーブル全体の順がズレる「フィルタ中でも直下」なら、画面(DataView)基準のPositionとDBの順序列の両方を意識

並び順は「ID」ではなく「仕様で決めたキー」を必ず使う

「とりあえずID順で表示していた」をやめ、SortOrder(または作成日時など)を採用するだけで、画面側のロジックが大幅に簡単になります。特に「ユーザー操作で並び順が意味を持つ」画面では、順序列はコスト以上の価値があります。

大量データの場合は「全部+1」は重くなることがある

SortOrder方式の弱点は、選択行が先頭に近いほど「ずらす対象行」が多くなる点です。数万行以上で頻繁に挿入が走るなら、次のような改善案も検討できます。

  • 間隔を空けて採番する(例:10,20,30…)。直下へ入れるときは間の値(例:15)を使う
  • 間が詰まったら一括で振り直す(夜間バッチやメンテ時)
  • SortOrderを BIGINT にして余裕を持たせる

ただし「常に整数で、常に直下がselected+1で分かりやすい」運用を優先するなら、まずは今回の“ずらす方式”で十分なことも多いです。重要なのは、データ量と更新頻度に合わせて設計を選ぶことです。

よくある症状と対策の早見表

症状原因対策
挿入後は見えるが、再読み込みすると一番下に行く表示順がID(IDENTITY)依存SortOrder列を追加し、ORDER BY SortOrderに統一
InsertAtしたのに直下に見えないソートが有効で並び替えられているソートを禁止するか、SortOrderで常にソート
ずらし更新でエラー(重複)になる一意制約/インデックスの衝突負の値退避など衝突回避手順+トランザクション
他ユーザーと同時操作で順番が壊れる更新と挿入が別々に実行されるトランザクション化、必要に応じてロック/ストアド化

結論:おすすめの落とし所

  • 「今の画面で直下に見えればOK」なら、DataTableに Rows.InsertAt してからUpdateする方法が手早い
  • 「再読み込みしても、他端末でも、ずっと直下に出したい」なら、SortOrder列を追加して永続化するのが正攻法
  • 表示順はIDではなく、SortOrderのような「順序専用キー」を唯一の基準にする
  • ずらし更新+挿入は、必ずトランザクションでまとめて事故を防ぐ

DataGridViewの行順問題は、UI側のテクニックだけで解決しようとすると限界が来ます。ユーザーにとって意味のある並び順を永続化したいなら、DB設計(SortOrder列)まで踏み込むことで、実装も運用も安定します。

この記事を書いた人

実務の現場で詰まりがちなポイントを地図にするITブログ「IT trip」を運営。Windows/Office(Teams・Excel)からSQL、サーバ運用、ガジェットまで、再現性のある手順と“なぜそうなるか”を丁寧に解説します。読んだらすぐ試せること、そして迷った人の次の一歩が見えることを大切にしています。

コメント

コメントする

目次