Phoenix / Elixir /Ecto — unique_constraint не работает с параметром name

#elixir #phoenix-framework #ecto

#elixir #phoenix-framework #ecto

Вопрос:

То, что я ожидал, произойдет: при попытке вставить persona с уже существующим именем пользователя для вызова Repo.insert, чтобы вернуть мне набор изменений с ошибкой в деталях.

Что происходит: Ecto вызывает жесткое исключение

Я получаю следующую ошибку в моем проекте Phoenix.

 ** (exit) an exception was raised:
    ** (Ecto.ConstraintError) constraint error when attempting to insert struct:

    * personas_username_index (unique_constraint)

If you would like to stop this constraint violation from raising an
exception and instead add it as an error to your changeset, please
call `unique_constraint/3` on your changeset with the constraint
`:name` as an option.

The changeset has not defined any constraint.
  

Поэтому я последовал его совету и добавил имя уникального индекса в свою базу данных, personas_username_index . Этот уникальный индекс определенно существует в реальной базовой базе данных.

Вот моя схема:

 defmodule Poaster.Persona do
  use Ecto.Schema
  import Ecto.Changeset
  alias Poaster.User

  schema "personas" do
    belongs_to :user, User
    field :background_image_url, :string
    field :bio, :string
    field :name, :string
    field :profile_image_url, :string
    field :username, :string

    timestamps()
  end

  @doc false
  def changeset(persona, attrs) do
    persona
    |> cast(attrs, [:username, :name, :bio, :profile_image_url, :background_image_url, :user])
    |> validate_required([:username])
    |> unique_constraint(:username, name: :personas_username_index)
  end
end
  

И вот мой код контроллера:

 def create(conn, %{"username" => username}) do
    # First attempt
    # result = Ecto.build_assoc(conn.assigns[:signed_user], :personas, %{ username: username })
    # IO.inspect(result)
    # result = Repo.insert(result)
    # IO.inspect(result)

    user = conn.assigns[:signed_user]
    result = Repo.insert(%Persona{username: username, user: user})


    case result do
      {:ok, persona} ->
        conn
          |> put_status(:created)
          |> json(persona_data(persona))

      {:error, _changeset} ->
        conn
          |> put_status(:internal_server_error)
          |> json(%{
              success: false,
              error: %{
                detail: "There was an error saving your username!"
              }
            })
    end
  end

  defp persona_data(persona) do
    %{
      id: persona.id,
      background_image_url: persona.background_image_url,
      bio: persona.bio,
      inserted_at: persona.inserted_at,
      name: persona.name,
      profile_image_url: persona.profile_image_url,
      updated_at: persona.updated_at,
      username: persona.username
    }
  end
  

Файл миграции:

 defmodule Poaster.Repo.Migrations.CreatePersonas do
  use Ecto.Migration

  def change do
    create table(:personas) do
      add :username, :string, null: false
      add :name, :string
      add :bio, :string
      add :profile_image_url, :string
      add :background_image_url, :string
      add :user_id, references(:users, on_delete: :nothing), null: false

      timestamps()
    end

    create unique_index(:personas, [:username])
    create index(:personas, [:user_id])
  end
end

  

Комментарии:

1. Вы использовали миграцию для создания индекса? Если да, не могли бы вы также добавить код переноса?

2. Да, я внес изменения в сообщение, добавив дополнительный файл миграции.

Ответ №1:

Я думаю, что вы должны вставить набор изменений, а не схему напрямую, чтобы иметь возможность перехватить ошибку, как описано в документах:

https://hexdocs.pm/ecto/Ecto.Набор изменений.html#unique_constraint/3-complex-constraints

Вы вызываете unique_constraint/3 в своей Persona.changeset/2 функции, но вы не вызываете Persona.changeset/2 в коде вашего контроллера.

Я считаю, что вам следует заменить следующую строку:

 result = Repo.insert(%Persona{username: username, user: user}
  

Автор::

 result = %Persona{}
  |> Persona.changeset(%{username: username, user: user})
  |> Repo.insert()
  

Или, скорее, вероятно, более идиоматично иметь create_persona/1 функцию, определенную в контексте, которая это делает, и вызывать эту функцию из вашего контроллера (по крайней мере, так это организует генератор).

Комментарии:

1. Спасибо, это изменение решило проблему! Да, я полагаю, проблема была в том, что я не передавал ему набор изменений. Вы упомянули контекст; я сгенерировал свою схему без контекста. Есть ли способ добавить контекст к уже сгенерированным схемам? Прошу прощения, я новичок в Elixir / Phoenix. У меня есть User , Persona и AuthToken схемы, которые я хотел бы сгруппировать в Accounts контекст.

2. Я думаю, вы можете попробовать запустить mix phx.gen.context еще раз. hexdocs.pm/phoenix/Mix.Tasks.Phx.Gen.Context.html#content — возможно, прямо в вашем проекте и посмотрите, как это работает — если результат слишком беспорядочный, вы можете создать новое пустое приложение, запустить эти команды там, затем скопировать Accounts файл контекста в свой проект. Это не должно требовать слишком много изменений, это почти всегда одно и то же.