Ecto own_to changeset insert вставляет в оба отношения

Вступление

У меня проблема с отношениями в Ecto. У меня есть две схемы User и Role, и я хочу иметь возможность вставить нового User с внешним ключом в БД в Таблица ролей.

Проблема в том, что каждый раз, когда я пытаюсь вставить нового пользователя, он не имеет отношения к таблице Role (столбец role_id имеет значение null в User таблица). Я пытался создать ассоциацию, чтобы установить отношения, но это не сработало.


Текущий код

Моя схема User:

  schema "user" do
    field :first_name, :string
    field :last_name, :string
    field :email, :string
    field :encrypted_password, :string
    field :password, :string, virtual: true

    belongs_to :role, TestApp.Role

    timestamps()
  end

Схема роли:

  schema "role" do
    field :name, :string

    timestamps()

    has_many :users, TestApp.User
  end

Я определил набор изменений User следующим образом:

  @required_fields ~w(first_name last_name email password)
  @optional_fields ~w(encrypted_password)

  @required_update_fields ~w()
  @optional_update_fields ~w(first_name last_name email password encrypted_password role)


  def create_changeset(struct, params \\ :empty) do
    changeset(struct, params, @required_fields, @optional_fields)
  end

  def update_changeset(struct, params \\ :empty) do
    changeset(struct, params, @required_update_fields, @optional_update_fields)
  end

  defp changeset(struct, params \\ :empty, required_field, optional_fiels) do
    struct
    |> cast(params, required_field, optional_fiels)
    |> validate_format(:email, ~r/@/, message: "invalid format")
    |> validate_length(:password, min: 5)
    |> validate_confirmation(:password, message: "password does not match")
    |> unique_constraint(:email, message: "email already taken")
    |> generate_encrypted_password
  end

И набор изменений Роль:

  def changeset(struct, params \\ %{}) do
    struct
    |> cast(params, [:name])
    |> validate_required([:name])
    |> unique_constraint(:name)
  end

В моем UserController у меня есть метод create:

  def create(conn, %{"user" => user_params}) do
    changeset = User.create_changeset(%User{}, user_params)

    case User.insert(changeset) do
      {:ok, user} ->
        conn
        |> put_status(:ok)
        |> render(TestApp.UserRender, "show.json", user: user)
      {:error, changeset} ->
        handle_user_creation_validation_error(conn, changeset: changeset)
    end
  end

Проверенные решения (безуспешно)

Я протестировал несколько вариантов, следующий список - это просто пример того, что я пробовал до сих пор (слишком много попыток). Его вообще не нужно компилировать, я просто отправлю его в качестве информации.

Я попытался создать метод insert внутри User, чтобы создать ассоциацию и сохранить набор изменений.

  def insert(changeset) do
    if changeset.valid? do
      IEx.pry
      TestApp.Role.find_by_name("user")
      |> Ecto.build_assoc(:users)
      |> Ecto.change(changeset.params)
      |> Repo.insert
    else
      {:error, changeset}
    end
  end 

Я также попробовал получить роль и создать помощника

TestApp.Role.find_by_name("user")
|> TestApp.Repo.preload(:users)
|> Ecto.build_assoc(:users)
|> Ecto.Changeset.put_assoc(:users, user)
|> Repo.insert!

Я видел этот пример в документации Ecto

  def insert(changeset) do
    Repo.transaction fn ->
      TestApp.Role.find_by_name("user")
      |> TestApp.Repo.preload(:users)
      |> Ecto.Changeset.put_assoc(:users, [changeset])
      |> Repo.insert
  end

Текущий статус

Мне удалось создать отношения между пользователями и ролями, но каждый раз, когда я вставлял нового пользователя новую роль также была вставлена ​​роль (я был шокирован, она просто каждый раз создавала отношение one_to_one!).

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

Решение

Что ж, похоже, необходимо отправить FK как параметр в наборе изменений User, поэтому мне пришлось изменить поле param role на role_id.

Пример запроса тела param может быть таким:

{
    "user" : {
        "first_name": "first name",
        "last_name": "last name",
        "email": "test@email",
        "password": "longtestpassword",
        "password_confirmation": "longtestpassword",
        "role_id": 1
    }
} 

Я также добавил cast_assoc в конвейер проверки набора изменений:

|> cast_assoc(:role)

Реализация User.insert/1 была удалена, и вместо этого контроллер вызывает Repo.insert/1.


Спасибо за вашу помощь!


person qgadrian    schedule 13.09.2016    source источник


Ответы (2)


Изменять

@optional_update_fields ~w(... role)

to:

@optional_update_fields ~w(... role_id)

а затем отправьте просто role_id в своих параметрах вместо карты полей ролей

person amatalai    schedule 13.09.2016
comment
кстати, я думаю, вам следует переместить first_name, last_name и email в обязательные поля, потому что вы, вероятно, не хотите очищать их и полностью удалять encrypted_password из полей обновления, потому что это то, что вы хотите сгенерировать в фоновом режиме - person amatalai; 13.09.2016
comment
Это сработало. Спасибо! Я также добавил |> cast_assoc(:role) в набор изменений пользователя, чтобы сначала проверить отношение. Интересно, почему build и assoc вообще не работают. - person qgadrian; 13.09.2016

Обычно я использую следующие способы решения этой ситуации:

Со стороны user:

  1. напрямую поместите role_id (который наверняка Role.id) в User changese attrs, поэтому после Repo.insert (пользователь) этот новый пользователь будет иметь ForeignKey из Role, например:
def changeset(%User{}=user, %{.., role_id: certain_role_id}=attrs) do
  user 
  |> cast(attrs, [...,:role_id]
  |> validate_require([:role_id, ...])
  1. build_assoc, на самом деле работает так же, как и предыдущий, просто вставьте "role_id" в user , но красивее ..

Со стороны role:

  1. put_assoc, используемый в Role changeset, будет управлять всеми users связанными с текущий role от put_assoc(:users, [user_changeset_lists]). Из-за put_assoc не проверяйте данные, поэтому лучше сделать changeset manual;, вот так:
def changeset(%Role{}=role, new_user) do
  users = Repo.preload(role, :users)
  users = user ++ User.changeset(new_user)
  role
  |> cast(..)
  |> validate_required(..)
  |> put_assoc(:users, users)
end
  1. cast_assoc, например, добавить user для role, не затрагивая уже связанные user и cast_assoc автоматически вызовет User.changeset, например:
def changeset(%Role{}=role, %{users: [new_user1, new_user2]}) do
  role
  ...
  |> cast_assoc(:users)
end
person Bingoabs    schedule 06.09.2017