Delete Record and display errors

I am testing my endpoint to delete a record. It works fine if record doesn’t have any child associations.

This is the code:

       def delete(conn, %{"id" => id}) do
         record = get_record(id)

          case Repo.delete(record) do
            {:ok, struct} ->
               send_resp(conn, :no_content, "Record deleted")

          {:error, reason} ->
           conn
             |> put_status(422)
             |> render(Admin.ErrorView, "error.json",
               code: 422,
              message: "Error occurred",
              errors: reason
             )
         end
       end

  def get_record(id) do
    case Repo.get(model, id) do
      nil ->
        render(Admin.ErrorView, "error.json", code: 404, message: "Record not found")

      record ->
        record
    end
  end

But I want to send the proper error message if the record has some relationship with some other records. It returns the internal server error . and not the foreign key error.

Any suggestions?

Thanks

You might use foreign key constraint.

The relevant code should be something like this …

your_record
|> Ecto.Changeset.change()
|> foreign_key_constraint(:your_assocs, name: :your_assocs_your_record_id_fkey, message: "still exist")
1 Like

Thanks for your suggestion.This foreign key constraint works if we want to create or update child and its parents doesn’t exist. What I want is other way around, Like if we delete a parent. I thought {:error, reason} should throw an error about foreign key but gives server error.

Well, the example is from Programming Phoenix 1.4, and it is for solving the exact same problem, to put a constraint on the parent, not to be able to delete it if there is still children :slight_smile:

PS. You should note the plural and singular form…

In the book, it’s for category, with many videos.

Have you tried getting a full backtrace? Or can you post what exact error phoenix is generating?

One thing that pops out to me is that Ecto.Repo.delete returns a changeset in the error tuple on failure… so the reason in {:error, reason} is a changeset rather than, say, a simple string. So if your template is expecting reason to be a plain renderable string, it probably isn’t :wink:

Also, it is possible under certain circumstances (e.g. missing table fields, such as from a typo) for an exception to be thrown and that won’t be caught by your case statement…

Hard to know without seeing more of the code though…

I made an example with nodes and leafs

  schema "nodes" do
    field(:name, :string)
    has_many(:leafs, Leaf)
    timestamps()
  end

...
  schema "leafs" do
    field(:name, :string)
    belongs_to(:node, Node)
    timestamps()
  end

I load a node, with leafs…

iex> import Ecto.Changeset
iex> n = EctoLab.Fk.get_node(1) |> EctoLab.Repo.preload(:leafs)
%EctoLab.Fk.Node{
  __meta__: #Ecto.Schema.Metadata<:loaded, "nodes">,
  id: 1,
  inserted_at: ~N[2018-09-06 12:41:43.978124],
  leafs: [
    %EctoLab.Fk.Leaf{
      __meta__: #Ecto.Schema.Metadata<:loaded, "leafs">,
      id: 2,
      inserted_at: ~N[2018-09-06 12:53:11.336298],
      name: "lolo", 
      node: #Ecto.Association.NotLoaded<association :node is not loaded>,
      node_id: 1,
      updated_at: ~N[2018-09-06 12:53:11.336308]
    }
  ],
  name: "koko",
  updated_at: ~N[2018-09-06 12:41:43.979841]
}

And here, if I try to delete the node

iex> changeset = Ecto.Changeset.change(n)
iex> changeset = foreign_key_constraint(changeset, :leafs, name: :leafs_node_id_fkey, message: "still exist") 
#Ecto.Changeset<action: nil, changes: %{}, errors: [], data: #EctoLab.Fk.Node<>,
 valid?: true>
iex(30)> EctoLab.Repo.delete(changeset)

14:59:08.942 [debug] QUERY OK db=0.1ms
begin []
 
14:59:08.949 [debug] QUERY ERROR db=6.4ms
DELETE FROM "nodes" WHERE "id" = $1 [1]
 
14:59:08.949 [debug] QUERY OK db=0.2ms
rollback []
{:error,
 #Ecto.Changeset<
   action: :delete,
   changes: %{},
   errors: [leafs: {"still exist", []}],
   data: #EctoLab.Fk.Node<>,
   valid?: false
 >}

I cannot delete it, because it has leafs, and I got the message in the changeset.

2 Likes

This is the proper solution. You can also use Ecto.Changeset.no_assoc_constraint which, as stated in the docs:

This is similar to foreign_key_constraint/3 except that the field is inferred from the association definition. This is useful to guarantee that parent can only be deleted (or have its primary key changed) if no child exists in the database. Therefore, it only applies to has_* associations.

2 Likes

Thanks

Appreciate your help.