Как использовать перечислимый тип Postgres с Ecto

В PostgreSQL мы можем сделать что-то вроде этого:

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

Из Ecto official doc, нет никакого родного типа для сопоставления перечисляемого типа Postgres. Этот модуль предоставляет настраиваемый тип для перечисляемых структур, но он сопоставляет целое число в базе данных. Я мог бы легко использовать эту библиотеку, но я бы предпочел использовать собственный нумерованный тип, который поставляется с базой данных.

Ecto предоставляет также способ создания настраиваемых типов, но, насколько я вижу, пользовательский тип должен отображать собственный тип Ecto...

Кто-нибудь знает, можно ли это сделать в схеме с Ecto? Если да, как будет работать миграция?

Ответ 2

Возможно, я сделал что-то не так, но я только что создал тип и поле вроде этого:

# 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

и, похоже, он работает.

Ответ 3

Небольшое усиление для @JustMichael. Если вам нужно откат, вы можете использовать:

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

Ответ 4

Вам нужно создать тип Ecto для каждого перечисления postgresql. В определении схемы вы просто имеете тип :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" будет именем enum, внутренним для Postgresql: вам нужно будет добавить инструкцию для создания типа перечисления в ваших миграциях базы данных. Конечным аргументом является список значений перечисления. Я использовал сигилу ~w, которая разделит строку на пробел, чтобы показать, насколько это может быть кратким. Я также добавил предложение, которое преобразует значения атомов в строковые значения, когда они проходят через схему Ecto.

Ответ 5

добавив к тому, что @JustMichael и @swennemen сказали... с ecto 2.2.6 у нас есть Ecto.Migration.execute/2, который принимает вверх и вниз arg. Таким образом, мы можем сделать:

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

В нашем файле миграции внутри блока change, и ecto сможет эффективно откатиться.

Ответ 6

Обобщая все кусочки здесь и там в ответах и комментариях. См. "Перечисляемые типы" в руководстве по PostgreSQL, чтобы узнать больше об используемых командах SQL.

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

схемы

Поскольку схема не может видеть тип базы данных, созданный в процессе миграции, используйте Ecto.Changeset.validate_inclusion/4 в Order.changeset/2 для обеспечения правильного ввода.

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