Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Ecto - Updating nested (Polymorphic) Associations

How does one update a model with a nested association (using [Elixir, Phoenix, Ecto])?

I've tried the following, to treat it as part of it's parent update, without success (using the platformatec blog as inspiration).

Models:

  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

Request (PATCH):

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

Controller:

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

Changeset (from logs):

%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"
      ....
    }
  }
}

Error: Fixed as of Ecto v1.0.3 or later

** (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
like image 741
Zachary Moshansky Avatar asked Nov 09 '22 05:11

Zachary Moshansky


1 Answers

(Postgrex.Error) ERROR (undefined_table): relation "abstract table: addresses" does not exist was due to a bug, which should be fixed in Ecto v1.0.3 or later.


The code above is missing an id for the address, without this, Ecto will insert a new resource instead of updating an existing one.

Request (PATCH):

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

The new controller code, including some of JoseValims suggested improvements:

 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

Or, in this situation since the address is both required and has_one, the id can be added server side:

 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
like image 92
Zachary Moshansky Avatar answered Nov 15 '22 09:11

Zachary Moshansky