3

I'm coming to the Phoenix framework from Rails. So far it's been a fairly easy transition. Phoenix is newer, though, and I'm having trouble finding some specific information:

I'm using my Phoenix app as an API service layer.
I want my UI form (and incoming curl requests) to use a virtual field to find an associated parent model, and fill the child model's changeset with the appropriate attribute. So far, so good:

in my child model:

schema "child" do
    field :parent_name, :string, virtual: true
    belongs_to :parent, MyApp.Parent
end

...

before_insert :find_and_fill_parent

    def find_and_fill_parent(changeset) do
        parent = MyApp.Repo.get_by(
                  MyApp.Parent, 
                  parent_name: changeset.changes.parent_name
                 )

        changeset
        |> Ecto.Changeset.put_change(:parent_id, parent.id)
    end

This is where I'm stuck. I do not want to allow the child to be created without a parent. How and where do I nil check the parent model? Everything I've tried has either blocked or allowed creation unconditionally, despite conditional statements.

It seems like I need to preload the parent model before the check, since Phoenix is designed to prevent people like me from abusing lazy load. Unfortunately, I don't know what the proper load pattern is in general, so I'm not sure how to apply it here. (I'm using MySQL, if that's relevant)

Hints and tips about where to look and what to look at to help me figure this out are appreciated! Thanks!

---EDIT---
Per @Gazler 's advice I have made sure that my child model migration has a reference:

create table(:child) do
    add :parent_id, references(:parent)
end

I'm still a little lost -- I want to find the parent by the parent field parent_name ("Jane Doe"), make sure the parent model exists, and associate the child using parent_id (1). I'm not sure how to trigger these actions around using a virtual field.

So, I'm not sure how to structure finding the parent, building the association, and checking the foreign key validation, since the foreign key will never exist in the original params. Thoughts?

Thanks so much.

---RESOLVED---
Using @Gazler 's updated answer, I can successfully nil check my parent model in the child controller without a virtual attribute or before_insert.

def create(conn, %{"post" => post_params}) do 
    user = Repo.get_by(User, %{name: post_params["name"]})
    if is_nil(user) do
        changeset = Post.changeset(%Post{}, post_params)
    else
        changeset = build(user, :posts) |> Post.changeset(post_params)
    end
    etc
end

this validates the incoming parameters exactly like i need! thanks @Gazler!

4

1 回答 1

4

A before_insert is probably not the correct place to be doing this.

You are using a belongs_to association, however if you include the has_many association on the other side then you can use build/3 to fill in the parent id:

build(user, :posts)

This is just a function that fills in the post_id field for the struct - it won't validate that the user actually exists.

If you want to ensure that the user exists before creating a post then you can use foreign_key_constraint/3 on your changeset:

cast(comment, params, ~w(user_id), ~w())
|> foreign_key_constraint(:user_id)

This will ensure at the database level that the record for the parent exists. You need to have the index in your database which will be created if your migration looked like:

create table(:posts) do
  add :user_id, references(:user)
end

EDIT

There is no need for a virtual attribute. Your controller action should look something like:

def create(conn, %{"name" => name, "post" => post_params}) do
  user = Repo.get_by(User, %{name: name})
  changeset = build(user, :posts) |> Post.changeset(post_params)
  case Repo.insert(changeset) do
    {:ok, post} -> ...
    {:error, changeset} -> #if constraint is violated then the error will be in the changeset
  end
end
于 2015-10-07T14:28:12.170 回答