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 とは機能や役割が違うので比較できる話ではないのですが。
上記は検討が浅いのでもっとよい対処法があるかもしれません。あればだれが教えてください(汗