C# Tips | 非同期・並列処理:非同期DBアクセス

C# C#
スポンサーリンク
スポンサーリンク

非同期DBアクセスとは「クエリの結果を待っている間にスレッドを解放する」ことです

業務・実務でC#を使っている読者のみなさんにとって、 データベースアクセスはシステムの心臓部と言ってよいほど重要な要素です。

  • ユーザー情報を取得する
  • 受注データを登録する
  • 集計結果を計算する
  • ログや監査情報を保存する

こうした処理はすべて「DBへの問い合わせ」であり、 クエリが実行されて結果が返ってくるまでの待ち時間が必ず発生します。

もしこれをすべて同期的に書いてしまうと、

  • サーバーのスレッドが待ちっぱなしになる
  • 同時接続数が増えると一気にパフォーマンスが落ちる
  • Webアプリ全体のレスポンスが遅くなる

といった問題が起きます。

非同期DBアクセスとは、 「DBクエリの結果を待っている間、スレッドを解放しておき、 終わったら続きを再開する」ための仕組みです。

ここから、プログラミング初心者の方にも分かるように、 ステップバイステップで非同期DBアクセスの考え方と実装方法を解説し、 業務で使えるユーティリティやセキュリティ・品質上の注意点まで丁寧に掘り下げていきます。

ステップ1:同期DBアクセスの問題点をイメージする

典型的な同期DBアクセスのイメージ(ADO.NET)

まず、古典的な同期DBアクセスの例を考えてみます。 ここではSQL Serverを例にします。

using System;
using System.Data.SqlClient;

public class SyncUserRepository
{
    private readonly string _connectionString;

    public SyncUserRepository(string connectionString)
    {
        _connectionString = connectionString;
    }

    public UserDto? GetUserById(int id)
    {
        Console.WriteLine("同期DBアクセス開始");

        using var connection = new SqlConnection(_connectionString);
        connection.Open(); // 同期的に接続

        string sql = "SELECT Id, Name, Email FROM Users WHERE Id = @Id";

        using var command = new SqlCommand(sql, connection);
        command.Parameters.AddWithValue("@Id", id);

        using var reader = command.ExecuteReader(); // 同期的にクエリ実行

        UserDto? user = null;
        if (reader.Read())
        {
            user = new UserDto
            {
                Id = reader.GetInt32(0),
                Name = reader.GetString(1),
                Email = reader.GetString(2)
            };
        }

        Console.WriteLine("同期DBアクセス終了");
        return user;
    }
}

public class UserDto
{
    public int Id { get; set; }
    public string Name { get; set; } = "";
    public string Email { get; set; } = "";
}
C#

このコードでは、

  • connection.Open() がDB接続完了までスレッドを占有します。
  • ExecuteReader() がクエリ完了までスレッドを占有します。
  • DBが重い・ネットワークが遅い・ロックが発生しているなどの状況では、待ち時間が長くなります。

サーバー側で大量のリクエストが同時にこのメソッドを呼ぶと、

  • スレッドが「DB待ち」で埋まる
  • 同時接続数が増えるとレスポンスが急激に悪化する

という状態になります。

ステップ2:ADO.NETの非同期メソッドでDBアクセスをasync/awaitにする

SqlConnection.OpenAsync/SqlCommand.ExecuteReaderAsyncを使う

ADO.NETには、DBアクセスを非同期で行うためのメソッドが用意されています。

using System;
using System.Data.SqlClient;
using System.Threading.Tasks;

public class AsyncUserRepository
{
    private readonly string _connectionString;

    public AsyncUserRepository(string connectionString)
    {
        _connectionString = connectionString;
    }

    public async Task<UserDto?> GetUserByIdAsync(int id)
    {
        Console.WriteLine("非同期DBアクセス開始");

        using var connection = new SqlConnection(_connectionString);
        await connection.OpenAsync(); // 非同期接続

        string sql = "SELECT Id, Name, Email FROM Users WHERE Id = @Id";

        using var command = new SqlCommand(sql, connection);
        command.Parameters.AddWithValue("@Id", id);

        using var reader = await command.ExecuteReaderAsync(); // 非同期クエリ実行

        UserDto? user = null;
        if (await reader.ReadAsync()) // 非同期で1行読み込み
        {
            user = new UserDto
            {
                Id = reader.GetInt32(0),
                Name = reader.GetString(1),
                Email = reader.GetString(2)
            };
        }

        Console.WriteLine("非同期DBアクセス終了");
        return user;
    }
}
C#

呼び出し側の例:

public static async Task Main(string[] args)
{
    var repo = new AsyncUserRepository("Server=...;Database=...;Trusted_Connection=True;");
    var user = await repo.GetUserByIdAsync(123);

    if (user != null)
    {
        Console.WriteLine($"ID: {user.Id}, Name: {user.Name}, Email: {user.Email}");
    }
}
C#

ここでのポイントは次のとおりです。

  • OpenAsync/ExecuteReaderAsync/ReadAsync は非同期メソッドであり、DB待ちの間にスレッドを解放します。
  • await によって、「接続完了・クエリ完了・行読み込み完了後に自然な形で処理を再開する」ことができます。
  • サーバー側では、同じスレッドで他のリクエストを処理できるため、スケーラビリティが向上します。

ステップ3:Entity Framework Coreでの非同期DBアクセス(実務で最もよく使うパターン)

EF CoreのToListAsync/FirstOrDefaultAsyncなどを使う

業務では、ORM(Object-Relational Mapping)として Entity Framework Core(EF Core)を使うケースが非常に多いです。

EF Coreは、非同期メソッドを標準で提供しています。

using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;

public class AppDbContext : DbContext
{
    public DbSet<UserEntity> Users { get; set; } = null!;
}

public class UserEntity
{
    public int Id { get; set; }
    public string Name { get; set; } = "";
    public string Email { get; set; } = "";
}

public class EfUserRepository
{
    private readonly AppDbContext _db;

    public EfUserRepository(AppDbContext db)
    {
        _db = db;
    }

    public async Task<UserEntity?> GetUserByIdAsync(int id)
    {
        // 非同期で1件取得
        return await _db.Users.FirstOrDefaultAsync(u => u.Id == id);
    }

    public async Task<List<UserEntity>> GetAllUsersAsync()
    {
        // 非同期で一覧取得
        return await _db.Users.ToListAsync();
    }
}
C#

呼び出し側:

public static async Task Main(string[] args)
{
    using var db = new AppDbContext(/* options */);
    var repo = new EfUserRepository(db);

    var user = await repo.GetUserByIdAsync(123);
    var allUsers = await repo.GetAllUsersAsync();

    Console.WriteLine($"ユーザー数: {allUsers.Count}");
}
C#

ここでのポイントは次のとおりです。

  • EF Coreは ToListAsync/FirstOrDefaultAsync/SingleOrDefaultAsync/CountAsync など、 非同期版のクエリメソッドを多数提供しています。
  • これらを使うことで、DB待ちの間にスレッドを解放しつつ、 「ほぼ同期処理と同じ書き心地」でクエリを書けます。
  • 業務ロジック側は「どのデータを取りたいか」に集中でき、 非同期処理の詳細はEF Coreに任せることができます。

ステップ4:非同期DBアクセスユーティリティのテンプレート(ADO.NET版)

汎用的な「非同期クエリ実行」ユーティリティ

using System;
using System.Collections.Generic;
using System.Data;
using System.Data.SqlClient;
using System.Threading.Tasks;

public static class DbAsyncUtility
{
    public static async Task<List<T>> QueryAsync<T>(
        string connectionString,
        string sql,
        Action<SqlParameterCollection> parameterBinder,
        Func<IDataRecord, T> mapper)
    {
        var results = new List<T>();

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

        using var command = new SqlCommand(sql, connection);
        parameterBinder(command.Parameters);

        using var reader = await command.ExecuteReaderAsync();

        while (await reader.ReadAsync())
        {
            results.Add(mapper(reader));
        }

        return results;
    }

    public static async Task<int> ExecuteNonQueryAsync(
        string connectionString,
        string sql,
        Action<SqlParameterCollection> parameterBinder)
    {
        using var connection = new SqlConnection(connectionString);
        await connection.OpenAsync();

        using var command = new SqlCommand(sql, connection);
        parameterBinder(command.Parameters);

        return await command.ExecuteNonQueryAsync();
    }
}
C#

利用例:ユーザー一覧取得

var users = await DbAsyncUtility.QueryAsync<UserDto>(
    connectionString: "Server=...;Database=...;Trusted_Connection=True;",
    sql: "SELECT Id, Name, Email FROM Users WHERE IsActive = 1",
    parameterBinder: parameters =>
    {
        // 今回はパラメータなし
    },
    mapper: record =>
    {
        return new UserDto
        {
            Id = record.GetInt32(0),
            Name = record.GetString(1),
            Email = record.GetString(2)
        };
    });
C#

利用例:ユーザー更新

int affected = await DbAsyncUtility.ExecuteNonQueryAsync(
    connectionString: "Server=...;Database=...;Trusted_Connection=True;",
    sql: "UPDATE Users SET Email = @Email WHERE Id = @Id",
    parameterBinder: parameters =>
    {
        parameters.AddWithValue("@Email", "[email protected]");
        parameters.AddWithValue("@Id", 123);
    });

Console.WriteLine($"更新件数: {affected}");
C#

ここでのポイントは次のとおりです。

  • ユーティリティ側は「接続・クエリ実行・非同期処理」に集中し、 呼び出し側は「パラメータの設定」と「結果のマッピング」に集中できます。
  • 非同期DBアクセスのパターンを統一することで、 プロジェクト全体のコードが読みやすくなり、保守性が向上します。

ステップ5:非同期DBアクセスで必ず押さえておくべき重要ポイント(深掘り)

1. トランザクションも非同期で扱う

DB更新では、トランザクションが重要です。 非同期でもトランザクションを使うことができます。

public async Task UpdateUserEmailAsync(string connectionString, int id, string newEmail)
{
    using var connection = new SqlConnection(connectionString);
    await connection.OpenAsync();

    using var transaction = connection.BeginTransaction();

    try
    {
        string sql = "UPDATE Users SET Email = @Email WHERE Id = @Id";

        using var command = new SqlCommand(sql, connection, transaction);
        command.Parameters.AddWithValue("@Email", newEmail);
        command.Parameters.AddWithValue("@Id", id);

        await command.ExecuteNonQueryAsync();

        transaction.Commit();
    }
    catch
    {
        transaction.Rollback();
        throw;
    }
}
C#

ポイント:

  • トランザクションは同期的に開始しますが、 中のクエリ実行は非同期にできます。
  • 例外が発生した場合は必ず Rollback し、 正常終了時には Commit します。
  • 非同期でも「更新の一貫性」を守ることが重要です。

2. 接続プールと非同期の関係

ADO.NETやEF Coreは、接続プールを内部的に利用します。

  • 非同期DBアクセスでも接続プールは有効です。
  • ただし、接続を長時間占有すると、プールが枯渇します。

そのため、

  • クエリは必要なものだけに絞る
  • 不要な待ち時間(Thread.Sleep など)をトランザクション内に入れない
  • 長時間ロックを持つような設計を避ける

といった工夫が必要です。

3. Result/Waitで同期的に待たない

非同期DBアクセスを導入したのに、 呼び出し側で Result や Wait() を使ってしまうと、 デッドロックやスレッド占有の問題が再び顔を出します。

悪い例:

public UserDto? GetUserSync(string connectionString, int id)
{
    var repo = new AsyncUserRepository(connectionString);
    return repo.GetUserByIdAsync(id).Result; // 同期的に待つ
}
C#

基本方針はシンプルです。

「非同期メソッドは、非同期のまま await でつなぐ」 「Result や Wait() で無理やり同期に戻さない」

ステップ6:セキュリティ・品質の観点から見た非同期DBアクセスの注意点

1. SQLインジェクション対策(パラメータ化クエリを徹底する)

DBアクセスでは、 SQLインジェクションへの対策が必須です。

悪い例:

string sql = $"SELECT Id, Name, Email FROM Users WHERE Name = '{name}'";
C#

これは、name に悪意ある文字列が入ると、 任意のSQLを実行される危険があります。

良い例:

string sql = "SELECT Id, Name, Email FROM Users WHERE Name = @Name";

using var command = new SqlCommand(sql, connection);
command.Parameters.AddWithValue("@Name", name);
C#

非同期でも同期でも、 「必ずパラメータ化クエリを使う」ことが重要です。

2. 接続文字列の保護

接続文字列には、

  • DBサーバーのホスト名
  • データベース名
  • 認証情報(ユーザー名・パスワード)

などが含まれます。

これをソースコードにベタ書きすると、

  • リポジトリに接続情報が残る
  • 誤って公開された場合に重大なインシデントになる

というリスクがあります。

対策としては、

  • 接続文字列は設定ファイル(appsettings.json など)に置く
  • 機密情報部分は環境変数やSecret管理(Azure Key Vaultなど)に分離する
  • ログに接続文字列を出さない

といった方針が重要です。

3. エラーハンドリングとログ

DBアクセスでは、次のようなエラーが発生し得ます。

  • 接続エラー(ネットワーク・認証・DB停止など)
  • タイムアウト
  • 一意制約違反
  • 外部キー制約違反

非同期メソッドでも例外は普通に発生しますので、 呼び出し側で try/catch を使って適切にハンドリングし、 ログに記録することが重要です。

ただし、

  • ログに機密情報(パスワード・個人情報)を含めない
  • SQL全文をそのまま出さない(必要ならマスクする)

といったセキュリティ配慮も忘れてはいけません。

まとめ:非同期DBアクセスは「クエリ待ちを味方にして、スレッドと接続を賢く使うための標準パターン」です

非同期DBアクセスを行うC#ユーティリティの本質は次の3つです。

  • ADO.NETの非同期メソッド(OpenAsync/ExecuteReaderAsync/ReadAsync/ExecuteNonQueryAsync など)やEF Coreの非同期クエリメソッド(ToListAsync/FirstOrDefaultAsync など)と async/await を組み合わせることで、DB待ちの間にスレッドを解放し、サーバーのスケーラビリティとレスポンスを向上させること。
  • 汎用ユーティリティ(DbAsyncUtility など)やリポジトリクラスを用意し、業務コード側は「どのデータを取りたいか/更新したいか」に集中できるようにすることで、非同期DBアクセスの品質と一貫性を高めること。
  • SQLインジェクション対策(パラメータ化クエリ)、接続文字列の保護、トランザクションの適切な利用、例外処理とログ設計など、セキュリティ・品質面の注意点を意識しながら、「非同期DBアクセス」をシステム設計の中で自然に組み込めるようにすること。

ここで紹介したステップとテンプレートをベースにすることで、 読者のみなさんは業務システムの中に 「非同期DBアクセスを軸にした、効率的で安全なデータアクセスレイヤー」を 自然に組み込めるようになります。

タイトルとURLをコピーしました