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):後ろをずらすUPDATEInsertWithSortOrder(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が期待通りにならないことがあります。
| パターン | 起きがち | 対策 |
|---|---|---|
| 複数選択ON | SelectedRowsが複数になり「どれの直下?」が曖昧 | 仕様で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列)まで踏み込むことで、実装も運用も安定します。

コメント