2012年9月27日木曜日

PostgreSQLの列の型のtimestampのtime zoneについて

0 コメント
PostgreSQLの列をtimestamp with time zoneで定義して、Npgsqlでアクセスした時、どんなふうになるのか気になったのでテスト。

PostgreSQLの動いている環境はこんな感じ。
$ psql --version
psql (PostgreSQL) 8.3.10
contains support for command-line editing
$ cat /etc/lsb-release
DISTRIB_ID=Ubuntu
DISTRIB_RELEASE=8.04
DISTRIB_CODENAME=hardy
DISTRIB_DESCRIPTION="Ubuntu 8.04.4 LTS"

ここにdb1というデータベースを作成して、確認用のテーブルtable1を作成します。
$ createdb db1
$ psql db1

db1=> create table table1
db1-> (
db1(>     id integer not null primary key
db1(>   , datetime1 timestamp without time zone
db1(>   , datetime2 timestamp with time zone
db1(> );
NOTICE:  CREATE TABLE / PRIMARY KEY will create implicit index "table1_pkey" for table "table1"
CREATE TABLE
db1=> insert into table1(id, datetime1, datetime2) values (1, current_timestamp, current_timestamp);
INSERT 0 1
db1=> select * from table1;
 id |         datetime1          |           datetime2
----+----------------------------+-------------------------------
  1 | 2012-09-27 11:23:28.487888 | 2012-09-27 11:23:28.487888+09
(1 row)
列のdatetime1をtimestamp without time zoneで作成し、列のdatetime2をtimestamp with time zoneで作成しています。

C#で次のコードを実行して、table1の内容を取得してみます。
using System;
using System.Collections.Generic;
using System.Text;
using System.Data;

namespace pgTimestamp
{
    class Program
    {
        static void Main(string[] args)
        {
            // データベース接続
            Npgsql.NpgsqlConnection conn = new Npgsql.NpgsqlConnection("Server=xxxx;"
                                                                     + "Port=5432;"
                                                                     + "User Id=yyyy;"
                                                                     + "Password=zzzz;"
                                                                     + "Database=db1;"
                                                                     + "Pooling=false;"
                                                                     + "Encoding=UNICODE;");
            try
            {
                conn.Open();
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.Message);
                Console.ReadKey();
                return;
            }

            // テーブルの生成
            DataTable table1 = new DataTable("table1");
            table1.Columns.Add(new DataColumn("id"       , typeof(int)     ));
            table1.Columns.Add(new DataColumn("datetime1", typeof(DateTime)));
            table1.Columns.Add(new DataColumn("datetime2", typeof(DateTime)));
            table1.PrimaryKey = new DataColumn[] { table1.Columns["id"] };

            // データの取得
            Npgsql.NpgsqlDataAdapter da = new Npgsql.NpgsqlDataAdapter();
            da.SelectCommand = new Npgsql.NpgsqlCommand("select id, datetime1, datetime2 from table1", conn);
            da.Fill(table1);

            // 取得したデータの表示
            foreach (DataRow row in table1.Rows)
            {
                Console.WriteLine("id=" + row["id"].ToString()
                              + ", datetime1=" + ((DateTime)row["datetime1"]).ToString()
                              + ", datetime2=" + ((DateTime)row["datetime2"]).ToString());
                Console.WriteLine("dateTime2(UTC)=" + ((DateTime)row["datetime2"]).ToUniversalTime().ToString());
            }
            // データベース切断
            try
            {
                conn.Close();
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.Message);
            }
            Console.ReadKey();
        }
    }
}
結果はこうなります。
id=1, datetime1=2012/09/27 11:23:28, datetime2=2012/09/27 11:23:28
dateTime2(UTC)=2012/09/27 2:23:28

ここで、サーバー側のタイムゾーンを台北に変更してみます。
$ sudo dpkg-reconfigure tzdata

Current default timezone: 'Asia/Taipei'
Local time is now:      Thu Sep 27 10:34:32 CST 2012.
Universal Time is now:  Thu Sep 27 02:34:32 UTC 2012.
PostgreSQLを再起動して、table1の内容を確認します。
sudo /etc/init.d/postgresql-8.3 restart
$ psql db1

db1=> select * from table1;
 id |         datetime1          |           datetime2
----+----------------------------+-------------------------------
  1 | 2012-09-27 11:23:28.487888 | 2012-09-27 10:23:28.487888+08
(1 row)
datetime1はタイムゾーンに関係なく入れた時のまま(タイムゾーンが東京のcurrent_timestampの値)で、datetime2は入れた時の台北の時間が表示されます。

ここで、先ほどのC#のコードを実行してみると
id=1, datetime1=2012/09/27 11:23:28, datetime2=2012/09/27 11:23:28
dateTime2(UTC)=2012/09/27 2:23:28
となります。
datetime2の値は、タイムゾーンが東京での時間になっています。

ここで、C#のコードを実行しているPCのタイムゾーンの設定を台北に変更してみます。
この状態で、コードを実行してみると
id=1, datetime1=2012/09/27 11:23:28, datetime2=2012/09/27 10:23:28
dateTime2(UTC)=2012/09/27 2:23:28
となり、datetime2は台北での時間となります。
ただ、このときToUniversalTime()で取得した時間は、どのパターンでも同じ時間となっています。

今度は、サーバー側のタイムゾーンを東京に戻してPostgreSQLを再起動します。
$ sudo dpkg-reconfigure tzdata

Current default timezone: 'Asia/Tokyo'
Local time is now:      Thu Sep 27 11:52:27 JST 2012.
Universal Time is now:  Thu Sep 27 02:52:27 UTC 2012.

$ sudo /etc/init.d/postgresql-8.3 restart
PCのタイムゾーンは台北のまま、コードを実行します。
id=1, datetime1=2012/09/27 11:23:28, datetime2=2012/09/27 10:23:28
dateTime2(UTC)=2012/09/27 2:23:28
となり、datetime2の値はサーバー側のタイムゾーンには関係なく、クライアント側のタイムゾーンで取得されています。
ここでPCのタイムゾーンも東京に戻します。


今度は、C#のコードでtable1に行を追加してみます。
次のようなコードを用意します。
using System;
using System.Collections.Generic;
using System.Text;
using System.Data;

namespace pgTimestamp
{
    class Program
    {
        static void Main(string[] args)
        {
            // データベース接続
            Npgsql.NpgsqlConnection conn = new Npgsql.NpgsqlConnection("Server=xxxx;"
                                                                     + "Port=5432;"
                                                                     + "User Id=yyyy;"
                                                                     + "Password=zzzz;"
                                                                     + "Database=db1;"
                                                                     + "Pooling=false;"
                                                                     + "Encoding=UNICODE;");
            try
            {
                conn.Open();
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.Message);
                Console.ReadKey();
                return;
            }

            // テーブルの生成
            DataTable table1 = new DataTable("table1");
            table1.Columns.Add(new DataColumn("id"       , typeof(int)     ));
            table1.Columns.Add(new DataColumn("datetime1", typeof(DateTime)));
            table1.Columns.Add(new DataColumn("datetime2", typeof(DateTime)));
            table1.PrimaryKey = new DataColumn[] { table1.Columns["id"] };

            // データの取得
            Npgsql.NpgsqlDataAdapter da = new Npgsql.NpgsqlDataAdapter();
            da.SelectCommand = new Npgsql.NpgsqlCommand("select id, datetime1, datetime2 from table1", conn);
            da.Fill(table1);

            // データの追加
            da.InsertCommand = new Npgsql.NpgsqlCommand
            (
                  "insert into table1 ("
                +      "id"
                +    ", datetime1"
                +    ", datetime2"
                + ") values ("
                +     " :id"
                +    ", :datetime1"
                +    ", :datetime2"
                + ")"
                , conn
            );
            da.InsertCommand.Parameters.Add(new Npgsql.NpgsqlParameter("id"       , NpgsqlTypes.NpgsqlDbType.Integer    , 0, "id"       , ParameterDirection.Input, false, 0, 0, DataRowVersion.Current, DBNull.Value));
            da.InsertCommand.Parameters.Add(new Npgsql.NpgsqlParameter("datetime1", NpgsqlTypes.NpgsqlDbType.Timestamp  , 0, "datetime1", ParameterDirection.Input, true , 0, 0, DataRowVersion.Current, DBNull.Value));
            da.InsertCommand.Parameters.Add(new Npgsql.NpgsqlParameter("datetime2", NpgsqlTypes.NpgsqlDbType.TimestampTZ, 0, "datetime2", ParameterDirection.Input, true , 0, 0, DataRowVersion.Current, DBNull.Value));

            DateTime value = DateTime.Parse("2012-10-01 09:00:00");
            DataRow newRow = table1.NewRow();
            newRow["id"       ] = 2;
            newRow["datetime1"] = value;
            newRow["datetime2"] = value;
            table1.Rows.Add(newRow);

            // トランザクション開始
            Npgsql.NpgsqlTransaction tran = null;
            try
            {
                tran = conn.BeginTransaction();
                da.InsertCommand.Transaction = tran;
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.Message);
                Console.ReadKey();
                return;
            }

            // 保存
            try
            {
                da.Update(table1);
            }
            catch (Exception ex)
            {
                tran.Rollback();
                Console.WriteLine(ex.Message);
                Console.ReadKey();
                return;
            }

            // コミット
            try
            {
                tran.Commit();
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.Message);
                Console.ReadKey();
                return;
            }

            // データベースからtable1の内容を取得しなおす
            table1.Rows.Clear();
            da.Fill(table1);

            // 取得したデータの表示
            foreach (DataRow row in table1.Rows)
            {
                Console.WriteLine("id=" + row["id"].ToString()
                              + ", datetime1=" + ((DateTime)row["datetime1"]).ToString()
                              + ", datetime2=" + ((DateTime)row["datetime2"]).ToString());
                Console.WriteLine("dateTime2(UTC)=" + ((DateTime)row["datetime2"]).ToUniversalTime().ToString());
            }

            // データベース切断
            try
            {
                conn.Close();
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.Message);
            }
            Console.ReadKey();
        }
    }
}
これを実行すると次のようになります。
id=1, datetime1=2012/09/27 11:23:28, datetime2=2012/09/27 11:23:28
dateTime2(UTC)=2012/09/27 2:23:28
id=2, datetime1=2012/10/01 9:00:00, datetime2=2012/10/01 9:00:00
dateTime2(UTC)=2012/10/01 0:00:00
サーバーでtable1を確認すると、
db1=> select * from table1;
 id |         datetime1          |           datetime2
----+----------------------------+-------------------------------
  1 | 2012-09-27 11:23:28.487888 | 2012-09-27 11:23:28.487888+09
  2 | 2012-10-01 09:00:00        | 2012-10-01 09:00:00+09
(2 rows)
となっており、datetime2もタイムゾーンは東京で、C#側のDateTime型で指定された時間になっています。
ここで、一旦id=2のレコードを削除します。
db1=> begin;
BEGIN
db1=> delete from table1 where id=2;
DELETE 1
db1=> commit;
COMMIT
今度は、サーバー側のタイムゾーンを台北にして実行してみます。
id=1, datetime1=2012/09/27 11:23:28, datetime2=2012/09/27 11:23:28
dateTime2(UTC)=2012/09/27 2:23:28
id=2, datetime1=2012/10/01 9:00:00, datetime2=2012/10/01 10:00:00
dateTime2(UTC)=2012/10/01 1:00:00
サーバーでtable1を確認すると、
db1=> select * from table1;
 id |         datetime1          |           datetime2
----+----------------------------+-------------------------------
  1 | 2012-09-27 11:23:28.487888 | 2012-09-27 10:23:28.487888+08
  2 | 2012-10-01 09:00:00        | 2012-10-01 09:00:00+08
(2 rows)
となり、C#側でDateTimeの時間を9:00にしてinsertすると、サーバー側には台北での9:00がinsertされ、クライアント側でその時刻を取得すると、10:00(台北時間の9:00を東京時間で表示)となります。

さらに、今度はクライアント側のタイムゾーンを台北にしてやってみると、
id=1, datetime1=2012/09/27 11:23:28, datetime2=2012/09/27 10:23:28
dateTime2(UTC)=2012/09/27 2:23:28
id=2, datetime1=2012/10/01 9:00:00, datetime2=2012/10/01 9:00:00
dateTime2(UTC)=2012/10/01 1:00:00
サーバーでtable1を確認すると、
db1=> select * from table1;
 id |         datetime1          |           datetime2
----+----------------------------+-------------------------------
  1 | 2012-09-27 11:23:28.487888 | 2012-09-27 10:23:28.487888+08
  2 | 2012-10-01 09:00:00        | 2012-10-01 09:00:00+08
(2 rows)
となり、クライアント側の結果は、クライアントとサーバーでタイムゾーンが一致しているので、insertした9:00となり、データベース側はクライアントのタイムゾーンが東京の時と同じ結果となっています。
サーバー側から見ると、クライアントのタイムゾーンがなんであっても、9:00としてinsertされたら、それはサーバー側のタイムゾーンでの時刻としてinsertされるようです。
念のため、今度はサーバー側のタイムゾーンを東京に戻して(クライアントのタイムゾーンは台北のまま)やってみると、
id=1, datetime1=2012/09/27 11:23:28, datetime2=2012/09/27 10:23:28
dateTime2(UTC)=2012/09/27 2:23:28
id=2, datetime1=2012/10/01 9:00:00, datetime2=2012/10/01 8:00:00
dateTime2(UTC)=2012/10/01 0:00:00
サーバーでtable1を確認すると、
db1=> select * from table1;
 id |         datetime1          |           datetime2
----+----------------------------+-------------------------------
  1 | 2012-09-27 11:23:28.487888 | 2012-09-27 11:23:28.487888+09
  2 | 2012-10-01 09:00:00        | 2012-10-01 09:00:00+09
(2 rows)
となります。

datetiime2の列の値を表にしてみると
タイムゾーンと結果UTC
サーバークライアント
東京9:00東京9:000:00
台北9:00東京10:001:00
台北9:00台北9:001:00
東京9:00台北8:000:00
となり、クライアントのタイムゾーンがなんであっても、サーバー側には9:00でinsertされ、それをクライアント側が取得するときは、サーバー側のタイムゾーンでの9:00をクライアント側のタイムゾーンでの時間にして取得されています。


ここで、少し気になるのが、C#のコードの中のDataAdapterのInsertCommandのNpgsqlParameterの設定で、
da.InsertCommand.Parameters.Add(new Npgsql.NpgsqlParameter("datetime2", NpgsqlTypes.NpgsqlDbType.TimestampTZ, 0, "datetime2", ParameterDirection.Input, true , 0, 0, DataRowVersion.Current, DBNull.Value));
としているところを、
da.InsertCommand.Parameters.Add(new Npgsql.NpgsqlParameter("datetime2", NpgsqlTypes.NpgsqlDbType.Timestamp, 0, "datetime2", ParameterDirection.Input, true , 0, 0, DataRowVersion.Current, DBNull.Value));
というように、NpgsqlDbTypeをTimestampTZからTimestampに変更した場合、どういう動作になるのか。
試してみると、
タイムゾーンと結果UTC
サーバークライアント
東京9:00東京9:000:00
台北9:00東京10:001:00
台北9:00台北9:001:00
東京9:00台北8:000:00
となり、全く同じ結果となりました。
NpgsqlDbType.TimestampTZとNpgsqlDbType.Timestampの使い分けがよくわからない感じですが、PostgreSQL側のテーブルの列がtimestamp with time zoneなら、NpgsqlDbType.TimestampTZを使って、timestamp without time zoneならNpgsqlDbType.Timestampを使っておけばいいのかな。

ただ、私のイメージしていた動きだと、C#のコードでDateTimeの時刻に9:00と入っていて、それをデータベースに書き込み、さらに読みなおしたときは9:00になっていて欲しい感じです。
タイムゾーンと結果UTC
サーバークライアント
東京9:00東京9:000:00
台北8:00東京9:000:00
台北9:00台北9:001:00
東京10:00台北9:001:00
テーブルの列の型がTimestamp with time zoneで、且つNpgsqlCommandのパラメータの型をNpgsqlDbType.TimestampTZにしているなら、サーバー側に書き込まれる時刻のUTCはクライアントの書き込もうとしている時刻のUTCと一致するようになるようなイメージ。そうだとクライアント側はサーバーのタイムゾーンを意識しなくてもいい気がするんだけど。。。もしかして、私が気がついていなくて考え方が間違っているのかな(^^;

追記:別のエントリで、もう少しツッコんで解決方法を考えてみました。

2012年9月19日水曜日

Windows8のODBCデータソース

0 コメント
Windows8 RTMで気がついたこと。

Windows7 64ビット版では、コントロールパネルにあるODBCデータソースを開くと、64ビット専用のものが開かれていました。32ビット版ODBCデータソースを開くときは、C:\WINDOWS\SysWOW64\Odbcad32.exeを実行する必要がありました。

Windows8では、コントロールパネルに

  • ODBC データ ソース (32 ビット)
  • ODBC データ ソース (64 ビット)

が用意され、別々に開くことができるようになっています。

また、ユーザーDSNとシステムDSNには、32ビット版で登録したものと64ビット版で登録したものの両方がリストに表示されます。
※Windows7では、32ビット版ODBCデータソースには32ビット版のDSNのみが表示され、64ビット版DSNデータソースには64ビット版のDSNのみが表示されていました。

こちらが32ビット版の画面。

こちらが64ビット版の画面。

このように、どちらの一覧にも32ビット版と64ビット版のDSNの一覧が表示されています。
※名前にモザイク入ってますけど、同じ物が表示されています(^_^;

ただし、32ビット版で追加したDSNは32ビット版のODBCデータソースでしか編集できず、同じく64ビット版で追加したDSNは64ビット版のODBCデータソースでしか編集できません。

また、DSNの名前は、32ビット版と64ビット版で同じものを使うことができます。
例えば、32ビット版でhogehogeという名前のDSNを登録しても、64ビット版でhogehogeという名前のDSNを登録することができます。

2012年9月7日金曜日

Windows8のスタートアップはデスクトップを開いた時に実行される

2 コメント
Windows8 RTMで気がついたこと。

ログインした時、自動的に実行したいプログラムをスタートアップに入れてみました。
スタートアップフォルダは、
C:\Users\ユーザー名\AppData\Roaming\Microsoft\Windows\Start Menu\Programs\Startup
にあり、ここにプログラムのショートカットを入れておきます。

ただ、Windows8の場合、Metro UI(Modern UI?)のスタート画面が開いた時にはまだ実行されず、デスクトップを開いた時に実行されるようです。

2012年9月6日木曜日

Windows8のシャットダウンは高速起動が初期設定になっている

0 コメント
Windows8 RTMで気がついたこと。

Windows8 RTMを試していますが、まだ元のWindows7の環境と行ったり来たりしないといけないので、こういうのを使ってSSDを差し替えて使っています。

2.5インチSATA内蔵リムーバブルケース(SATA接続トレイ付き) SA25-RC1-BK

Windows8からWindows7に環境を変えるとき、Windows8をシャットダウンして、SSDを入れ替えて電源を入れるんですが、そのとき画面に「Hibanationなんとかかんとか」が一瞬表示され、内蔵している別のHDDのチェックディスクが始まります。

どうやら、Windows8のシャットダウンはハイバネーションと組み合わせて起動時間を短縮するようになっているみたいです。

ということで、コントロールパネルの設定を見ると、
となっていて、下段のシャットダウン設定の中に

  • 高速スタートアップを有効にする(推奨)

という項目があり、有効になっています。
このチェックを外そうとしましたが、操作できない状態になっています。

これを変更したい時は、画面上段の
「現在利用可能ではない設定を変更します」
をクリックします。

そうすると、シャットダウン設定も変更可能になるので、
高速スタートアップを有効にする(推奨)のチェックを外して、[変更の保存]をクリックします。

これで、シャットダウン時にハイバネーションを利用しないようになります。

この設定を変更しても、SSDを使っている場合はそれほど遅くなった感じはしませんでした。
ただ、通常の使い方なら、このチェックは(推奨)とあるように、有効にしておいたほうが良いと思います。

2012年9月4日火曜日

Windows8でユーザーフォルダ名が日本語に

0 コメント
Windows8 RTMで気がついたこと。

Windows8をインストールするとき、MicrosoftアカウントでPCにサインインすると、C:\Usersの下に作られるフォルダ名が、Microsoftアカウントに登録している名前(苗字ではなく)になります。

このとき、Microsoftアカウントに名前を日本語で登録していると、日本語のフォルダ名になります。

問題はない(KOBOでは問題になっていたけど)とは思うけど、フォルダ名が日本語なのは少し気持ち悪いです。

インストール時に一旦「Microsoftアカウントでサインインしない」を選択して、英字のローカルアカウントを作成し、そのあとでMicrosoftアカウントに切り替えると、C:\Usersの下は一旦作ったローカルアカウント名のフォルダが使用されるみたいなので、正規版が出て入れなおすときはそうしようかと思っています。

2012年4月5日木曜日

DataTableのデータをDBに保存する際、エラーが発生してRollbackしたときの問題

0 コメント
ADO.NETのDataTableとDataAdapterを使って、DataTableのデータをデータベースに書き込むとき、途中でエラーが発生してロールバックすると、DataTableのDataRowの一部(更新処理がうまくいったDataRow)のRowStateがUnchangedになってしまう場合がある。

サンプルを、SQLiteを使って試してみた。

テスト用に、sample_tableという名前のテーブルを
CREATE TABLE sample_table
(
    id          INTEGER NOT NULL PRIMARY KEY
  , value       TEXT NOT NULL
  , update_date TIMESTAMP DEFAULT (DATETIME('now','localtime'))
);
上記のように作成。
ここに、
idvalueupdate_date
1ABCD2012-04-05 18:05:56
2EFGH2012-04-05 18:05:56
3IJKL2012-04-05 18:05:56
というデータを予め入れておく。
このテーブルをDataTableに取り込み、
idvalueupdate_date
1ABCDDATE('now','localtime')
2EFGHDATE('now','localtime')
3MNOPDATE('now','localtime')
4NULLDATE('now','localtime')
id=3の行のvalueを'MNOP'に書き換え、id=4の行をvalue=NULLで追加する。
※value列はNOT NULLとしているので、id=4の行の追加はエラーとなる。
DataTableのデータをデータベースに書きこむとき、id=4の行の追加でエラーが発生するのでロールバックする。
このときのDataTableのRowStateをチェックするというサンプルを用意した。
using System;
using System.Collections.Generic;
using System.Text;
using System.Data;
using System.Data.SQLite;

namespace SQLiteSample1
{
    class Program
    {
        static private SQLiteConnection m_conn;

        static void Main(string[] args)
        {
            //////////////////////////////////////////////// 準備
            // データベース接続
            string dbfile = System.IO.Path.Combine(System.IO.Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location), "dbfile.db");
            m_conn = new SQLiteConnection("Data Source=" + dbfile);
            m_conn.Open();

            // テーブルの有無を確認
            bool exists = false;
            SQLiteCommand existsCommand = new SQLiteCommand("SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='sample_table';", m_conn);
            object existsResult = existsCommand.ExecuteScalar();
            try
            {
                if (int.Parse(existsResult.ToString()) > 0)
                {
                    exists = true;
                }
            }
            catch
            {
            }

            // テーブル生成
            if (exists == false)
            {
                SQLiteCommand createCommand = new SQLiteCommand
                (
                      "CREATE TABLE sample_table ("
                    +  " id INTEGER NOT NULL PRIMARY KEY"
                    + ", value TEXT NOT NULL"
                    + ", update_date TIMESTAMP DEFAULT (DATETIME('now','localtime'))"
                    + ");"
                    , m_conn
                );
                createCommand.ExecuteNonQuery();

                // データ生成
                SQLiteCommand insertCommand;
                insertCommand = new SQLiteCommand("INSERT INTO sample_table (id, value) VALUES (1, 'ABCD');", m_conn);
                insertCommand.ExecuteNonQuery();
                insertCommand = new SQLiteCommand("INSERT INTO sample_table (id, value) VALUES (2, 'EFGH');", m_conn);
                insertCommand.ExecuteNonQuery();
                insertCommand = new SQLiteCommand("INSERT INTO sample_table (id, value) VALUES (3, 'IJKL');", m_conn);
                insertCommand.ExecuteNonQuery();
            }

            // データベース切断
            m_conn.Close();


            //////////////////////////////////////////////// テスト
            // データベース接続
            m_conn.Open();

            // データテーブル生成
            DataTable table = new DataTable();
            table.Columns.Add(new DataColumn("id"   , typeof(int)   ));
            table.Columns.Add(new DataColumn("value", typeof(string)));
            table.PrimaryKey = new DataColumn[] { table.Columns["id"] };

            // DataAdapter生成
            SQLiteDataAdapter da = new SQLiteDataAdapter();
            // SelectCommand
            da.SelectCommand = new SQLiteCommand("SELECT id, value, update_date FROM sample_table", m_conn);
            // InsertCommand
            da.InsertCommand = new SQLiteCommand("INSERT INTO sample_table(id, value) VALUES (@id, @value);", m_conn);
            da.InsertCommand.Parameters.Add(new SQLiteParameter("id"   , DbType.Int32 , 0, ParameterDirection.Input, false, 0, 0, "id"   , DataRowVersion.Current, DBNull.Value));
            da.InsertCommand.Parameters.Add(new SQLiteParameter("value", DbType.String, 0, ParameterDirection.Input, false, 0, 0, "value", DataRowVersion.Current, DBNull.Value));
            // UpdateCommand
            da.UpdateCommand = new SQLiteCommand("UPDATE sample_table SET value=@value, update_date=(DATETIME('now','localtime')) WHERE id=@id;", m_conn);
            da.UpdateCommand.Parameters.Add(new SQLiteParameter("value", DbType.String, 0, ParameterDirection.Input, false, 0, 0, "value", DataRowVersion.Current , DBNull.Value));
            da.UpdateCommand.Parameters.Add(new SQLiteParameter("id"   , DbType.Int32 , 0, ParameterDirection.Input, false, 0, 0, "id"   , DataRowVersion.Original, DBNull.Value));
            // DeleteCommand
            da.DeleteCommand = new SQLiteCommand("DELETE FROM sample_table WHERE id=@id;", m_conn);
            da.DeleteCommand.Parameters.Add(new SQLiteParameter("id"   , DbType.Int32 , 0, ParameterDirection.Input, false, 0, 0, "id"   , DataRowVersion.Original, DBNull.Value));
            // RowUpdated
            da.RowUpdated += new EventHandler<System.Data.Common.RowUpdatedEventArgs>(da_RowUpdated);

            // Fill
            da.Fill(table);
            
            // データ変更
            DataRow updateRow = table.Rows.Find(3);
            updateRow["value"] = "MNOP";
            // value列がNULLの行を追加...Updateでエラーになる
            DataRow newRow = table.NewRow();
            newRow["id"   ] = 4;
            newRow["value"] = DBNull.Value;
            table.Rows.Add(newRow);

            Console.WriteLine("保存前");
            foreach (DataRow row in table.Rows)
            {
                Console.WriteLine(row["id"].ToString() + " : " + row.RowState.ToString());
            }

            // 保存
            SQLiteTransaction tran = m_conn.BeginTransaction();
            try
            {
                da.Update(table);
                tran.Commit();
                Console.WriteLine("保存成功");
            }
            catch (Exception ex)
            {
                tran.Rollback();
                Console.WriteLine("保存失敗");
                Console.WriteLine(ex.Message);
            }
            Console.WriteLine("保存後");
            foreach (DataRow row in table.Rows)
            {
                Console.WriteLine(row["id"].ToString() + " : " + row.RowState.ToString());
            }

            // id=4のvalueをセットして再度保存
            DataRow row4 = table.Rows.Find(4);
            row4["value"] = "QRST";
            tran = m_conn.BeginTransaction();
            try
            {
                da.Update(table);
                tran.Commit();
                Console.WriteLine("保存成功");
            }
            catch (Exception ex)
            {
                tran.Rollback();
                Console.WriteLine("保存失敗");
                Console.WriteLine(ex.Message);
            }
            Console.WriteLine("保存後");
            foreach (DataRow row in table.Rows)
            {
                Console.WriteLine(row["id"].ToString() + " : " + row.RowState.ToString());
            }
            Console.ReadKey();

            // データベース切断
            m_conn.Close();
        }

        static void da_RowUpdated(object sender, System.Data.Common.RowUpdatedEventArgs e)
        {
            if (e.Status == UpdateStatus.Continue)
            {
                if ((e.StatementType == StatementType.Insert) || (e.StatementType == StatementType.Update))
                {
                    // update_dateの取得
                    SQLiteCommand cmd = new SQLiteCommand("SELECT update_date FROM sample_table WHERE id=@id", m_conn);
                    SQLiteParameter param = new SQLiteParameter("id", DbType.Int32, 0, "id", DataRowVersion.Original);
                    param.Value = e.Row["id"];
                    cmd.Parameters.Add(param);
                    try
                    {
                        e.Row["update_date"] = cmd.ExecuteScalar();
                        e.Row.AcceptChanges();
                    }
                    catch
                    {
                        throw;
                    }
                }
            }
        }
    }
}
これを実行すると
保存前
1 : Unchanged
2 : Unchanged
3 : Modified
4 : Added
保存失敗
Abort due to constraint violation
sample_table.value may not be NULL
保存後
1 : Unchanged
2 : Unchanged
3 : Unchanged
4 : Added
保存成功
保存後
1 : Unchanged
2 : Unchanged
3 : Unchanged
4 : Unchanged
となり、この時のデータベースのテーブルの内容は、
最初のda.Update(table)(失敗する)のあとでは
idvalueupdate_date
1ABCD2012-04-05 18:05:56
2EFGH2012-04-05 18:05:56
3IJKL2012-04-05 18:05:56
と、ロールバックしているので当然初期の状態と何も変わらない。
DataTableの内容は
idvalueupdate_dateRowState
1ABCD2012-04-05 18:05:56Unchanged
2EFGH2012-04-05 18:05:56Unchanged
3MNOP2012-04-05 18:05:56Unchanged
4QRSTNULLAdded
と、DataTableのid=3のDataRowのRowStateはUnchangedになり、エラーの原因となるid=4のDataRowのRowStateはAddedのままとなっている。
次に、id=4のvalueに文字列をセットした後の2回目のda.Update(table)のあとのデータベースのテーブルは
idvalueupdate_date
1ABCD2012-04-05 18:05:56
2EFGH2012-04-05 18:05:56
3IJKL2012-04-05 18:05:56
4QRST2012-04-05 18:07:27
となっている。
このとき、id=4のDataRowが保存されるので、このDataRowのRowStateがUnchangedに変わり、すべての行がUnchangedとなる。
このとき、DataTableの中身は
idvalueupdate_dateRowState
1ABCD2012-04-05 18:05:56Unchanged
2EFGH2012-04-05 18:05:56Unchanged
3MNOP2012-04-05 18:05:56Unchanged
4QRST2012-04-05 18:07:27Unchanged
となっていて、実際のデータベース上のテーブルと食い違いが発生している。

このサンプルプログラムでは、2回続けて保存したが、例えばこのDataTableがDataGridViewで編集されるようなFormアプリケーションだった場合、エラー発生後に利用者がid=4のvalueに値をセットして、再度保存処理をやっても、id=3の行のvalueはデータベースには反映されない。
※しかも、Form上でid=3のvalueは修正した内容になっているため、利用者側にはid=3のvalueもデータベースに正しく書きこまれたかのように見えてしまう。

データベースへの保存の際、エラーが発生してロールバックするとき、DataTableの各行のRowStateも元の状態に戻せれば問題を解決できる。
方法として、DataTableの変更した行のみを取り出して、別のDataTableを生成し、そのDataTableを使って保存処理を行う。
保存が失敗した場合、元のDataTableの各行のRowStateは何も影響を受けていないため、保存処理前の状態になっている。
保存が成功した場合、コピーしたDataTableの内容と各行のRowStateを元のテーブルに反映させる。

具体的には、元のソースの
            // 保存
            SQLiteTransaction tran = m_conn.BeginTransaction();
            try
            {
                da.Update(table);
                tran.Commit();
                Console.WriteLine("保存成功");
            }
上記部分(2ヶ所)を、
            // 保存
            SQLiteTransaction tran = m_conn.BeginTransaction();
            try
            {
                DataTable tempTable = table.GetChanges();
                da.Update(tempTable);
                tran.Commit();
                Console.WriteLine("保存成功");
                table.Merge(tempTable);
                foreach (DataRow row in tempTable.Rows)
                {
                    if (row.RowState == DataRowState.Unchanged)
                    {
                        DataRow orgRow = table.Rows.Find(row["id"]);
                        if (orgRow != null)
                        {
                            orgRow.AcceptChanges();
                        }
                    }
                }
            }
のように修正する。

DataTable tempTable = table.GetChanges();で、元DataTableの変更された行のみをコピーしたDataTableを生成する。
データベースへの保存もこのコピーしたDataTableを使ってda.Update(tempTable);とする。
保存が成功した場合、table.Merge(tempTable);でコピーしたDataTableの内容を元のDataTableに取り込む。
ただし、まだ元のDataTableの各行のRowStateはModifiedやAddedのままなので、コピーしたDataTableの行に対応する元のDataTableのDataRowを見つけ、AcceptChanges()で、Unchangedにする。
※このサンプルならtable.AcceptChanged();でも良い。

変更したプログラムを実行すると
保存前
1 : Unchanged
2 : Unchanged
3 : Modified
4 : Added
保存失敗
Abort due to constraint violation
sample_table.value may not be NULL
保存後
1 : Unchanged
2 : Unchanged
3 : Modified
4 : Added
保存成功
保存後
1 : Unchanged
2 : Unchanged
3 : Unchanged
4 : Unchanged
となり、最初の保存で失敗したあとのロールバック後もDataTableの各行のRowStateは元のままになっている。
この時のデータベースのテーブルの内容は、
最初のda.Update(table)(失敗する)のあとでは
idvalueupdate_date
1ABCD2012-04-05 18:28:23
2EFGH2012-04-05 18:28:23
3IJKL2012-04-05 18:28:23
と、ロールバックしているので当然初期の状態と何も変わらない。
DataTableの内容は
idvalueupdate_dateRowState
1ABCD2012-04-05 18:05:56Unchanged
2EFGH2012-04-05 18:05:56Unchanged
3MNOP2012-04-05 18:05:56Modified
4QRSTNULLAdded
となっている。
次に、id=4のvalueに文字列をセットした後の2回目のda.Update(table)のあとのデータベースのテーブルは
idvalueupdate_date
1ABCD2012-04-05 18:28:23
2EFGH2012-04-05 18:28:23
3MNOP2012-04-05 18:28:27
4QRST2012-04-05 18:28:27
となっている。
このとき、DataTableの中身は
idvalueupdate_dateRowState
1ABCD2012-04-05 18:28:23Unchanged
2EFGH2012-04-05 18:28:23Unchanged
3MNOP2012-04-05 18:28:27Unchanged
4QRST2012-04-05 18:28:27Unchanged
となっていて、実際のデータベース上のテーブルと一致している。


修正版のソースを載せておきます。
using System;
using System.Collections.Generic;
using System.Text;
using System.Data;
using System.Data.SQLite;

namespace SQLiteSample1
{
    class Program
    {
        static private SQLiteConnection m_conn;

        static void Main(string[] args)
        {
            //////////////////////////////////////////////// 準備
            // データベース接続
            string dbfile = System.IO.Path.Combine(System.IO.Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location), "dbfile.db");
            m_conn = new SQLiteConnection("Data Source=" + dbfile);
            m_conn.Open();

            // テーブルの有無を確認
            bool exists = false;
            SQLiteCommand existsCommand = new SQLiteCommand("SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='sample_table';", m_conn);
            object existsResult = existsCommand.ExecuteScalar();
            try
            {
                if (int.Parse(existsResult.ToString()) > 0)
                {
                    exists = true;
                }
            }
            catch
            {
            }

            // テーブル生成
            if (exists == false)
            {
                SQLiteCommand createCommand = new SQLiteCommand
                (
                      "CREATE TABLE sample_table ("
                    +  " id INTEGER NOT NULL PRIMARY KEY"
                    + ", value TEXT NOT NULL"
                    + ", update_date TIMESTAMP DEFAULT (DATETIME('now','localtime'))"
                    + ");"
                    , m_conn
                );
                createCommand.ExecuteNonQuery();

                // データ生成
                SQLiteCommand insertCommand;
                insertCommand = new SQLiteCommand("INSERT INTO sample_table (id, value) VALUES (1, 'ABCD');", m_conn);
                insertCommand.ExecuteNonQuery();
                insertCommand = new SQLiteCommand("INSERT INTO sample_table (id, value) VALUES (2, 'EFGH');", m_conn);
                insertCommand.ExecuteNonQuery();
                insertCommand = new SQLiteCommand("INSERT INTO sample_table (id, value) VALUES (3, 'IJKL');", m_conn);
                insertCommand.ExecuteNonQuery();
            }

            // データベース切断
            m_conn.Close();


            //////////////////////////////////////////////// テスト
            // データベース接続
            m_conn.Open();

            // データテーブル生成
            DataTable table = new DataTable();
            table.Columns.Add(new DataColumn("id"   , typeof(int)   ));
            table.Columns.Add(new DataColumn("value", typeof(string)));
            table.PrimaryKey = new DataColumn[] { table.Columns["id"] };

            // DataAdapter生成
            SQLiteDataAdapter da = new SQLiteDataAdapter();
            // SelectCommand
            da.SelectCommand = new SQLiteCommand("SELECT id, value, update_date FROM sample_table", m_conn);
            // InsertCommand
            da.InsertCommand = new SQLiteCommand("INSERT INTO sample_table(id, value) VALUES (@id, @value);", m_conn);
            da.InsertCommand.Parameters.Add(new SQLiteParameter("id"   , DbType.Int32 , 0, ParameterDirection.Input, false, 0, 0, "id"   , DataRowVersion.Current, DBNull.Value));
            da.InsertCommand.Parameters.Add(new SQLiteParameter("value", DbType.String, 0, ParameterDirection.Input, false, 0, 0, "value", DataRowVersion.Current, DBNull.Value));
            // UpdateCommand
            da.UpdateCommand = new SQLiteCommand("UPDATE sample_table SET value=@value, update_date=(DATETIME('now','localtime')) WHERE id=@id;", m_conn);
            da.UpdateCommand.Parameters.Add(new SQLiteParameter("value", DbType.String, 0, ParameterDirection.Input, false, 0, 0, "value", DataRowVersion.Current , DBNull.Value));
            da.UpdateCommand.Parameters.Add(new SQLiteParameter("id"   , DbType.Int32 , 0, ParameterDirection.Input, false, 0, 0, "id"   , DataRowVersion.Original, DBNull.Value));
            // DeleteCommand
            da.DeleteCommand = new SQLiteCommand("DELETE FROM sample_table WHERE id=@id;", m_conn);
            da.DeleteCommand.Parameters.Add(new SQLiteParameter("id"   , DbType.Int32 , 0, ParameterDirection.Input, false, 0, 0, "id"   , DataRowVersion.Original, DBNull.Value));
            // RowUpdated
            da.RowUpdated += new EventHandler<System.Data.Common.RowUpdatedEventArgs>(da_RowUpdated);

            // Fill
            da.Fill(table);
            
            // データ変更
            DataRow updateRow = table.Rows.Find(3);
            updateRow["value"] = "MNOP";
            // value列がNULLの行を追加...Updateでエラーになる
            DataRow newRow = table.NewRow();
            newRow["id"   ] = 4;
            newRow["value"] = DBNull.Value;
            table.Rows.Add(newRow);

            Console.WriteLine("保存前");
            foreach (DataRow row in table.Rows)
            {
                Console.WriteLine(row["id"].ToString() + " : " + row.RowState.ToString());
            }

            // 保存
            SQLiteTransaction tran = m_conn.BeginTransaction();
            try
            {
                DataTable tempTable = table.GetChanges();
                da.Update(tempTable);
                tran.Commit();
                Console.WriteLine("保存成功");
                table.Merge(tempTable);
                foreach (DataRow row in tempTable.Rows)
                {
                    if (row.RowState == DataRowState.Unchanged)
                    {
                        DataRow orgRow = table.Rows.Find(row["id"]);
                        if (orgRow != null)
                        {
                            orgRow.AcceptChanges();
                        }
                    }
                }
            }
            catch (Exception ex)
            {
                tran.Rollback();
                Console.WriteLine("保存失敗");
                Console.WriteLine(ex.Message);
            }
            Console.WriteLine("保存後");
            foreach (DataRow row in table.Rows)
            {
                Console.WriteLine(row["id"].ToString() + " : " + row.RowState.ToString());
            }

            // id=4のvalueをセットして再度保存
            DataRow row4 = table.Rows.Find(4);
            row4["value"] = "QRST";
            tran = m_conn.BeginTransaction();
            try
            {
                DataTable tempTable = table.GetChanges();
                da.Update(tempTable);
                tran.Commit();
                Console.WriteLine("保存成功");
                table.Merge(tempTable);
                foreach (DataRow row in tempTable.Rows)
                {
                    if (row.RowState == DataRowState.Unchanged)
                    {
                        DataRow orgRow = table.Rows.Find(row["id"]);
                        if (orgRow != null)
                        {
                            orgRow.AcceptChanges();
                        }
                    }
                }
            }
            catch (Exception ex)
            {
                tran.Rollback();
                Console.WriteLine("保存失敗");
                Console.WriteLine(ex.Message);
            }
            Console.WriteLine("保存後");
            foreach (DataRow row in table.Rows)
            {
                Console.WriteLine(row["id"].ToString() + " : " + row.RowState.ToString());
            }
            Console.ReadKey();

            // データベース切断
            m_conn.Close();
        }

        static void da_RowUpdated(object sender, System.Data.Common.RowUpdatedEventArgs e)
        {
            if (e.Status == UpdateStatus.Continue)
            {
                if ((e.StatementType == StatementType.Insert) || (e.StatementType == StatementType.Update))
                {
                    // update_dateの取得
                    SQLiteCommand cmd = new SQLiteCommand("SELECT update_date FROM sample_table WHERE id=@id", m_conn);
                    SQLiteParameter param = new SQLiteParameter("id", DbType.Int32, 0, "id", DataRowVersion.Original);
                    param.Value = e.Row["id"];
                    cmd.Parameters.Add(param);
                    try
                    {
                        e.Row["update_date"] = cmd.ExecuteScalar();
                        e.Row.AcceptChanges();
                    }
                    catch
                    {
                        throw;
                    }
                }
            }
        }
    }
}

SQLiteでTIMESTAMP列のデフォルト値のタイムゾーンをJSTにする

0 コメント
SQLite3で次のようなテーブルを作った。
CREATE TABLE sample_table
(
    id          INTEGER   NOT NULL PRIMARY KEY
  , value       TEXT,
  , update_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
すると、update_dateにはタイムゾーンがUTCで日時がセットされてしまう。

調べてみると、DATETIME('now','localtime')とすると、タイムゾーンがJSTで日時が取れるということがわかった。
そこで、早速上記SQLを
CREATE TABLE sample_table
(
    id          INTEGER   NOT NULL PRIMARY KEY
  , value       TEXT,
  , update_date TIMESTAMP DEFAULT DATETIME('now','localtime')
);
としてみた。 しかし、
SQLite error
near "(": syntax error
というエラーが発生。

色々試してみて、DATETIME('now','localtime')を括弧で囲めばいいことがわかった。
最終的には、次のようなSQLとなった。
CREATE TABLE sample_table
(
    id          INTEGER   NOT NULL PRIMARY KEY
  , value       TEXT,
  , update_date TIMESTAMP DEFAULT (DATETIME('now','localtime'))
);