ЭКТО-актуализация вложенных (полиморфных) ассоциаций


Как обновить модель с вложенной ассоциацией (используя [Elixir, Phoenix, Ecto])?

Я попытался сделать следующее, чтобы рассматривать его как часть родительского обновления, но безуспешно (используя блог platformatec в качестве вдохновения).

Модели:

  schema "user" do
    has_one :address, {"users_addresses", MyApp.Address}, foreign_key: :assoc_id
  end
  @required_fields ~w(address)

------

  # Materialized in users_addresses table 
  schema "abstract table: addresses" do
    field :assoc_id,        :integer
    field :street_address,  :string
  end

Запрос (патч):

{
  "user" => {
    "address" => {
      "street_address" => "1234"
    }
  }
}

Контроллер:

def update(conn, %{"id" => id, "user" => params}) do
  user = MyApp.Repo.get(User, id)
    |> MyApp.Repo.preload [:address]

  if is_nil(user) do
    send_resp(conn, 404, "")
  else
    changeset = User.changeset(user, params)

    if changeset.valid? do
      case MyApp.Repo.update(changeset) do
        {:ok, model} -> send_resp(conn, 204, "")
        {:error, changeset} -> conn
          |> put_status(:unprocessable_entity)
          |> render(MyApp.ChangesetView, "error.json", changeset: changeset)
      end
    else
      conn
      |> put_status(:unprocessable_entity)
      |> render(MyApp.ChangesetView, "error.json", changeset: changeset)
    end
  end
end

Набор изменений (из журналов):

%Ecto.Changeset{action: nil,
  changes: %{address: %Ecto.Changeset{action: :insert, changes: %{street_address: "1234"},
    constraints: [],

....

  model: %MyApp.User{__meta__: #Ecto.Schema.Metadata<:loaded>,
    address: %MyApp.Address{__meta__: #Ecto.Schema.Metadata<:loaded>,
      assoc_id: 1229, id: 308,
      street_address: "41423 Jakubowski Village"
      ....
    }
  }
}

ошибка: исправлено с Ecto v1. 0. 3 или более поздней версии

** (exit) an exception was raised:
    ** (Postgrex.Error) ERROR (undefined_table): relation "abstract table: addresses" does not exist
        (ecto) lib/ecto/adapters/sql.ex:479: Ecto.Adapters.SQL.model/6
        (ecto) lib/ecto/repo/model.ex:219: Ecto.Repo.Model.apply/4
        (ecto) lib/ecto/repo/model.ex:71: anonymous fn/10 in Ecto.Repo.Model.insert/4
        (ecto) lib/ecto/repo/model.ex:340: anonymous fn/3 in Ecto.Repo.Model.wrap_in_transaction/8
        (ecto) lib/ecto/adapters/sql.ex:531: anonymous fn/10 in Ecto.Adapters.SQL.transaction/3
        (ecto) lib/ecto/pool.ex:262: Ecto.Pool.inner_transaction/3
        (ecto) lib/ecto/adapters/sql.ex:534: Ecto.Adapters.SQL.transaction/3
        (ecto) lib/ecto/association.ex:368: Ecto.Association.Has.on_repo_action/7
1 5

1 ответ:

(Postgrex.Error) ERROR (undefined_table): relation "abstract table: addresses" does not exist это произошло из-за ошибки, которая должна быть исправлена в Ecto v1.0.3 или позже.


В приведенном выше коде отсутствует идентификатор адреса, без которого Ecto вставит новый ресурс вместо обновления существующего.

Запрос (патч):

{
  "user" => {
    "address" => {
      "id" => 4,
      "street_address" => "1234"
    }
  }
}

Новый код контроллера, включая некоторые из предложенных улучшений JoseValims:

 def update(conn, %{"id" => id, "user" => params}) do
    user = MyApp.Repo.get!(User, id)
      |> MyApp.Repo.preload [:address

    changeset = User.changeset(user, params)
    case MyApp.Repo.update(changeset) do
      {:ok, model} -> send_resp(conn, 204, "")
      {:error, changeset} -> conn
        |> put_status(:unprocessable_entity)
        |> render(MyApp.ChangesetView, "error.json", changeset: changeset)
    end
  end

Или, в этой ситуации, поскольку адрес является одновременно обязательным и has_one, идентификатор может быть добавлен на стороне сервера:

 def update(conn, %{"id" => id, "user" => params}) do
    user = MyApp.Repo.get!(User, id)
      |> MyApp.Repo.preload [:address]

    address = params["address"]
    address = Map.put address, "id", user.address.id
    params = Map.put params, "address", address

    changeset = User.changeset(user, params)
    case MyApp.Repo.update(changeset) do
      {:ok, model} -> send_resp(conn, 204, "")
      {:error, changeset} -> conn
        |> put_status(:unprocessable_entity)
        |> render(MyApp.ChangesetView, "error.json", changeset: changeset)
    end
  end