Ecto 不会将 :date 字段插入数据库——为什么?

Ecto will not insert a :date field into the database - why?

我在学习 Elixir 和 Ecto 时遇到问题。这个想法是建立一个标准的 posts/comments 页面来了解基础知识是如何工作的。我正处于定义模式、编写迁移并尝试通过 Repo 将数据插入数据库 (PostgreSQL) 时遇到错误的地步。我进行了大量的网络搜索和文档阅读,这让我相信这是一个应该可行的场景,但我在某个地方犯了一个我看不到的愚蠢错误。

定义如下:

lib/hello/schemas.ex

defmodule Hello.PostAuthor do
    use Ecto.Schema

    schema "post_authors" do
        field :name, :string
    end
end

defmodule Hello.CommentAuthor do
    use Ecto.Schema

    schema "comment_authors" do
        field :name, :string
    end
end

defmodule Hello.Comment do
    use Ecto.Schema

    schema "comments" do
        has_one :author, Hello.CommentAuthor
        field :date, :date
        field :body, :string
    end
end

defmodule Hello.Post do
    use Ecto.Schema

    schema "posts" do
        has_one :author, Hello.PostAuthor
        field :date, :date
        field :body, :string
        has_many :comments, Hello.Comment
    end
end

如您所见,我有两个 :date 类型的字段 - on post 和注释模式。对应的迁移如下:

defmodule Hello.Repo.Migrations.CreatePosts do
  use Ecto.Migration

  def change do
    create table(:post_authors) do
      add :name, :string
    end

    create table(:comment_authors) do
      add :name, :string
    end

    create table(:comments) do
      add :author, references(:comment_authors)
      add :date, :date
      add :body, :string
    end

    create table(:posts) do
      add :author, references(:post_authors), null: false
      add :date, :date
      add :body, :string
      add :comments, references(:comments)

      timestamps()
    end
  end
end

现在,当我开始 iex -S mix 我可以成功创建所有结构:

iex(1)> post_author = %Hello.PostAuthor{name: "John"} 
%Hello.PostAuthor{
  __meta__: #Ecto.Schema.Metadata<:built, "post_authors">,
  id: nil,
  name: "John"
}

iex(2)> comment_author = %Hello.PostAuthor{name: "Adam"}
%Hello.PostAuthor{
  __meta__: #Ecto.Schema.Metadata<:built, "post_authors">,
  id: nil,
  name: "Adam"
}


iex(3)> comment = %Hello.Comment{author: comment_author, date: ~D[2019-01-01], body: "this is a comment"}
%Hello.Comment{
  __meta__: #Ecto.Schema.Metadata<:built, "comments">,
  author: %Hello.PostAuthor{
    __meta__: #Ecto.Schema.Metadata<:built, "post_authors">,
    id: nil,
    name: "Adam"
  },
  body: "this is a comment",
  date: ~D[2019-01-01],
  id: nil
}

iex(4)> post = %Hello.Post{author: post_author, date: ~D[2019-01-01], body: "this is a post", comments: [comment]}
%Hello.Post{
  __meta__: #Ecto.Schema.Metadata<:built, "posts">,
  author: %Hello.PostAuthor{
    __meta__: #Ecto.Schema.Metadata<:built, "post_authors">,
    id: nil,
    name: "John"
  },
  body: "this is a post",
  comments: [%Hello.Comment{
    __meta__: #Ecto.Schema.Metadata<:built, "comments">,
    author: %Hello.PostAuthor{
      __meta__: #Ecto.Schema.Metadata<:built, "post_authors">,
      id: nil,
      name: "Adam"
    },
    body: "this is a comment",
    date: ~D[2019-01-01],
    id: nil
  }],
  date: ~D[2019-01-01],
  id: nil
}

当我调用 Hello.Repo.insert(post) 时出现问题(其中 post 是表示 Hello.Post 架构的结构)。我收到看起来像序列化错误的内容:

iex(8)> Hello.Repo.insert(post)                                                                     [debug] QUERY OK db=0.1ms
begin []
[debug] QUERY ERROR db=1.6ms
INSERT INTO "posts" ("body","date") VALUES (,) RETURNING "id" ["this is a post", ~D[2019-01-01]]
[debug] QUERY OK db=0.1ms
rollback []
** (DBConnection.EncodeError) Postgrex expected a binary, got ~D[2019-01-01]. Please make sure the value you are passing matches the definition in your table or in your query or convert the value accordingly.
    (postgrex) lib/postgrex/type_module.ex:897: Postgrex.DefaultTypes.encode_params/3
    (postgrex) lib/postgrex/query.ex:75: DBConnection.Query.Postgrex.Query.encode/3
    (db_connection) lib/db_connection.ex:1148: DBConnection.encode/5
    (db_connection) lib/db_connection.ex:1246: DBConnection.run_prepare_execute/5
    (db_connection) lib/db_connection.ex:540: DBConnection.parsed_prepare_execute/5
    (db_connection) lib/db_connection.ex:533: DBConnection.prepare_execute/4
    (postgrex) lib/postgrex.ex:198: Postgrex.query/4
    (ecto_sql) lib/ecto/adapters/sql.ex:666: Ecto.Adapters.SQL.struct/10
    (ecto) lib/ecto/repo/schema.ex:651: Ecto.Repo.Schema.apply/4
    (ecto) lib/ecto/repo/schema.ex:262: anonymous fn/15 in Ecto.Repo.Schema.do_insert/4
    (ecto) lib/ecto/repo/schema.ex:916: anonymous fn/3 in Ecto.Repo.Schema.wrap_in_transaction/6
    (ecto_sql) lib/ecto/adapters/sql.ex:898: anonymous fn/3 in Ecto.Adapters.SQL.checkout_or_transaction/4
    (db_connection) lib/db_connection.ex:1415: DBConnection.run_transaction/4

这是我迷路的地方。模式和迁移都期待 :date 。我相信 ~D[2019-01-01] 是一个日期。 PostgreSQL 将日期定义为一个 4 字节的二进制值。我期望 Ecto.Adapters.Postgres 将 elixir 日期结构转换为 Postgres 二进制值。这没有发生。为什么?

结构本身只是原始数据。你应该通过 Ecto.Changeset as shown in the documentation, specifically to all types to be cast to the respective DB types with Ecto.Changeset.cast/4.

转换将自动完成,但您需要显式调用 cast/4(因此 Changeset),否则适配器不知道如何转换您的 ecto 类型。