なべひろBlog

プログラミングをメインに仕事に関するアレコレを発信しています。

C#でNpgsqlを使ってPostgreSQLへアクセス【SELECT】

もくじ
尚、以下のサンプルコードを基に記述したのにビルドエラーが出る場合は「using System.Data;」が抜けている可能性がありますので確認してください。
はじめに
もしかしたら一番メインになる部分かもしれません。
データは一度INSERTすればそれで終わりですが、データを参照する回数はINSERTより多いかもしれませんね。
Npgsqlの本家情報は
私の作成するサンプルソースファイルは
基本的なテーブルは下記構成となりますが不動所数点と浮動小数点の解説に関しては別のテーブルを用いて解説します。
テーブル名 概要
id serial 自動的にセットされる通し番号
time timestamp トランザクション開始時刻または入力された日付
name text 任意の文字列
numeric integer 任意の数値
基本的なSELECT
一番シンプルで可読性の高い手法です。
但し、取得したデータはobject型なのでキャストする必要があります。
using NpgsqlCommand cmd = new("SELECT * FROM data;", con);
using NpgsqlDataReader rd = cmd.ExecuteReader();
while (rd.Read())
{
    Console.WriteLine($"name:{rd["name"]} numeric:{rd["numeric"]}");
}
NpgsqlDataReader型のReadメソットがtrueを返さなくなるまでデータが存在しています。
d["name"]がカラム名を指定したデータの取得です。
プログラムコードを見て、どのデータを取得しているのかとても分かりやすいです。
データベーステーブルのn番目のカラムデータを取得
取得するカラムを数値で指定し、数値(変数)を変化させたい時に有効かもしれません。
using NpgsqlCommand cmd = new("SELECT * FROM data;", con);
using NpgsqlDataReader rd = cmd.ExecuteReader();
while (rd.Read())
{
    Console.WriteLine($"name:{rd[2]} numeric:{rd[3]}");
}
但し、この例では「SELECT *」としているので必要なカラムが何番目かを把握する必要があります。
「nameは」3番めなので0基点で2となり「numeric」は4番目なので0基点で3となります。
SELECTのn番目のカラムデータを取得
必要なデータだけを取得する場合は「SELECT *」ではなく「SELECT name,numeric」なクエリ文にしますが、この場合はSELECTで明確化されたカラムの順番となります。
using NpgsqlCommand cmd = new("SELECT name,numeric FROM data;", con);
using NpgsqlDataReader rd = cmd.ExecuteReader();
while (rd.Read())
{
    Console.WriteLine($"name:{rd[0]} numeric:{rd[1]}");
}
この場合は「name」が1番めなので0基点で0となり「numeric」が2番めなので0基点で1となります。
NpgsqlCommand.FieldCountでカラム数を参照
状況によっては利用価値があると思いますが、私は今まで一度も使った事がありません。
今後役に立つかもしれませんので参考までに.
using NpgsqlCommand cmd = new("SELECT name,numeric FROM data;", con);
using NpgsqlDataReader rd = cmd.ExecuteReader();
while (rd.Read())
{
    for (int i = 0; i < rd.FieldCount; i++)
    {
        Console.WriteLine($"{rd[i]}");
    }
}
FieldCountプロパティは1行あたりのカラム数を示します。
これをfor文で回せばrd[n]でnの値を間違えなく(例外を発生させない)プログラムが実行できます。
DbDataReader.GetValueで取得
こんなメソッドもあります。
using NpgsqlCommand cmd = new("SELECT name,numeric FROM data;", con);
using NpgsqlDataReader rd = cmd.ExecuteReader();
while (rd.Read())
{
    Console.WriteLine($"name:{rd.GetValue(0)} numeric:{rd.GetValue(1)}");
    Console.WriteLine($"name:{rd.GetValue("name")} numeric:{rd.GetValue("numeric")}");
}
GetValueメソッドはn番目、またはカラム名で取得できます。
これもobject型なのでキャストが必要となります。
型を明確にして取得
若干取り扱いに注意が必要ですが、キャスト記述が省略でき可読性も向上するかもしれません。
using NpgsqlCommand cmd = new(@$"SELECT id,name,numeric FROM data;", con);
using NpgsqlDataReader rd = cmd.ExecuteReader();
try
{
    while (rd.Read())
    {
        // PostgreSQLではinteger型をGetInt16で取得可能だが符号あり16bitで表現できる数値以上だと例外「OverflowException」となる。
        Console.WriteLine($"id:{rd.GetInt16(0)} name:{rd.GetString(1)} numeric:{rd.GetInt32(2)}");
        // GetInt16,GetStringなどは引数にカラム名を用いて明確にできる
        Console.WriteLine($"id:{rd.GetInt16("id")} name:{rd.GetString("name")} numeric:{rd.GetInt32("numeric")}");
        // 数値をGetStringで取得すると「InvalidCastException」が発生する
        Console.WriteLine($"id:{rd.GetInt16("id")} name:{rd.GetString("name")} numeric:{rd.GetString("numeric")}");
    }
}
catch (InvalidCastException)
{
    // 無効なキャストまたは明示的な型変換に対してスローされる例外
}
catch (OverflowException)
{
    // 算術演算、キャスト演算、または変換演算の結果オーバーフローが発生した場合にスローされる例外
}
8行目ではserial型のidをGetInt16で取得しています。
GetInt16は「16bit符号あり」のデータを対象としています。
しかしidが「16bit符号あり」で表現できる数値の場合は問題なくデータが取得できますが、それ以上のデータとなった時に例外「OverflowException」が発生します。
デバッグ時には不具合が起きないが、長く使っていると不具合が起きるパターンとなるので注意が必要です。
10行目ではカラム名を直接指定しており、このように明確なカラム名とする事でミスを減らせます。
12行目はinteger型のnumericを文字列取得用の「GetString」で取得しようとしていますが、これは例外「InvalidCastException」が発生します。
データは正しい型で取得しないとダメな例です。
時刻データを取得してみる
時刻データはWHEREで日付を指定するやり方が多いかと思いますが、こんな事もできるという例です。
using NpgsqlCommand cmd = new("SELECT time FROM data;", con);
using NpgsqlDataReader rd = cmd.ExecuteReader();
while (rd.Read())
{
    // PostgreSQLのtimestampをGetDateTimeメソッドで取得
    Console.WriteLine($"time:{rd.GetDateTime(0)}");
    // GetDateTimeメソッドで取得した値はC#のDateTime型である
    DateTime t = rd.GetDateTime(0);
    DateTime t = rd.GetDateTime("time");
    // GetTimeStampメソッドでも取得できるがNpgsqlDateTime型なので上記より利便性は落ちるかも
    NpgsqlDateTime t = rd.GetTimeStamp(0);
}
GetDateTimeはtimestamp型を取得できます。
GetDateTimeメソッドの引数は前に解説した時と同じく番号でもカラム名でも可能です。
そしてC#のDateTime型に値を入れるのはキャスト不要なので楽です。
また、GetTimeStampメソットでNpgsqlDateTime型として取得も可能です。
浮動小数点
C#で浮動小数点を使うならsingleかdoubleが多いかと思いますのでこの2つで解説します。
併せて違う型にした時の問題点も解説します。
using NpgsqlCommand cmd = new("CREATE TABLE data(real_data real, double_data double precision)", con);
_ = cmd.ExecuteNonQuery();
// データをINSERTする
cmd.CommandText = "INSERT INTO data(real_data, double_data) VALUES (1.1, 1.1);";
int result1 = cmd.ExecuteNonQuery();
cmd.CommandText = ("SELECT * FROM data;");
using NpgsqlDataReader rd = cmd.ExecuteReader();
try
{
    while (rd.Read())
    {
            // 標準的なデータ取得
        {
            float value1 = rd.GetFloat("real_data");
            double value2 = rd.GetDouble("double_data");
            Console.WriteLine($"{value1} , {value2}");
        }

        // double precisionをGetFloatすると例外「InvalidCastException」
        {
            float value1 = rd.GetFloat("real_data");
            float value2 = rd.GetFloat("double_data");
            Console.WriteLine($"{value1} , {value2}");
        }

        // realをGetDoubleで取得できるがreal値1.1が1.100000023841858になる
        {
            double value1 = rd.GetDouble("real_data");
            double value2 = rd.GetDouble("double_data");
            Console.WriteLine($"{value1} , {value2}");
        }
    }
}
catch (InvalidCastException)
{
    // 無効なキャストまたは明示的な型変換に対してスローされる例外
}
データテーブルは「real」型と「double precision」型が入るテーブルです。
どちらも各型で「1.1」をINSERTしています。
14行目及び15行目はテーブルと同じ型(real型はGetFloatメソッドで、double precision型はGetDoubleメソッド)で取得していますので、取得した結果はどちらも1.1となります。
21行目と22行目はどちらもreal型用のGetFloatメソッドでデータを取得してみましたがdouble precision型の値を取得する時に例外「InvalidCastException」が発生します。
28行目と29行目はどちらもdouble precision型のGetDoubleメソッドでデータを取得してみましたがdouble precision型は正しく1.1と取得できますがreal型だった方は取得した値が1.100000023841858となります。
例えば2つの実数値を足せば1になるのが当たり前の計算も浮動小数点の足し算では0.99999999となる事をご存知の方なら納得するかと思います。
正しい型を使用しないと期待した値にならないのは仕方ありませんのでMath.Roundなどを使い値の丸めをする必要があります。
配列
前回のINSERT編でも解説しましたがSELECTテクニックという事で配列のSELECTを解説します。
INSERT手法は省略しますが、テーブル名「data」にboolean型の配列「result」がある事とします。
cmd.CommandText = ("SELECT result FROM data;");
using var rd = cmd.ExecuteReader();
while (rd.Read())
{
    bool[] result1 = (bool[])rd["result"];
    bool[] result2 = rd.GetFieldValue<bool[]>("result");
    List<bool> result3 = new((bool[])rd["result"]);
    List<bool> result4 = new(rd.GetFieldValue<bool[]>("result"));
}
自分が分かりやすい方法、配列かListかで使い分けすればいいと思います。
関連記事