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

Abefore_insertはおそらくこれを行うのに適切な場所ではありません。

関連付けを使用していますが、反対側に関連付けbelongs_toを含める場合は、build/3を使用して親 IDを入力できます。has_many

build(user, :posts)

これは、構造体のフィールドに入力する関数にすぎません。実際に存在するpost_idかどうかは検証しません。user

投稿を作成する前にユーザーが存在することを確認したい場合は、変更セットでforeign_key_constraint/3を使用できます:

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

これにより、データベース レベルで親のレコードが存在することが保証されます。移行が次のようになった場合に作成されるデータベースにインデックスが必要です。

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

編集

仮想属性は必要ありません。コントローラー アクションは次のようになります。

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 に答える