質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

ただいまの
回答率

88.06%

PostgreSQL + Ecto + Timex でタイムゾーン付き日時情報の扱い方

解決済

回答 3

投稿 編集

  • 評価
  • クリップ 1
  • VIEW 2,825

score 37

前提・実現したいこと

Ecto + Timex を使って、PostgreSQL の timestamp with timezone を扱いたいんですが、Model で持っている日時とデータベースに保存される時間がずれてしまうので、理由と対処法を知りたいです。

もっと別の方法があるよ、とかでもいいです。

参考

  • https://github.com/bitwalker/timex_ecto
  • https://hexdocs.pm/timex_ecto/Timex.Ecto.DateTimeWithTimezone.html

↑を読むと、カスタム複合タイプ datetimetz つくって対処するといいよって書いてある。

発生している問題・エラーメッセージ

インタラクションに従って、データベースに datetimetz 型をつくって、Elixir からはTimex.Ecto.DateTimeWithTimezone 型の変数を入れられるようにしました。

CREATE TYPE datetimetz AS (
    dt timestamptz,
    tz varchar
);

Timex.Ecto.DateTimeWithTimezone な変数の中身をダンプすると以下のようになっています。

IO.inspect model.inserted_at
# {{{2017, 6, 2}, {11, 41, 57, 93698}}, "Asia/Tokyo"}

データベースには以下のように登録されています。

("2017-06-02 20:41:57.093698+09",Asia/Tokyo)

9時間ずれている。。実際の時刻は上の方です(今日の午前11時に実行しました)。

該当のソースコード

migrationファイル

  def change do
    create table(:messages) do
      add :message, :text
      add :inserted_at, :datetimetz
    end
  end

モデルクラス

schema "messages" do
  field :message, :string
  field :inserted_at, Timex.Ecto.DateTimeWithTimezone
end

インサート

changeset = Message.changeset(%Message{}, %{message: message, inserted_at: Timex.now("Asia/Tokyo")})
Repo.insert(changeset)

試したこと

すみません、何を試せばいいのかすら思いつかず。。

Ectoのコード読んだくらいです(でもよく分からなかった。。)

2017-06-04 13:00 追記

@mhashi さんのコメントにもあるように、インサートの際に UTC で入れないとダメなようです。
そもそも PostgreSQL の timestamp with timezone はタイムスタンプを UTC で持っており、それを、必要に応じて変換してから出力しているみたいなので、そこに JST の日時を入れてしまうと二重に +9 されてしまうようです。

たどり着いたコードとしては、
Ecto.Adapters.Postgres.Timestamp.encode/1 の中で

:calendar.datetime_to_gregorian_seconds({{2017,6,4},{13,0,0}})

のようにして秒に変換しているのですが、当然ながらここではタイムゾーンは考慮されてないです。このタプルは、単純に Timex.Ecto.DateTimeWithTimezone の値 {{{2017,6,4},{13,0,0}}, "Asia/Tokyo"} のタイムスタンプ部分を取ってきているにすぎません。

2017-06-04 13:00 追記ここまで

補足情報(言語/FW/ツール等のバージョンなど)

Elixir 1.4
ecto 2.1
timex 3.1
timex_ecto 3.1

  • 気になる質問をクリップする

    クリップした質問は、後からいつでもマイページで確認できます。

    またクリップした質問に回答があった際、通知やメールを受け取ることができます。

    クリップを取り消します

  • 良い質問の評価を上げる

    以下のような質問は評価を上げましょう

    • 質問内容が明確
    • 自分も答えを知りたい
    • 質問者以外のユーザにも役立つ

    評価が高い質問は、TOPページの「注目」タブのフィードに表示されやすくなります。

    質問の評価を上げたことを取り消します

  • 評価を下げられる数の上限に達しました

    評価を下げることができません

    • 1日5回まで評価を下げられます
    • 1日に1ユーザに対して2回まで評価を下げられます

    質問の評価を下げる

    teratailでは下記のような質問を「具体的に困っていることがない質問」、「サイトポリシーに違反する質問」と定義し、推奨していません。

    • プログラミングに関係のない質問
    • やってほしいことだけを記載した丸投げの質問
    • 問題・課題が含まれていない質問
    • 意図的に内容が抹消された質問
    • 過去に投稿した質問と同じ内容の質問
    • 広告と受け取られるような投稿

    評価が下がると、TOPページの「アクティブ」「注目」タブのフィードに表示されにくくなります。

    質問の評価を下げたことを取り消します

    この機能は開放されていません

    評価を下げる条件を満たしてません

    評価を下げる理由を選択してください

    詳細な説明はこちら

    上記に当てはまらず、質問内容が明確になっていない質問には「情報の追加・修正依頼」機能からコメントをしてください。

    質問の評価を下げる機能の利用条件

    この機能を利用するためには、以下の事項を行う必要があります。

回答 3

checkベストアンサー

+1

timestamp with time zoneのカラムにUTCではなくローカルタイムを入れているため、
日本時刻(UTC+9)に二重に+9されているのだと思います。

("2017-06-02 20:41:57.093698+09",Asia/Tokyo)
これの時刻の末尾+9なのでUTC 11:41...で登録されてますね。

without time zoneでカラムを作るか、 Timex.now("Asia/Tokyo")を Timex.nowとすれば要求を満たせるかもしれません。

投稿

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2017/06/02 14:41

    @mhashi さん、回答ありがとうございます。
    カラムを timestamp with timezone にして、UTC でインサートしたらうまくいきました。

    ```elixir
    INSERT INTO "test" ("ts") VALUES ($1) RETURNING "id" [{{2017, 6, 2}, {5, 39, 48, 589422}}]
    {:ok,
    %SlackBot.Test{__meta__: #Ecto.Schema.Metadata<:loaded, "test">, id: 2,
    ts: #<DateTime(2017-06-02T05:39:48.589422Z Etc/UTC)>}}
    ```

    キャンセル

  • 2017/06/02 15:15

    しかし、なんとなくもやもやはしていて、アプリケーション内でも DB 内でも JST で日時を扱いたいんですが、Repo.insert のときだけ、UTC に戻してやらないといけないの?っていう素朴な疑問が残ります。

    しかも、Timex.Ecto が推奨している DateTimeWithTimezone はタプルで表現すると、タイムスタンプとタイムゾーン名を持つタプルになるので、タイムスタンプには UTC の値が入り、タイムゾーン名には "Asia/Tokyo" と入っているっていう状態になってしまい、ちょっと気持ち悪いなぁ、と。

    ```
    iex(11)> Timex.now("Asia/Tokyo")
    #<DateTime(2017-06-02T15:10:51.979615+09:00 Asia/Tokyo)>
    iex(12)> Timex.now("Asia/Tokyo") |> Timex.Ecto.DateTimeWithTimezone.dump
    {:ok, {{{2017, 6, 2}, {15, 11, 19, 490587}}, "Asia/Tokyo"}}
    ```

    最終的には Timex.Ecto.DateTimeWithTimezone を使わないでやるしかなさそうですね。

    キャンセル

+1

Elixir
{{{2017, 6, 2}, {11, 41, 57, 93698}}, "Asia/Tokyo"}

SQL
("2017-06-02 20:41:57.093698+09",Asia/Tokyo)

これらはズレていなく、一致しているかと思います。
SQLの方の

20:41:57.093698+09

の"+09"の部分は標準時間と比べて日本は9時間早いということを表しているので、データとしては

20:41:57.093698+09 = 11:41:57.093698

で、Modelの方と一致していると思います。
以下のサイトは参考になるかもしれません。
http://qiita.com/kidatti/items/272eb962b5e6025fc51e

投稿

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2017/06/02 14:54

    @Naoki_Tsuchi さん、回答ありがとうございます。

    すみません、一致しているとのことですが、ちょっとピンとこないです。

    試しに直接データベース操作して確認してみました。

    ```sql
    testdb=# insert into test values(current_timestamp);
    INSERT 0 1
    testdb=# select * from test;
    ts
    -------------------------------
    2017-06-02 14:29:14.588632+09
    (1 row)
    ```

    iex で確認すると、

    ```elixir
    iex(1)> Timex.now()
    #<DateTime(2017-06-02T05:48:05.476850Z Etc/UTC)>
    iex(2)> Timex.now("Asia/Tokyo")
    #<DateTime(2017-06-02T14:48:18.877824+09:00 Asia/Tokyo)>
    ```

    UTC でインサートすると、 `2017-06-02 14:39:48.589422+09` と登録されたので、@mhashi さんからのコメントにもあるように、データ型は timestamp with timezone でつくりつつ、Elixir からは UTC 時間で入れるようにしないとダメってことでしょうか?(だとすると、Timex.Ecto.DateTimeWithTimezone の意味は。。

    キャンセル

+1

私はElixirで開発をしてないので間違っているかもしれませんが、こちらに Timex.Ecto.DateTimeWithTimezone の使い方についての詳しい説明があるのを見つけましたので、それに基づいて回答します。

まず、DateTimeWithTimezoneは、PostgreSQLのtimestamptz (timestamp with timezone) にデータを格納することはできないようです。

上記のドキュメントに書かれている使い方を説明します。

この機能は現状PostgreSQLのみで使用できる。

対象のデータベースで必ず以下のSQLを実行しなければならない。これにより datetimetz 型が作られる。

CREATE TYPE datetimetz AS (
    dt timestamptz,
    tz varchar
);

あとは detatimetz 型を使ってテーブルを定義すればよい。

CREATE TABLE example (
  id integer,
  created_at datetimetz
);

以上です。試してみてください。

投稿

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2017/06/02 17:01

    mix.lock を確認したところ、最新版でした(timex 3.1.15、timex_ecto 3.1.1)。
    パースエラーなどは起こっておらず、insert は正常に行われるのですが、データを見ると、+9時間ずれているという現象です。
    Timex -> Ecto へデータを渡すときに値が変わっていないか、デバッグしてみます。

    キャンセル

  • 2017/06/02 19:18

    バージョンについて承知しました。すぐには無理なのですが、時間ができたら私も試してみようと思います。

    キャンセル

  • 2017/06/02 19:19

    すみません、ありがとうございます!

    キャンセル

15分調べてもわからないことは、teratailで質問しよう!

  • ただいまの回答率 88.06%
  • 質問をまとめることで、思考を整理して素早く解決
  • テンプレート機能で、簡単に質問をまとめられる

関連した質問

同じタグがついた質問を見る