Ben Smith
Ben Smith

Reputation: 861

Ecto grandparent key on nested children

An organization has many users

schema "organizations" do
  field :name, :string
  has_many :users, TestApp.User
end

A user has many subordinates

schema "users" do
  field :name, :string
  belongs_to :organization, TestApp.Organization
  belongs_to :manager, TestApp.User,
    foreign_key: :manager_id
  has_many :subordinates, TestApp.User,
    foreign_key: :manager_id
end

How do I ensure subordinates have an organization_id value when they are created in the following manner?

  test "create Org and User and Subordinate in one step" do
    subordinate =
      %User{}
      |> User.changeset(%{name: "A Subordinate"})

    manager =
      %User{}
      |> User.changeset(%{name: "A Manager"})
      |> Changeset.put_assoc(:subordinates, [subordinate])

    organization =
      %Organization{}
      |> Organization.changeset(%{name: "An Organization"})
      |> Changeset.put_assoc(:users, [manager])

    %{users: [ %{subordinates: [subordinate]} = manager]} = organization = Repo.insert!(organization)

    # Passes
    assert manager.organization_id == organization.id
    # Fails
    assert subordinate.organization_id == organization.id
  end

Upvotes: 3

Views: 164

Answers (2)

ife brand
ife brand

Reputation: 51

This is a late response, but for anyone who is struggling with creating nested records on Phoenix Ecto. can make use of Ecto.Multi's run or merge function.

You can read more in the official documentation here.

For this problem, you can use this snippet:

Ecto.Multi.run or "Ecto.Multi.merge"

Ecto.Multi.new()
   |> Ecto.Multi.insert(:organization, Organization.create_changeset(%Organization{}, organization_params))
    |> Ecto.Multi.insert(:subordinate, fn %{organization: organization} ->
      User.create_changeset(%User{}, %{
        name: "A Manager",
         organization_id: organization.id
      }))
       |> Ecto.Multi.run(:manager, fn _repo, %{subordinate: subordinate} ->
     Users.create_changeset(
             name: "A Subordinate",
             subordinate_id: organization.id
          )
        |> Ecto.build_assoc(:subordinate)
        |> Repo.insert
   end)
|> Repo.transaction

Upvotes: 0

amatalai
amatalai

Reputation: 417

Option1
Invoke Repo.insert! on every changeset separately

subordinate =
  %User{}
  |> User.changeset(%{name: "A Subordinate"})
  |> Repo.insert!

manager =
  %User{}
  |> User.changeset(%{name: "A Manager"})
  |> Changeset.put_assoc(:subordinates, [subordinate])
  |> Repo.insert!

Create helper function that returns list of managers merged with their subordinates

defp managers_with_subordinates(managers) do
  Enum.reduce(managers, [], &(&2 ++ [&1] ++ &1.subordinates))
end

then pass result to put_assoc

users = managers_with_subordinates([manager])

organization =
  %Organization{}
  |> Organization.changeset(%{name: "An Organization"})
  |> Changeset.put_assoc(:users, users)

and change your pattern matching to

%{users: [user1, user2]} = organization = %Organization{} |> Organization.changeset |> put_assoc(:users, x) |> Repo.insert!

assert user1.organization_id == organization.id
assert user2.organization_id == organization.id
assert user2.manager_id == user1.id

Option2

subordinate =
  %User{}
  |> User.changeset(%{name: "A Subordinate"})
  |> prepare_changes( fn(changeset) ->
    manager = changeset.repo.get(User, changeset.changes.manager_id)
    changeset |> cast(%{organization_id: manager.organization_id}, [:organization_id])
  )

Upvotes: 1

Related Questions