如何在Ecto中使用Postgres的枚举类型

19

使用PostgreSQL,我们可以这样做:

CREATE TYPE order_status AS ENUM ('placed','shipping','delivered')

根据Ecto官方文档,没有本地类型可以映射到Postgres的枚举类型。这个模块提供了一个自定义类型来处理枚举结构,但它将被映射为数据库中的整数。我可以轻松地使用该库,但我更喜欢使用与数据库一起提供的本地枚举类型。

Ecto还提供了创建自定义类型的方法,但据我所见,自定义类型必须映射到本地Ecto类型...

有人知道在Ecto模式中是否可以做到这一点吗?如果可以,迁移会如何工作?

6个回答

30

也许我做错了什么,但我只是像这样创建了类型和字段:

# creating the database type
execute("create type post_status as enum ('published', 'editing')")

# creating a table with the column
create table(:posts) do
  add :post_status, :post_status, null: false
end

然后只需将字段变为字符串:

field :post_status, :string

看起来它能够工作。


14
对于那些对该框架完全陌生的人,JustMichael的解决方案是有效的,但我想补充一下代码需要放置的位置。第一个代码段在迁移文件中,位于change do块内。第二个块位于模型文件中,在schema do块内。 - jeffreymatthias
2
当您传递一个不是“published”或“editing”的字符串时会发生什么?会出现什么类型的错误? - Terence Chow
2
@TerenceChow 很有可能会引发数据库错误,导致您的数据库操作失败。 - NoDisplayName
2
我需要更正我的先前评论,应该是validate_inclusion(changeset, :post_status, ["published", "editing"]) - sinned

10

为 @JustMichael 进行小改进。如果需要回滚,您可以使用:

def down do
  drop table(:posts)
  execute("drop type post_type")
end

10

总结所有答案和评论中的零散信息。有关使用的SQL命令,请参阅PostgreSQL手册中的"枚举类型"

Ecto 3.0.0及以上版本

自从Ecto 3.0.0以来,有Ecto.Migration.execute/2,它“执行可逆的SQL命令”,因此可以在change/0中使用:

迁移

使用mix ecto.gen.migration create_orders生成迁移后:

defmodule CreateOrders do
  use Ecto.Migration

  @type_name :order_status

  def change do    
    execute(
      """
      CREATE TYPE #{@type_name}
        AS ENUM ('placed','shipping','delivered')
      """,
      "DROP TYPE #{@type_name}"
     )

    create table(:orders) do
      add :order_status, @type_name, null: false
      timestamps()
    end
  end
end

架构

与"Ecto 2.x.x及以下版本"中相同。

Ecto 2.x.x及以下版本

迁移

使用mix ecto.gen.migration create_orders生成迁移后:

defmodule CreateOrders do
  use Ecto.Migration

  @type_name :order_status

  def up do    
    execute(
      """
      CREATE TYPE #{@type_name}
        AS ENUM ('placed','shipping','delivered'})
      """)

    create table(:orders) do
      add :order_status, @type_name, null: false
      timestamps()
    end
  end

  def down do
    drop table(:orders)
    execute("DROP TYPE #{@type_name}")
  end
end

模式

由于模式无法看到迁移中创建的数据库类型,因此在Order.changeset/2中使用Ecto.Changeset.validate_inclusion/4来确保输入有效。

defmodule Order do

  use Ecto.Schema
  import Ecto.Changeset

  schema "orders" do
    field :order_status, :string    
    timestamps()
  end

  def changeset(
    %__MODULE__{} = order,
    %{} = attrs
  ) do

    fields = [ :order_status ]

    order
    |> cast(attrs, fields)
    |> validate_required(fields)
    |> validate_inclusion(
         :order_status,
         ~w(placed shipping delivered)
       )
  end
end

1
感谢您提供的解决方案。我已经看到了它,并准备在我的代码库中实现它,但是将生产后端代码包含在迁移的一部分中感觉不对。很可能您order模式上的statuses函数会发生变化(例如更改名称/删除函数),这将防止应用程序编译。在我看来,最好将实时代码排除在迁移之外,并将其视为始终可以编译和运行的常青文档。如果添加了新状态,请创建一个新的迁移以记录它(您永远不会运行旧的迁移以升级类型)。 - Dorian
1
你说得对,我完全把它复杂化了。我也刚刚编辑了答案。感谢你的见解! - toraritte

6

您需要为每个postgresql枚举创建一个Ecto类型。在模式定义中,您只需将类型设置为:string。在迁移中,您将类型设置为模块名称。然而,这可能会变得非常繁琐,因此我在我的项目中使用以下宏来使用Postgresql枚举:

defmodule MyDB.Enum do

  alias Postgrex.TypeInfo

  defmacro defenum(module, name, values, opts \\ []) do
    quote location: :keep do
      defmodule unquote(module) do

        @behaviour Postgrex.Extension

        @typename unquote(name)
        @values unquote(values)

        def type, do: :string

        def init(_params, opts), do: opts

        def matching(_), do: [type: @typename]

        def format(_), do: :text

        def encode(%TypeInfo{type: @typename}=typeinfo, str, args, opts) when is_atom(str), do: encode(typeinfo, to_string(str), args, opts)
        def encode(%TypeInfo{type: @typename}, str, _, _) when str in @values, do: to_string(str)
        def decode(%TypeInfo{type: @typename}, str, _, _), do: str

        def __values__(), do: @values

        defoverridable init: 2, matching: 1, format: 1, encode: 4, decode: 4

        unquote(Keyword.get(opts, :do, []))
      end
    end
  end

end

可能的用途:

import MyDB.Enum
defenum ColorsEnum, "colors_enum", ~w"blue red yellow"

ColorsEnum 将作为模块名称,"colors_enum" 将是 Postgresql 内部的枚举名称:您需要在数据库迁移中添加一个语句来创建枚举类型。最后一个参数是枚举值列表。我使用了 ~w 符号,它将字符串按空格拆分,以显示其简洁性。当原子值通过 Ecto schema 时,我还添加了一个子句将其转换为字符串值。


谢谢你的回答!这看起来很有前途,但我不知道如何在模式中使用ColorsEnum(我明白迁移部分)。当我在模式中添加字段时,应该使用什么类型?:string - Jean-Pierre Bécotte
是的,在实际模式定义中应该使用:string。Enum类型之所以对于postgrex发挥作用很必要,是因为它将内部postgresql oid映射到Elixir类型,而在这种情况下是一个字符串。 - asonge
1
@asonge 虽然我尊重你的 Elixir 技能,但我个人不需要为每个 PostgreSQL 枚举类型创建一个 Ecto 类型。也许我错过了什么,但是你为什么需要它呢?只需使用字符串和 changeset 验证器即可。https://hexdocs.pm/ecto/Ecto.Changeset.html#validate_inclusion/4 - Dave Goulash
1
@DaveGoulash 我记不清这是在改变集之前还是之后了,但它们肯定还不太流行。根据你想要支持的 Ecto API 层,你仍然可能想要将枚举用作 Ecto 类型。更改集只是 Ecto 中众多 API 中的一个,您不必使用它们,在实践中,如果您忘记在一个更改集上放置 validate_inclusion 而在另一个更改集上没有呢?在现代实践中,你是对的。大多数人只使用更改集 API 来更新或插入数据。 - asonge
@asonge 如果程序员决定绕过Ecto进行一些更新怎么办?;-) 不,我知道你在说什么,但是我可以不考虑Elixir代码中的每种可能的输入错误。我想我可以接受Postgres在某些情况下作为最后的数据保护。 - Dave Goulash

5

4
似乎 ecto_enum 不再维护。 - ryanwinchester
1
实际上,由于Ecto具有更改集验证“validate_inclusion”并将值存储为字符串,因此枚举并不是真正必需的。 - apelsinka223

5

除了 @JustMichael 和 @swennemen 所说的之外,截至ecto 2.2.6版本,我们有Ecto.Migration.execute/2函数,它需要up和down参数。所以我们可以在我们的迁移文件中的change块内执行以下代码:

execute("create type post_status as enum ('published', 'editing')", "drop type post_status")

这样,ecto就能有效地回滚。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接