[Oracle] [C#] Timestamp with time zone 型へのアクセス方法

2020年7月28日

 VB.NET や C# から Timestamp with time zone 型へのアクセス方法について検討してみます。

 まずは公式ドキュメントを参照してみます。
 「OracleDataReaderオブジェクトからのデータの取得」をみると「表3-10 .NETタイプのアクセッサ」に Timestamp with time zone はタイムゾーン情報のない System.DateTime に対応していて、タイムゾーン情報のある System.DateTimeOffset とは対応しないことが読み取れます。
 また、「表3-11 ODP.NETタイプのアクセッサ」をみると、ODP.NET の型としては OracleTimeStampTZ 型が用意されていることがわかります。OracleTimeStampTZ 型については「OracleTimeStampTZ 構造」に詳細が記載されています。(おそらく Structure の直訳が「構造」なのでそうなっているだけで、日本語としては「OracleTimeStampTZ 構造体」が正しい気がする・・・)

 ここまでで予想できることは、.net のタイムゾーン情報のある日付型である DateTimeOffset 型と、Oracle の Timestamp with time zone 型, OracleTimeStampTZ 型の間には相互変換ができる仕組みがなさそうだ、ということです。

 ということで試してみます。

 以下のような表があるとします。
 TSTZ 列は Timestamp with time zone 型です。

SQL> select ID, TSTZ from TIME;

ID TSTZ
-- -------------------------------
 1 15-01-06 00:29:53.006000 +09:00
 2 15-01-05 10:29:53.488000 -05:00

 これを普通に取り出してみます。以下の例では Dapper を使用して select しています。

// using Oracle.ManagedDataAccess.Types;
// using Oracle.ManagedDataAccess.Client;
// using Dapper;

var connStr = "User Id = scott; password = tiger; data source = DBHOST/ORACLE_SID";
var conn = new OracleConnection(connStr);

var results = conn.Query("select ID, TSTZ from TIME order by ID");
foreach (var result in results)
{
    Console.WriteLine("{0}: {1:yyyy/MM/dd hh:mm.ss.fffffff}", result.ID, result.TSTZ);
}

 結果は以下です。タイムゾーン情報が抜け落ちた現地時間が DateTime 型で戻ってきます。
 DateTime 型ではなく DateTimeOffset 型で取得してくれたらよかったのですがそうではないようです。公式ドキュメント通りですね(汗
 したがって情報が欠落してしまうので Dapper でアクセスすることができません。(誰かよい方法を知っていたら教えてください。-_-;)

1: 2015/01/06 12:29.53.0060000
2: 2015/01/05 10:29.53.4880000

 そこで普通に OracleCommand を使用して OracleTimeStampTZ 型経由で取得を試みます。
 こちらも OracleTimeStampTZ 型ではなく DateTimeOffset 型で取得してくれたらよいのですが、GetDateTimeOffset メソッドはありません。これも公式ドキュメント通りです(汗

var connStr = "User Id = scott; password = tiger; data source = DBHOST/ORACLE_SID";
var conn = new OracleConnection(connStr);
conn.Open();

var cmd = new OracleCommand("select ID, TSTZ from TIME order by ID", conn);
var reader = cmd.ExecuteReader();

while (reader.Read())
{
    // 取得
    var ID = reader.GetInt32(0);
    var tstz = reader.GetOracleTimeStampTZ(1);

    Console.WriteLine("{0}: {1:yyyy/MM/dd}", ID, tstz);
}

conn.Close();

 しかし OracleTimeStampTZ 型は使い勝手がよくないです。
 理由のひとつは、OracleTimeStampTZ 型は Oracle 独自の型なので、この型をいろんなところで持ちまわすと、例えば将来データベースを変更するようなことが起こると大変なことになりそうです。
 もう一つの理由は、Console.WriteLine() などで書式指定した場合に、意図通りにならない点です。
 上記サンプルコードは書式として「yyyy/MM/dd」を指定していますが、出力結果は意図したものと異なっています。

1: 01/06/2015 00:29:53.006 +09:00
2: 01/05/2015 10:29:53.488 -05:00

 以上のようなことから、できるだけ .net 標準の DateTime 型または DateTimeOffset 型を使いたい。
 ということで、OracleTimeStampTZ 型と DateTimeOffset 型を相互に変換する拡張メソッドを用意することにしました。
 ただし実装はかなり力技です(汗 # 特に Replace("+", "") あたり・・・(汗

public static class OracleTsTzExtensions
{
    // OracleTimeStampTZ → DateTimeOffset
    public static DateTimeOffset ToDateTimeOffset(this OracleTimeStampTZ value)
    {
        return new DateTimeOffset((DateTime)value, TimeSpan.Parse(value.TimeZone.Replace("+", "")));
    }

    // DateTimeOffset → OracleTimeStampTZ
    public static OracleTimeStampTZ ToTimeStampTZ(this DateTimeOffset value)
    {
        return new OracleTimeStampTZ(value.DateTime, value.Offset.ToString());
    }
}

 OracleTimeStampTZ 型を DateTimeOffset 型に変換する拡張メソッドを使用すると、以下のように書けます。

var connStr = "User Id = scott; password = tiger; data source = DBHOST/ORACLE_SID";
var conn = new OracleConnection(connStr);
conn.Open();

var cmd = new OracleCommand("select ID, TSTZ from TIME order by ID", conn);
var reader = cmd.ExecuteReader();

while (reader.Read())
{
    // 取得
    var ID = reader.GetInt32(0);
    var tstz = reader.GetOracleTimeStampTZ(1);

    // DateTimeOffset 型に変更
    DateTimeOffset dt = tstz.ToDateTimeOffset();

    Console.WriteLine("{0}: {1:yyyy/MM/dd HH:mm.ss.fffffff K}", ID, dt);

}

conn.Close();

 結果は以下の通り。

1: 2015/01/06 00:29.53.0060000 +09:00
2: 2015/01/05 10:29.53.4880000 -05:00

 select ではなく、insert や update などをするときには DateTimeOffset 型から OracleTimeStampTZ 型に変換する拡張メソッドを使います。

var connStr = "User Id = scott; password = tiger; data source = DBHOST/ORACLE_SID";
var conn = new OracleConnection(connStr);
conn.Open();

var dt = DateTimeOffset.Parse("2020/7/28 15:00:23.1234567 -07:00");
OracleCommand cmd = new OracleCommand("update TIME set TSTZ = :tz where ID = 1", conn);

OracleTimeStampTZ tstz = dt.ToTimeStampTZ();
cmd.Parameters.Add("tz", tstz);

cmd.ExecuteNonQuery();

conn.Close();

 結果は以下です。

SQL> select ID, TSTZ from TIME where ID=1;

ID TSTZ
-- -------------------------------
 1 20-07-28 15:00:23.123457 -07:00

 面倒ですね(汗
 その点、Timestamp with local time zone 型のほうは、タイムゾーン情報は持たないので DateTime 型でアクセスでき、お手軽ではあります。もっとも Timestamp with time zone とは機能や役割が違うので比較できる話ではないのですが。

 上記は検討が浅いのでもっとよい対処法があるかもしれません。あればだれが教えてください(汗






カテゴリー: C#, Oracle, 時刻

Follow comments via the RSS Feed | Leave a comment | Trackback URL

コメントを投稿する

日本語が含まれない投稿は無視されますのでご注意ください。(スパム対策)


«   »
 
Powered by Wordpress and MySQL. Theme by Shlomi Noach, openark.org