C#でのデータベース接続とクエリの実行方法を徹底解説

結論からいうと、C#からSQL Serverへ接続する基本形は、Microsoft.Data.SqlClientを使い、接続を必要な処理の直前に開き、SQLの値を必ずパラメーターで渡し、usingまたはawait usingで接続・コマンド・Readerを確実に破棄する形です。取得はExecuteReader、単一値はExecuteScalar、追加・更新・削除はExecuteNonQueryを選び、複数の変更を一組として扱う場合だけトランザクションを使います。

以下はSQL Server向けのADO.NET例です。ほかのデータベースでは、プロバイダー、接続文字列、パラメーター記号、SQL構文が異なります。接続先の情報は利用環境で決まるため、本文では接続文字列を引数として受け取り、実在の接続先や資格情報をコードへ埋め込みません。

目次

最小構成と処理の流れを決める

データアクセス処理は「接続を作る」「開く」「コマンドを作る」「パラメーターを設定する」「目的に合う実行メソッドを呼ぶ」「結果を読み取る」「破棄する」の順です。最初は一つのSELECTだけで接続確認を行い、読み取りが成功してから更新処理を追加すると、接続問題とSQL問題を切り分けやすくなります。

  • 一覧や複数行の取得はExecuteReaderまたはExecuteReaderAsync。
  • 件数や合計など一つの値はExecuteScalarまたはExecuteScalarAsync。
  • INSERT、UPDATE、DELETEの影響行数はExecuteNonQueryまたはExecuteNonQueryAsync。
  • 関連する複数変更をまとめて成功または失敗させる場合はローカルトランザクション。

Microsoft.Data.SqlClientを選び依存関係を整える

新しいSQL Server向け開発では、Microsoftが案内するMicrosoft.Data.SqlClient名前空間を使用します。既存コードがSystem.Data.SqlClientを使っている場合は、単にusing行だけを置換せず、Microsoft公式の移行差分と利用中の.NET対象バージョンを確認してから依存関係を変更します。

パッケージのバージョンを記事の固定値で決めるのではなく、プロジェクトの対象フレームワークとMicrosoft.Data.SqlClientの対応表に合う、サポート中の安定版を選びます。追加後はビルドを行い、SqlConnection、SqlCommand、SqlDataReaderがMicrosoft.Data.SqlClient由来であることを確認します。

接続文字列と接続の寿命を分離する

接続文字列はアプリケーション設定から取得し、データアクセスメソッドへ渡します。文字列をログへ出したり、ソースコードへ直接書いたりしません。接続文字列を組み立てる必要がある場合は、手作業の文字列連結ではなくSqlConnectionStringBuilderを使うと、キーと値を構造的に扱えます。

SqlConnectionは処理の直前に作成してOpenAsyncで開き、処理が終わったら破棄します。短時間で破棄しても、接続プールが利用できる環境では物理接続が再利用されます。アプリ全体で一つのSqlConnectionを持ち回るより、各処理の範囲を明確にした方が、失敗時に状態を追いやすくなります。

パラメーター付きSELECTを実装する

次の例は、dbo.Productsテーブルから指定価格以上の商品を読み取る完全な最小例です。テーブル名と列名は例示したスキーマなので、実際のデータベース定義に合わせて変更します。値はSQL文字列へ連結せず、@minimumPriceパラメーターとして渡します。

using System.Data;
using Microsoft.Data.SqlClient;

public sealed record ProductRow(int Id, string Name, decimal Price);

public static class ProductRepository
{
    public static async Task<IReadOnlyList<ProductRow>> LoadAsync(
        string connectionString,
        decimal minimumPrice,
        CancellationToken cancellationToken)
    {
        var rows = new List<ProductRow>();

        await using var connection = new SqlConnection(connectionString);
        await connection.OpenAsync(cancellationToken);

        const string sql = "SELECT ProductId, ProductName, UnitPrice FROM dbo.Products WHERE UnitPrice >= @minimumPrice ORDER BY ProductId;";
        await using var command = new SqlCommand(sql, connection);

        var priceParameter = command.Parameters.Add("@minimumPrice", SqlDbType.Decimal);
        priceParameter.Precision = 18;
        priceParameter.Scale = 2;
        priceParameter.Value = minimumPrice;

        await using var reader = await command.ExecuteReaderAsync(cancellationToken);
        int idIndex = reader.GetOrdinal("ProductId");
        int nameIndex = reader.GetOrdinal("ProductName");
        int priceIndex = reader.GetOrdinal("UnitPrice");

        while (await reader.ReadAsync(cancellationToken))
        {
            rows.Add(new ProductRow(
                reader.GetInt32(idIndex),
                reader.GetString(nameIndex),
                reader.GetDecimal(priceIndex)));
        }

        return rows;
    }
}

パラメーター型、Precision、Scaleはデータベース列の定義に合わせます。型を曖昧にしたままAddWithValueへ任せるより、SqlDbTypeと必要なサイズを明示した方が、意図しない型変換を避けやすくなります。

DataReaderで結果とNULLを正しく読む

DataReaderは前方向へ行を読み取る方式で、ReadAsyncがtrueを返す間だけ現在行へアクセスできます。列番号を直書きするとSELECT列順の変更に弱いため、例のようにGetOrdinalで列名から位置を取得します。数値、日付、文字列は列定義に合うGetInt32、GetDateTime、GetStringなどで読みます。

NULLを許可する列は、値取得の前にIsDBNullまたはIsDBNullAsyncで確認します。SQLのNULLとC#のnullは同じ扱いではありません。書き込みパラメーターへデータベースのNULLを渡す場合はDBNull.Valueを使い、空文字や0を代用しないようにします。

大量行を取得する場合は、全件をListへ入れる設計が適切かも確認します。必要な列だけをSELECTし、絞り込みと並び順をデータベース側で行い、画面表示ならページ単位で取得します。まず正しい結果を作り、その後に実行時間と行数を測って改善します。

追加・更新・削除は影響行数で検証する

INSERT、UPDATE、DELETEはExecuteNonQueryAsyncを使い、戻り値の影響行数を確認します。たとえば一件だけ更新する想定で0件なら対象が存在しない可能性があり、複数件ならWHERE条件が広すぎる可能性があります。期待件数と違う場合に成功扱いしない設計が重要です。

更新値だけでなくWHERE条件の値もパラメーターにします。更新前に同じ条件のSELECTで対象を確認し、本番データではなく検証用データベースでSQLを試します。DELETEを実装するときは、対象を特定する主キーと期待件数を明示し、画面から渡された文字列をSQLへ連結しません。

新しく採番された一つの値が必要なら、SQL Server側で値を返すSQLとExecuteScalarAsyncを組み合わせます。戻り値がnullまたはDBNull.Valueになり得るかを確認し、C#側の型へ変換する前に分岐します。

トランザクションで複数変更をまとめる

一つの業務処理で複数のINSERTやUPDATEがすべて成功する必要がある場合は、接続を開いた後にBeginTransactionAsyncでローカルトランザクションを開始します。各SqlCommandのTransactionへ同じトランザクションを設定し、全処理と期待件数の確認が成功したらCommitAsync、途中で失敗したらRollbackAsyncを呼びます。

トランザクションは必要な範囲だけに限定します。ユーザー入力待ちや外部通信をトランザクション内へ入れると、ロック保持時間が長くなります。また、ロールバック自体も接続障害時には失敗し得るため、元の例外を失わないように記録し、処理結果を成功として返さない設計にします。

ロールバックの検証は、二つ目のコマンドを意図的に失敗させるテストを検証用データベースで行い、一つ目の変更も残っていないことをSELECTで確認します。これにより、単に例外が出たことではなく、データが元の状態へ戻ったことまで検証できます。

例外処理・動作確認・公式資料

例外は握りつぶさず、処理名、実行時刻、SQLを識別できる固定名、例外の種類を記録します。接続文字列そのものやパラメーターの実値を無条件にログへ出さないでください。キャンセル要求はCancellationTokenでOpenAsync、Execute系、ReadAsyncへ渡し、キャンセルを一般エラーと区別します。

  • 接続だけを開閉するテストで接続経路を確認する。
  • 既知の一行をSELECTし、型とNULLの扱いを確認する。
  • 更新前後をSELECTし、影響行数と値を確認する。
  • 失敗時にトランザクションがロールバックされることを確認する。
  • 接続数が増え続けないこと、キャンセル後に処理が終了することを確認する。

実装の根拠は、次のMicrosoft公式資料で確認できます。

まとめ

C#からSQL Serverを扱うときは、Microsoft.Data.SqlClient、短い接続スコープ、明示的なパラメーター型、目的に合うExecuteメソッド、確実な破棄を基本にします。更新では影響行数を確認し、複数変更だけをトランザクションへまとめます。読み取り、更新、失敗時ロールバックを検証用データで確認してから利用範囲を広げれば、問題の原因と戻し方が明確な実装になります。

この記事を書いた人

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

コメント

コメント一覧 (1件)

  • 接続文字列に何を書いていいかがわかりません。どこにも載っていません。

コメントする

目次