C#でDB参照・自動型変換

毎度テーブルに合わせて型指定するの大変だな～で作ったもの

:::note warn
#### ※日付型・浮動小数点数には要注意(後述)
:::

## テストテーブル
テストなので意地の悪いテーブルを用意
| id | name | float | date | bool |
|:-:|:-:|:-:|:-:|:-:|
|  0 | 佐藤  | 0.01  |2019/07/02 0:00:00.000  | 0  |
|  1 |  ﾔﾏﾀﾞ | 0.20  |2023/03/03 3:33:33.333 | 1  |
|  2|  Mic |  1.00  |null | null  |

id(int)=int
name(varchar(n))=string
float(float)=float
date(datetime)=DateTime
bool(bit)=bool
として取得参照したい。

## 接続・取得クラス
#### 変換の弊害
浮動小数点数は2進数で格納➡ほとんどの10進数の小数は2進数で表現➡誤差がでる。
（範囲内であれば正確な格納も可能）
int.Parseなどの変換はnullチェックが必須（解析不可能の為、例外が発生してしまう）
なのでnullを許容するConvertクラスを使用。
DateTimeはただ変換するとミリ秒が削られることを留意。
DateTimeの値がnullの場合として後述のコードでもいいが以下の対策もある。
　1.クエリで抽出時にnullを許さない
　2.DateTime?型(null許容型)に入れる
　3.nullチェック後、任意の値を入れる

```c#:SQLRead.cs
using System;
using System.Collections.Generic;
using System.Data;
using System.Data.SqlClient;

public class SQLRead
{
    SqlConnectionStringBuilder builder = new SqlConnectionStringBuilder();
    public string error_message = null;
    public List<DateTime> dateTimes;
    //各々のDBを参照するなら引数で指定しよう
    public SQLRead()
    {
        builder.DataSource = "あどれす,ぽーと";
        builder.UserID = "ID";
        builder.Password = "PASS";
        builder.InitialCatalog = "データベース名";
        builder.ConnectTimeout = 20;
        //builder.IntegratedSecurity = true;
    }
    //呼び出す・取り出すを考えクエリ文(sql),列(column)を引数にしDataTableで返す
    public DataTable Read(string sql, string[] column)
    {
        //日付確認用
        dateTimes = new List<DateTime>()
        DataTable table = new DataTable();
        //DataTableに列指定（入れ物用意）
        foreach (string i in column) table.Columns.Add(i);
        SqlConnection con = new SqlConnection(builder.ConnectionString);
        try
        {
            con.Open();
            SqlCommand command = new SqlCommand(sql, con);
            SqlDataReader reader = command.ExecuteReader();

            while (reader.Read())
            {
                DataRow dr = table.NewRow();
                foreach (string m in column)
                {
                    System.Type type = reader[m].GetType(reader[m]);
                    //Nullがない場合はtypeで判断できる
                    if (if(type.Name == "DateTime")
                    {
                        //reader.GetOrdinal()でないとミリ秒がとれない
                        DateTime time = reader.GetDateTime(reader.GetOrdinal(m));
                        //文字列として入れる
                        dr[m] = time.ToString("yyyy-MM-dd HH:mm:ss.fff");
                        //文字列として入れる⓶
                        //dr[m] = String.Format("{0:yyyy-MM-dd HH:mm:ss.fff}", time);
                        //DataTableにDateTime型として入れるとミリ秒が削れる
                        //dr[m] = time;　➡2019/07/02 0:00:00
                        //reader.GetOrdinal()で取得しDateTime型にいれるなら問題ない
                        dateTimes.Add(time);
                        
                    }
                    //※●●.Parseはnullでは返れない為Convert
                    else dr[m] = Convert.ChangeType(reader[m], type);
                }
                table.Rows.Add(dr);
            }
        }
        catch (Exception ex) { this.error_message = ex.Message; }
        finally
        {
            con.Close();
            con.Dispose();
        }
        return table;
    }
}
```
## 参照クラス
```c#:
using System;
using System.Collections.Generic;
using System.Data;

private void allRead()
{
    string sql = "SELECT id,name,float,date,bool FROM test";//クエリ
    string[] column = new string[] { "id","name","float","date","bool" }//カラム指定
    SQLRead sql_read = new SQLRead();
    DataTable dt = sql_read.Read(sql,column);
    if(sql_read.error_message == null)
    {
        //dataGridViewをレイアウトに置くとすぐ目視出来て便利
        dataGridView1.DataSource = dt;
        dataGridView1.AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.AllCells;
        if (dt.Rows.Count > 0)
        {
            //取り出してみる
            for (int i = 0; i < dt.Rows.Count; i++)
            {
                Console.WriteLine("id:"+ dt.Rows[i].ItemArray[0].ToString());
                Console.WriteLine("name:"+ dt.Rows[i].ItemArray[1].ToString());
                Console.WriteLine("float:"+ dt.Rows[i][2].ToString());
                Console.WriteLine("date:"+ dt.Rows[i]["date"].ToString());
                Console.WriteLine("dateList:"+ dt.Rows[i]["bool"].ToString());
            }
        }
    }
    else Console.WriteLine(read.error_message);
    //List<DateTime>確認
    Console.WriteLine( sql_read.dateTimes.Count());
}
```
※以下2024-12-03追記
## 結果
![00.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3937966/713ae14a-23ad-3a2c-4da1-c07bc2a9e566.png)
Console.WriteLine( request.dateTimes.Count()) ➡ 2
nullがあってもエラー無く参照できている。
しかしdateとboolの値がnullの場合
Type type = reader[m].GetType()でのtype.NameがDBNullとして処理され値が入っていない。
またif(type.Name == "DateTime")に入らない為、sql_read.dateTimesリストのカウントが2だ。
クエリで整えた方がC#内での自動化としてはスッキリする。

```sql:
SELECT id,name,float,IsNull(date,'1900-01-01 00:00:00.000') as date,IsNull(bool,0) as bool FROM test
```
![04.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3937966/0dce9bdb-0f65-bc4c-aae5-bdff42c9d215.png)


## おまけ C#で任意の値を入れる
もはや自動ではない。
type.NameがDBNull...この場合カラムで見る等の工夫が必要。
nullチェック後、任意の値を入れる。苦行。
```c#:SQLRead.cs
    while(reader.Read())
    {
        DataRow dr = table.NewRow();
        foreach (string m in column)
        {
            Type type = reader[m].GetType();
            //カラム名で判別する
            if (m == "date")
            {
                if (type.Name == "DBNull") dr[m] = DateTime.Parse("1900-01-01 00:00:00.000").ToString("yyyy-MM-dd HH:mm:ss.fff");
                else
                {
                    DateTime time = reader.GetDateTime(reader.GetOrdinal(m));
                    dr[m] = time.ToString("yyyy-MM-dd HH:mm:ss.fff");
                }
            }
            else if(m == "bool")  dr[m] = type.Name == "DBNull"  || (Boolean)reader[m] == false ? false:true;
            else dr[m] = Convert.ChangeType(reader[m], type);
        }
    }
```