转载

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

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

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 | +------+----------+---------------------+---------------------+
原文  https://segmentfault.com/a/1190000005958198
正文到此结束
Loading...