Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Elixir Ecto: How to validate foreign key constraint?

I'm playing with Elixir and the Phoenix web framework, but now I'm stuck on trying to validate a foreign key constraint. So, given a model Post with many comments, I wrote the Comment model as follows:

defmodule MyApp.Comment do
  use MyAPp.Web, :model

  schema "comments" do
    field :body, :text
    belongs_to :post, MyApp.Post

    timestamps
  end

  @required_fields ~w(body post_id)
  @optional_fields ~w()

  def changeset(model, params \\ :empty) do
    model
    |> cast(params, @required_fields, @optional_fields)
    |> foreign_key_constraint(:post_id)
  end
end

and its unit test:

defmodule MyApp.CommentTest do
  # [...]
  test "changeset with non existent post" do
    attrs = %{
      body: "A comment."
      post_id: -1 # some non-existent id?
    }
    refute Comment.changeset(%Comment{}, attrs).valid?
    assert {:post_id, "does not exist"} in errors_on(%Comment{}, %{})
  end
end

According to http://hexdocs.pm/ecto/Ecto.Changeset.html#foreign_key_constraint/3:

The foreign key constraint works by relying on the database to check if the associated model exists or not. This is useful to guarantee that a child will only be created if the parent exists in the database too.

I expected that the code I wrote worked, instead it only checks for presence (as defined in @required_fields ~w(body post_id)). I'm not excluding I did something wrong or misunderstood the statement in the docs.

Has anyone already stumbled upon this?

UPDATE: For completeness, here's the migration:

def change do
  create table(:comments) do
    add :body, :text
    add :post_id, references(:posts)

    timestamps
  end

  create index(:comments, [:post_id])
end
like image 690
Andrea Pavoni Avatar asked Aug 25 '15 13:08

Andrea Pavoni


2 Answers

Since it relies on the database, you need to add the references in the migration and do the actual database operation. You must call Repo.insert/1 or Repo.update/1 giving your changeset and it will then return {:error, changeset}.

Remember, there are no objects in Elixir nor in Ecto. Therefore changeset.valid? could never perform a database operation, it is just data reflecting a set of changes to be performed and the state of this data transforms as you perform operations, like insert or update.

One final note, errors_on/2 is always going to return a new changeset and not the one you have been working with so far. Your last line should likely be:

assert {:post_id, "does not exist"} in changeset.errors
like image 157
José Valim Avatar answered Nov 06 '22 17:11

José Valim


"relying on the database" means you need to have a FOREIGN KEY CONSTRAINT in your Database model.

In your migration, you should have had something like this:

create table(:comments) do
  add :post_id, references(:posts)
end

which enforces a FOREIGN KEY CONSTRAINT CHECK between the parent and the child table.

like image 43
Lenin Raj Rajasekaran Avatar answered Nov 06 '22 16:11

Lenin Raj Rajasekaran