Elixir Ecto: 解决UTC时间戳和本地时间8小时时差的问题

Ecto 默认使用的是UTC时间, 它要比中国区的本地时间晚 8 小时.

2016-07-21 更新: 模块已经添加到 Hex.pm, 添加 {:ecto_timestamps, "~> 1.0"} 依赖到mix.exs即可, 详细信息参考项目Ecto Timestamps 项目页面

注意, :autogenerate 需要 Ecto 2.0的支持. 所以要使用这个自定义选项, 需要升级到 Ecto 2.0

Ecto 的源码 说明了这个问题.

经过查找资料, 我们在 Ecto 文档 中找到了 timestamps 宏的选项 :autogenerate, 它的格式为一个三元组, 分别是模块, 函数, 参数

{Module, :function, []}

Ecto 的 timestamps 的时间戳是自动生成的, 同时Ecto 也给我们提供了自定义的方法, 我们参考上面的Ecto源码实现本地时间的插入

首先编写一个新的Localtime模块, 把UTC时间修改为本地时间, 定义如下, 只是替换了两个函数, 分别是:

  • :erlang.localtime替换:erlang.universaltime

  • :calendar.now_to_local_time 替换 :calendar.now_to_datetime

defmodule Test.Localtime do
  def autogenerate(precision \\ :sec)
  def autogenerate(:sec) do
    {date, {h, m, s}} = :erlang.localtime
    erl_load({date, {h, m, s, 0}})
  end
  def autogenerate(:usec) do
    timestamp = {_, _, usec} = :os.timestamp
    {date, {h, m, s}} = :calendar.now_to_local_time(timestamp)
    erl_load({date, {h, m, s, usec}})
  end
  def erl_load({{year, month, day}, {hour, min, sec, usec}}) do
    %Ecto.DateTime{
      year: year,
      month: month,
      day: day,
      hour: hour,
      min: min,
      sec: sec,
      usec: usec
    }
  end
end

在模型的模块属性中声明 @timestamps_opts 时间戳选项:

@timestamps_opts [
  autogenerate: {Test.Localtime, :autogenerate, [:sec]}
]

然后我们用一个例子来证实

模型的公共模块

require Logger
defmodule Test.Model do
  defmacro __using__(_opts) do
    quote do
      import Ecto.Query
      use Ecto.Schema
      alias Ecto.Changeset
      alias Test.Repo

      # 这里为了演示时差的问题, 先注释掉
      # @timestamps_opts [autogenerate: {Test.Localtime, :autogenerate, [:sec]}]
    end
  end
end

创建一个角色模型

defmodule Test.Model.Role do
  @moduledoc """
  角色表
  """

  use Test.Model

  schema "role" do
    field :name, :string # 角色名称
    timestamps
  end
end

创建移植脚本

➜  mix ecto.gen.migration create_role_table -r Test.Repo
* creating priv/repo/migrations
* creating priv/repo/migrations/20160713121457_create_role_table.exs

内容如下

defmodule Test.Repo.Migrations.CreateRoleTable do
  use Ecto.Migration

  def up do
    create table(:role) do
      add :name, :string # 角色名称
      timestamps
    end
  end

  def down do
    drop table(:role)
  end
end

创建表

➜ ✗ mix ecto.migrate
Compiling 14 files (.ex)
20:47:24.510 [info]  == Running Test.Repo.Migrations.CreateRoleTable.up/0 forward

20:47:24.511 [info]  create table role

20:47:24.534 [info]  == Migrated in 0.0s

角色模型修改为如下, 增加了测试函数:

defmodule Test.Model.Role do
  @moduledoc """
  角色表
  """

  use Test.Model

  schema "role" do
    field :name, :string # 角色名称
    timestamps
  end

  def insert(map) do
    Map.merge(%__MODULE__{}, map) |> Repo.insert
  end

  def test_insert do
    row = %{
      name: "技术总监"
    }
    insert(row)
  end
end

启动IEx测试, 现在的时间是2016-07-13 21:02:49, 插入的时间为2016-07-13 13:02:49, 晚了8个小时

(输出手工格式化, 以便阅读)

iex> Test.Model.Role.test_insert
QUERY OK db=6.9ms decode=1.0ms queue=0.9ms
INSERT INTO "role" ("name","inserted_at","updated_at") VALUES ($1,$2,$3) 
    RETURNING "id" ["技术总监", {{2016, 7, 13}, {13, 2, 49, 0}}, {{2016, 7, 13}, {13, 2, 49, 0}}]
{:ok, %Test.Model.Role{
    __meta__: #Ecto.Schema.Metadata<:loaded, "role">, 
    id: 6,
    inserted_at: #Ecto.DateTime<2016-07-13 13:02:49>, 
    name: "技术总监", 
    updated_at: #Ecto.DateTime<2016-07-13 13:02:49>
}}

数据库中插入的数据为

select * from role;
+------+----------+---------------------+---------------------+
|   id | name     | inserted_at         | updated_at          |
|------+----------+---------------------+---------------------|
|    6 | 技术总监 | 2016-07-13 13:02:49 | 2016-07-13 13:02:49 |
+------+----------+---------------------+---------------------+

取消上面 Test.Model 模块的属性声明的注释, 并重新编译, 进入IEx

require Logger
defmodule Test.Model do
  defmacro __using__(_opts) do
    quote do
      import Ecto.Query
      use Ecto.Schema
      alias Ecto.Changeset
      alias Test.Repo

      # 这里为了演示时差的问题, 先注释掉
      @timestamps_opts [autogenerate: {Test.Localtime, :autogenerate, [:sec]}]
    end
  end
end

再次执行

iex(18)> Test.Model.Role.test_insert    
QUERY OK db=8.5ms decode=1.0ms queue=1.1ms
INSERT INTO "role" ("name","inserted_at","updated_at") VALUES ($1,$2,$3) 
    RETURNING "id" ["技术总监", {{2016, 7, 13}, {21, 4, 58, 0}}, {{2016, 7, 13}, {21, 4, 58, 0}}]
{:ok, %Test.Model.Role{
    __meta__: #Ecto.Schema.Metadata<:loaded, "role">, 
    id: 7,
    inserted_at: #Ecto.DateTime<2016-07-13 21:04:58>, 
    name: "技术总监", 
    updated_at: #Ecto.DateTime<2016-07-13 21:04:58>
}}

数据为

select * from role;
+------+----------+---------------------+---------------------+
|   id | name     | inserted_at         | updated_at          |
|------+----------+---------------------+---------------------|
|    6 | 技术总监 | 2016-07-13 13:02:49 | 2016-07-13 13:02:49 |
|    7 | 技术总监 | 2016-07-13 21:04:58 | 2016-07-13 21:04:58 |
+------+----------+---------------------+---------------------+

你可能感兴趣的:(ecto,elixir)