@nao1928

Are you sure you want to delete the question?

If your question is resolved, you may close it.

Leaving a resolved question undeleted may help others!

We hope you find it useful!

C#のデータベース操作についての質問

解決したいこと

環境
OS:Windows10
Visual Studio 2019 Ver16.11.36
.NET Framework Ver 4.8.09037
SQL Server Ver16.0.1000.6

現在勉強中の初心者の為意味不明な質問かもしれませんが予めご了承ください。

次のプログラムを作成しました。

①3つのデータベース操作の共通メソッドを作成。
②作成した共通メソッドを使いメインクラスで初期化時にデータベースへ接続する
③SELECTしてデータを取得
④取得したしたデータから必要なデータを取り出しデータグリッドビューに表示
⑤データベースから切断

そこで質問なのですが、データベース操作をする際は、『レコードオブジェクト』『コマンドオブジェクト』『コネクションオブジェクト』をそれぞれ破棄?しないといけないと聞いたのですが、このソースコードではそれができているのでしょうか。
『コマンドオブジェクト』と『コネクションオブジェクト』はデータベース切断で破棄出来ていて、『レコードオブジェクト』はデータレコーダーのメソッドを呼ぶ際に毎回初期化(NULL代入)しているので大丈夫だと思うのですが、その認識で合っていますか。

以下が実際のソースコードを質問用に簡略化し編集したものです。編集する際に何か誤植があるかもしれませんが実行して求めている結果は得られています。

該当するソースコード

・共通メソッド
--------------------------------------------------------------------------------------
namespace Common
{
    public class DB
    {
        #region 変数宣言
        private string _connectionString = "";  // 接続文字列
        private SqlConnection _objConnection;   // 接続オブジェクト
        private SqlTransaction _objTransaction; // トランザクションオブジェクト
        #endregion

        #region Connect
        /// <summary>
        /// データベースの接続
        /// </summary>
        /// <param name="connectionString"></param>
        /// <returns></returns>
        public bool Connect(string connectionString)
        {
            this._connectionString = connectionString;

            if (this._objConnection == null)
            {
                try
                {
                    this._objConnection = new SqlConnection(this._connectionString);
                    this._objConnection.Open();
                }
                catch 
                {
                    return false;
                }
            }

            // 成功
            return true;
        }
        #endregion

        #region Disconnect
        /// <summary>
        /// データベースの切断
        /// </summary>
        public void Disconnect()
        {
            if (this._objConnection != null)
            {
                this._objConnection.Close();
                this._objConnection.Dispose();
            }
            this._objConnection = null;
        }
        #endregion

	/// <summary>
        /// 検索SQL実行
        /// </summary>
        /// <param name="sql">(I) 選択型SQL文</param>
        /// <returns>SqlDataReaderオブジェクト</returns>
        public SqlDataReader CreateDataReader
            (
                string sql	// (I) 選択型SQL文
            )
        {
            if (this._objConnection == null)
            {
                this.Connect(this._connectionString);
            }

            // 実行結果(データリーダー)
            SqlDataReader objReader = null;

            // 実行
            var cmd = new SqlCommand(sql, this._objConnection);

            try
            {
                objReader = cmd.ExecuteReader();
            }
            catch
            {
                throw
            }

            // 結果
            return objReader;
        }

    

    }
}

------------------------------------------------------------------------------------------------------------------------------

・メインクラス
------------------------------------------------------------------------------------------------------------------------------


namespace DBTest
{
    public partial class DBTest : Form
    {
        
	#region INIファイルのパス
        private const string connectionString = "xxx";
        #endregion
	
	#region コンストラクタ
	public DBTest()
        {
            InitializeComponent();
        }
	#endregion

        #region フォーム初期化時
	private void DBTest_Shown(object sender, EventArgs e)
        {
            try
            {
		//DBクラスをインスタンス
            	var objDb = new Common.DB();

            	try
            	{
                   if(objDb.Connect(xxx))
                   {
			#region SQL文作成
			string sql = $@" yyy ";
			#endregion

			//データリーダー実行
                    	DbDataReader reader = objDb.CreateDataReader(sql);

			//データグリッドビューに表示する処理//


	    }
            catch (Exception ex)
            {
                MessageBox.Show(ex.Message);
            }
	  finally
            {
                //データベース接続解除
                objDb.Disconnect();
            }
        }
        #endregion
0 likes

2Answer

開発環境を質問欄を編集して追記してください。OS, Visual Studio, SQL Server のバージョン、.NET Framework or .NET/.NET Core どっちかとそのバージョンなど。


そこで質問なのですが、データベース操作をする際は、『レコードオブジェクト』『コマンドオブジェクト』『コネクションオブジェクト』をそれぞれ破棄?しないといけないと聞いたのですが、このソースコードではそれができているのでしょうか。

できてません。

IDisposable インターフェースを継承しているクラスは、インスタンスを作成して使い終わった後は Dispose する必要があります。

特に重要なのが SqlConnection で、これを Dispose しないと接続が接続プールに戻されず、プール内の接続が枯渇してついには重大な問題を起こす可能性があります。

そこは finally 句の objDb.Disconnect(); で対処されているようですが、SqlCommand, SqlDataReader が対応されてないようです。

SqlDataReader については Close しないと以下の記事に書いてあるような問題が出るかもしれません。

Multiple Active Result Sets (MARS)
http://surferonwww.info/BlogEngine/post/2022/09/15/multiple-active-result-sets.aspx

 『コマンドオブジェクト』と『コネクションオブジェクト』はデータベース切断で破棄出来ていて、『レコードオブジェクト』はデータレコーダーのメソッドを呼ぶ際に毎回初期化(NULL代入)しているので大丈夫だと思うのですが、その認識で合っていますか。

合ってません。null を代入しても Dispose されたことにはなりません。using 句を使うことをお勧めします。以下のような感じ。

using(SqlConnection conn = new SqlConnection("接続文字列"))
{
 
  conn.Open();
 
  using(SqlCommand cmd = new SqlCommand("クエリ", conn))
  {
    using (SqlDataReader reader = cmd.ExecuteReader())
    {
      if (reader != null)
      {
        while (reader.Read())
        {
          // 何らかの処置
        }
      }
    } // SqlDataReader の Dispose(注1)
 
  } // SqlCommand の Dispose
 
} // SqlConnection の Dispose(注2)

注1: SqlDataReader によって使用されているリソースを解放し、Close メソッドを呼び出します。

注2: SqlConnection クラスの Close メソッドと Dispose メソッドは、機能的に同じです。

あと、本題とは直接関係ないことですが、例外処置のやり方がダメです。とくに Exception をキャッチしているところ。詳しくは書きませんが、興味があれば新たに別のスレッドを立てて質問してください。

3Like

Comments

  1. @nao1928

    Questioner

    回答ありがとうございます。
    環境を追記しました。

    メソッドを使って書きたいのですが、その場合データ取得メソッドのreturnの後に、SqlCommandとSqlDataReaderのDispose処理を追加すれば良いのでしょうか。

    また最近Qiitaに登録した為「別のスレッドを立てて質問」とはどういう意味か分かりかねます。同じソースコードを張り付けて例外処理についてもう一度質問すれば良いという事ですか。

  2. 環境を追記しました。

    間違ってるようです。追記した、

    .NET Framework Ver 4.8.09037
    SQL Server Ver16.0.1000.6

    は Visual Studio のバージョン情報を表示するダイアログから取得したと思いますが、だとするとそれではありません。NET Framework or .NET/.NET Core どっちかも分からないようですが、それではこの先話が通じませんので勉強してください。

    メソッドを使って書きたいのですが、その場合データ取得メソッドのreturnの後に、SqlCommandとSqlDataReaderのDispose処理を追加すれば良いのでしょうか。

    「メソッドを使って書きたい」の意味が不明ですが、どのようにするにせよ、確実に Dispose されるように書くということです。

    「別のスレッドを立てて質問」というのはどういう事でしょうか。

    一つのスレッドでは一つの課題と一つの質問に限って、それに対する回答になるようにしてください。今回のスレッドは DB 操作の際のオブジェクトの破棄の話。

    そうするのがここのような Q&A サイトの基本です。例外処置はこのスレッドの課題とは直接関係ないので、その質問をするなら別にスレッドを立ててくださいということです。

  3. @nao1928

    Questioner

    説明が下手で申し訳ないです。

    回答していただいたソースコードではデータベース操作のメソッドを作らない書き方なので、メソッドを作成する場合はデータ読み取りメソッド内のreturnの後に書けば良いのでしょうかという意味です。

  4. .NET Framework or .NET/.NET Core どっちかを、ちゃんと勉強して調べて、書いてください。それ、結構重要です。

    メソッドを作ると言ってますが、質問のコードを見る限り特に有用なことはなさそうに思えます。何か考え違いで見当はずれなことをしているようにしか見えません。でも、まぁ、それはちょっと置いといて・・・

    「returnの後に書けば良い」というのも意味不明ですが、そう思うなら、あなたの考えを、上の回答で書いたことをよく考えて、自力でコードを書いて提示してください。

  5. @nao1928

    Questioner

        ///


    /// 検索SQL実行
    ///

    /// (I) 選択型SQL文
    /// SqlDataReaderオブジェクト
    public SqlDataReader CreateDataReader
    (
    string sql // (I) 選択型SQL文
    )
    {
    if (this._objConnection == null)
    {
    this.Connect(this._connectionString);
    }

            // 実行結果(データリーダー)
            SqlDataReader objReader = null;
    
            // 実行
            var cmd = new SqlCommand(sql, this._objConnection);
    
            try
            {
                objReader = cmd.ExecuteReader();
            }
            catch
            {
                throw
            }
    
            // 結果
            return objReader;
        }
    

    ↑このメソッドの一番最後のreturnの後にDispose処理を追加すればいいのでしょうかという事です。
    .NET Framework です。

  6. 実際にやってみましたか? エラーになりませんでしたか? 実際に試しもしないでテキトーなことを書いてませんか?

    .NET Framework です。

    どこを見てそう判断しましたか?

  7. @nao1928

    Questioner

    最初の質問にもある通りきちんと動いています。公開できない情報があったり実際はもう少し複雑なプログラムな事もあり、質問の所に貼っているソースコードは実際のものを編集したものになります。ですので誤植はあるかもしれません。逆にどこを見てエラーが起こりそうだと思われたのでしょうか。

    .NET Framework4.7.2です。
    プロジェクトのプロパティ→アプリケーション→対象のフレームワーク(G)というところを見て判断しました。

  8. 上の私のコメントで、

    「returnの後に書けば良い」というのも意味不明ですが、そう思うなら、あなたの考えを、上の回答で書いたことをよく考えて、自力でコードを書いて提示してください。

    と言いました。あなたがやることを期待しているのです。それをやらないで同じこと、

    このメソッドの一番最後のreturnの後にDispose処理を追加すればいいのでしょうかという事です。

    を聞いてくるから、

    実際にやってみましたか? エラーになりませんでしたか? 実際に試しもしないでテキトーなことを書いてませんか?

    と言ったのです。自力でコードを書いて提示してください。

    上のコメントにも書きましたが、あなたのメソッドは、質問のコードを見る限り、有用なことはなさそうで、何か考え違いで見当はずれなことをしているようにしか見えません。そういうコードを添削する気力は自分には有りません。悪しからず了承ください。

  9. @nao1928

    Questioner

    あなたのメソッドは、質問のコードを見る限り、有用なことはなさそうで、何か考え違いで見当はずれなことをしているようにしか見えません。

    質問の冒頭にも書いていますが、勉強中であり現在何かを作成しているわけではありません。

    1つのプロジェクト内で複数回データ操作したり、複数のプロジェクトでデータベースを操作することを想定して「Common」プロジェクトのようなものにデータベース操作の共通関数を作成しそれを呼びだして使用するプログラムを作ってみたいだけです。

    ですので知りたいことは『データベースを操作する方法』ではなく『"共通メソッドを作成し、それを用いて"データベースを操作する方法』です。質問のソースコード単体で見たときに有用かは関係なく共通メソッドを作りたいのです。

    そして質問内容は、質問のソースコードで「SqlConnection』「SqlCommand」「SqlDataReader」のDispose処理ができているか。できていなければ何処にその処理を追加すればいいのか。という2つです。ですので質問のソースコードで完結していると思うのですがこれ以上何を追記すれば良いのでしょうか。

  10. 質問の冒頭にも書いていますが、勉強中であり現在何かを作成しているわけではありません。

    だったら私の提案に素直に耳を傾けてはいかが? 少なくともあなたより知識と経験はあって、適切なアドバイスをしてると自負しているのですが。

    ですので質問のソースコードで完結していると思うのですがこれ以上何を追記すれば良いのでしょうか。

    それに対しては、あなたの最初の質問の回答として答えてます。聞く耳持ってますか? その回答を再掲しておきます。

    <回答再掲>
    できてません。

    IDisposable インターフェースを継承しているクラスは、インスタンスを作成して使い終わった後は Dispose する必要があります。

    null を代入しても Dispose されたことにはなりません。
    <ここまで>

    そして確実に Dispose できる方法として using 句を使う方法を、サンプルコードまで提示して説明しました。

    アドバイスを聞く聞かないはあなたの勝手ですが、聞かないのであればあとは自力で Dispose の実装をやってくださいということです。そのコードを提示したらそれに対するアドバイスぐらいはしますよ。

  11. 最後にもう一つアドバイスしておきます。これを読んでも(読まないかな?)聞く耳持たず我が道を行くということなら、あとは勝手にやってください。

    Windows Forms アプリで SQL Server のテーブルのレコードを DataGridView に表示する場合、DataSet / DataTable を DataGridView のデータソースとして使います。

    何故かと言うと、以下の記事の「非接続型のデータ更新」のセクションの図1と図2を見てください。文章は読まなくてもいいので図だけ見てください。

    DB 設計者のための明解 ADO.NET 第 1 回
    https://docs.microsoft.com/ja-jp/previous-versions/cc482903%28v=msdn.10%29

    ユーザーが DataGridView を操作(行の削除・追加・訂正)した結果は図1にあるように DataSet / DataTable に反映されます。ユーザーの編集操作が終わったら Update メソッドで図1の編集結果が一度に DB に反映されます。そういうアプリが簡単に作れるようにできているのです。

    質問のような SqlDataReader を使ったコードを書くことはありません。なので、質問者さんのコードは、「何か考え違いで見当はずれなことをしているようにしか見えません」と言いました。

    DataTable を取得するメソッドなら、以下のサンプルのようにメソッド内で using 句を使って、どこで Dispose するかに悩むことなく実現できます。

    private DataTable CreateDataTable()
    {
        string connString = @"・・・接続文字列・・・";
        string selectQuery = @"SELECT 文";
     
        using (var connection = new SqlConnection(connString))
        {
            using (var command = new SqlCommand(selectQuery, connection))
            {
                var adapter = new SqlDataAdapter(command);
                var table = new DataTable();
                adapter.Fill(table);
                return table;
            }
       }
    }
    

    もう一つ、.NET Framework 版の Windows Forms アプリを作っているなら、Visual Studio のデータソース構成ウィザードを使って、型付き DataSet / DataTable + TableAdapter + TableAdapterManager を作成して使ってください。。

    以下にその手順を書きますので、やってみてはいかがですか。

    DB が SQL Server の場合ですが、以下のチュートリアル、

    チュートリアル : データベースへのデータの保存 (単一テーブル)
    https://learn.microsoft.com/ja-jp/previous-versions/0f92s97z%28v=vs.120%29

    10 行でズバリ !! 非接続型のデータ アクセス (ADO.NET) (VB)
    https://github.com/microsoftarchive/msdn-code-gallery-community-0-9-non-alphabetic/tree/master/10%20%E8%A1%8C%E3%81%A7%E3%82%BA%E3%83%90%E3%83%AA%20%21%21%20%E9%9D%9E%E6%8E%A5%E7%B6%9A%E5%9E%8B%E3%81%AE%E3%83%87%E3%83%BC%E3%82%BF%20%E3%82%A2%E3%82%AF%E3%82%BB%E3%82%B9%20%28ADO.NET%29%20%28VB%29

    新しいデータ ソースの追加
    https://learn.microsoft.com/ja-jp/visualstudio/data-tools/add-new-data-sources?view=vs-2022

    ・・・のように Visual Studio のデータソース構成ウィザードを利用して型付 DataSet + TableAdapter を作ると、データソースウィンドウにそれが表示されるので、デザイン画面でそれを Form にドラッグ&ドロップします、

    DragDrop.jpg

    そうすると、以下のような、DataGridview ⇔ BindingSource / BindingNavigator ⇔ DataSet / DataTable ⇔ DataAdapter ⇔ SQL Server という構造のアプリが自動的に生成されます。

    result4.jpg

  12. @nao1928

    Questioner

    返信ありがとうございます。
    最初の回答はメイン画面の一番最後にDispose処理を追加しろという意味だったという事ですか。そのうえで違う書き方を示してくれたという事ですかね。もしそうでしたら私の読解力が足りていませんでした。申し訳ないです。

    最後のコメントのDataTable を取得するメソッドのアクセス修飾子をpubulicにして引数にSELECT文を設定し私のDB操作クラスのデータベース接続メソッドの下に置けばどこからでも呼び出して利用できるようになるのでしょうか。
    そして戻り値のデータテーブルを予め作成しておいた型付きデータセットに代入して利用するという事です合っていますか。

    それが綺麗で便利だという事は理解しました。しかしデータベースに対応するデータテーブルを毎回作りデータグリッドビューに表示する際はそのデータテーブルをさらに編集しないといけないという事ですよね。少しだけ操作をしたいとき用にリーダーも作りたいのですが、何か良い方法はありますでしょうか。

  13. 最初の回答はメイン画面の一番最後にDispose処理を追加しろという意味だったという事ですか。そのうえで違う書き方を示してくれたという事ですかね。

    違いますよ。質問に書いてあったあなたのコードのどこに Dispose を書けばいいかなんてことは一言も言ってません。

    私が言ったことの要点だけ繰り返しますと、

    (1) IDisposable インターフェースを継承しているクラスは、インスタンスを作成して使い終わった後は Dispose する必要があります。

    (2) 「returnの後に書けば良い」というのも意味不明ですが、そう思うなら、あなたの考えを、上の回答で書いたことをよく考えて、自力でコードを書いて提示してください。

    です。そして、(2) の結果のコードを見せてくれたら、それに対するアドバイスぐらいはしますよと言いました。

    そして、あなたのコメントの

    最後のコメントのDataTable を取得するメソッドのアクセス修飾子をpubulicにして・・・

    以降の件については、基本を勉強してから、その後でもまだ聞く必要があれば、質問内容をよく考えてから出直してください。基本的な知識をお持ちでないようなので、これ以上やり取りしても話は通じそうもないです。

  14. 補足しておくと、using を使うと Dispose 忘れのようなヒューマンエラーを防ぐ事が出来ます。初心者なればこそ、解放は基本的に Dispose が必要なオブジェクトはセットで using を使う習慣をつけておいた方がいいと思います。
    その上で、どうしても自分で Dispose を呼ぶ必要があるのであれば、try~finally のfinally 処理で行えばよいのではないでしょうか。

    少しだけ操作をしたいとき用にリーダーも作りたいのですが、何か良い方法はありますでしょうか。

    DataReaderを返さず、必要なデータを読み込んだDataTableを返せばよいでしょう。上でサンプル書いてくれてますよね。(CreateDataTableってやつ)
    ユーザーにDataReaderを公開すると、クラス側からはいつDataReaderが解放されたか判らないので困るし、ユーザー側からしてもDataReaderを解放するという手間が増えます。(他にも色々問題がありますが)

質疑応答を解きほぐして、まとめてみるわ。

1

【質問】

質問のソースコードで「SqlConnection」「SqlCommand」「SqlDataReader」のDispose処理ができているか。

【回答】

できてません。

【補足】
「SqlConnection」は明示的にDisposeメソッドを呼び出しているので、即時にDispose処理ができています。
「SqlCommand」「SqlDataReader」は、即時にDispose処理ができていません。
SqlCommand、SqlDataReaderオブジェクトはそれらが不要になった後で、やがては.NETが自動的にDispose処理を実行してくれますが、それがいつになるかは分かりません。
(メモリの都合などを考慮して.NET側がDisposeの実行タイミングを勝手に決めます。)

即時にDispose処理をするためには、明示的にDisposeメソッドの呼び出しやusing句の利用をする必要があります。

2

【質問】

できていなければ何処にその処理を追加すればいいのか。

【回答】

using 句を使うことをお勧めします。

【補足】
using句とは、ブロックを抜けるときに即時にDisposeを呼び出す構文です。
以下の2つのコードは同じ動きをします。

var obj = new SampleDisposableClass();
try {
    // 何かの処理
} finally {
    If (obj != null) {
        obj.Dispose();
    }
}
Using (var obj = new SampleDisposableClass()) {
    // 何かの処理
}

この知識は「using句って何?」と疑問に思った段階でweb検索等をすれば、質問者さん自身で得られる知識です。

3

【質問】

メソッドを使って書きたいのですが、その場合データ取得メソッドのreturnの後に、SqlCommandとSqlDataReaderのDispose処理を追加すれば良いのでしょうか。

回答していただいた(using 句を使う)ソースコードではデータベース操作のメソッドを作らない書き方なので、メソッドを作成する場合はデータ読み取りメソッド内のreturnの後に書けば良いのでしょうかという意味です。

【回答】

「returnの後に書けば良い」というのも意味不明ですが、そう思うなら、あなたの考えを、上の回答で書いたことをよく考えて、自力でコードを書いて提示してください。

【補足】
returnより後ろの行は実行されないので、メソッド内のreturnの後にDispose処理を書くことは間違っています。

returnの後にDispose処理を書いたコードをビルドすると「到達できないコードが検出されました」警告が発生するので、この間違いは自力でコードを書けば質問者さん自身で気づくことができる間違いです。

ではどこに書くべきかというと、例えば以下のように書くとよいでしょう。

  • SqlCommand
public SqlDataReader CreateDataReader
    (
        string sql      // (I) 選択型SQL文
    )
{
    if (this._objConnection == null)
    {
        this.Connect(this._connectionString);
    }

    // 実行結果(データリーダー)
    SqlDataReader objReader = null;

    // 実行
    using(var cmd = new SqlCommand(sql, this._objConnection)) // ●追加
    {

       try
       {
           objReader = cmd.ExecuteReader();
       }
       catch
       {
           throw
       }

    } // ●Disposeの呼び出し

    // 結果
    return objReader;
}
  • SqlDataReader

このオブジェクトは共通メソッドの実行後に、共通メソッドの外部で利用しています。
そのため共通メソッドの中にDispose処理を書くべきではありません。
共通メソッドの外(メインクラス)でDispose処理を書いてください。

//データリーダー実行
using(DbDataReader reader = objDb.CreateDataReader(sql)) { // ●追加

    //データグリッドビューに表示する処理//

} // ●Disposeの呼び出し
2Like

Your answer might help someone💌