Nested associations, get and list functions return duplicate nested values when they should be unique

EDIT: I figured out the associations. See below. Please delete this if you think it would be best. This post helped me a lot.

Hello everyone,

Thanks for all the great work you do and the effort you put into this community.

I’m new to Elixir and Phoenix (have been at it for about a week), and I’m really struggling to get off the ground and make my associations behave the way I want them to. This is also my first foray into backend development. I am primarily an Elm progammer who wants to build cool things with Elixir too.

I have some pretty complicated associations that I can’t really change without wrecking the frontend we built. Our old backend is Django Rest, but I’m trying to learn Elixir and Phoenix to replace it. Please don’t mistake my thickheadedness here for lack of effort in reading the docs. I’ve been pouring over them and beating my face off the keyboard for several days trying to get going.

My associations look like this (code below):

I have some documents. Each document has two contributors. I’ve called them writer and editor. The same contributor might be a writer for one document and an editor for another document. Each contributor has something called an attention profile that has some more primitives inside of it.

I can add documents and they come out how I expect them to with unique contributors in my writer and editor fields. I can call create_document in iex with some data and see that everything is okay. I can call my render function and see that it’s dealing with a correct contract. I can look at the database itself and see that I have a unique writer and a unique editor per document, but when I do list_documents or get_document, I get back documents that have the same contributor in the writer field and the editor field.

Here’s my contributors table:

 id | name  |                 address                  |                email                | document_id |        inserted_at         |         
updated_at

1 | Steve | {"456 Local St",Here,"OH, 11115",USA}    | {steve@college.edu,steve@gmail.com} |           1 | 
2018-10-08 22:12:22.877254 | 2018-10-08 22:12:22.877268
2 | Sarah | {"123 Smith St",Faraway,"NH, 32445",USA} | {sarah@.uni.edu,sarah@gmail.com}    |           1 | 
2018-10-08 22:12:22.881554 | 2018-10-08 22:12:22.881566
(2 rows)

Here’s what I end up with with at http://localhost:4000/documents/1:

{"data":
    {"writer":{"name":"Steve","id":1,"email":["steve@college.edu","steve@gmail.com"],"attn_profile": 
    [{"phone":"876876","id":1,"fax_number":"876876","email":"eva6@college.edu","attention":"Eva"}],"
    address": ["456 Local St","Here","OH, 11115","USA"]},
   "title":"new research",
    "id":1,"
    editor": 
    {"name":"Steve","id":1,"email":["steve@college.edu","steve@gmail.com"],"attn_profile": 
    [{"phone":"876876","id":1,"fax_number":"876876","email":"eva6@college.edu","attention":"Eva"}],
    "address": ["456 Local St","Here","OH, 11115","USA"]},
 "content":"new content!"}}

Here are my schemas and changesets.

  schema "documents" do
    field :title, :string
    field :content, :string
    has_one :writer, Contributor
    has_one :editor, Contributor

    timestamps()
end

@doc false
def changeset(document, attrs) do
  document
  |> cast(attrs, [:title, :content])
  |> validate_required([:title, :content])
end

  schema "contributors" do
    field :name, :string
    field :address, {:array, :string}
    field :email, {:array, :string}
    has_many :attn_profile, AttnProfile
    belongs_to :document, Document

   timestamps()
end

@doc false
def changeset(contributor, attrs) do
  contributor
  |> cast(attrs, [:name, :address, :email])
  |> validate_required([:name, :address, :email])
end

  schema "attn_profile" do
    field :attention, :string
    field :email, :string
    field :fax_number, :string
    field :phone, :string
    belongs_to :contributor, Contributor

    timestamps()
  end

@doc false
def changeset(attn_profile, attrs) do
  attn_profile
  |> cast(attrs, [:attention, :email, :phone, :fax_number])
  |> validate_required([:attention, :email, :phone, :fax_number])
end

Migrations:

def change do
  create table(:documents) do
    add :title, :string
    add :content, :string

    timestamps()
  end
end

def change do
  create table(:contributors) do
    add :name, :string
    add :address, {:array, :string}
    add :email, {:array, :string}
    add :document_id, references(:documents, on_delete: :nothing)

    timestamps()
   end
end

def change do
  create table(:attn_profile) do
    add :attention, :string
    add :email, :string
    add :phone, :string
    add :fax_number, :string
    add :contributor_id, references(:contributors, on_delete: :delete_all),
               null: false

    timestamps()
  end

  create index(:attn_profile, [:contributor_id])
 end

Here’s my code for creating a contract:

def create_document(attrs \\ %{}) do
%Document{}
  |> Document.changeset(attrs)
  |> Ecto.Changeset.cast_assoc(:writer, with: &create_writer_or_editor_from_req/2)
  |> Ecto.Changeset.cast_assoc(:editor, with: &create_writer_or_editor_from_req/2)
  |> Repo.insert()
end

def create_writer_or_editor_from_req(_struct, map_to_use \\ %{}) do
  %Contributor{}
    |> Contributor.changeset(map_to_use)
    |> Ecto.Changeset.cast_assoc(:attn_profile, with: &AttnProfile.changeset/2)
end

And finally the code for listing all contracts and getting individual contracts:

  def list_documents do
    Document
      |> Repo.all()
      |> Repo.preload([writer: [:attn_profile], editor: [:attn_profile]])
  end

  def get_document!(id) do
    Document
     |> Repo.get!(id)
     |> Repo.preload([writer: [:attn_profile], editor: [:attn_profile]])
  end

Sorry for the long post, but I tried to include everything from the start.

These are my questions. They are all pretty closely related, I think, and maybe an answer to one will make the others non-issues.

  1. Have I made a bad design decision that’s forced me into strange territory I shouldn’t be in, and if so, is there a better way that’s immediately obvious?
  2. If my basic design is sound, how can I make sure I return contracts that don’t have the same contributor in both the writer and editor field?
  3. Our old Django backend was nice in that you could submit a request with just a writer ID and an editor ID without sending the whole object. Is there a good way to do that or would that go against the Ecto way? I imagine I would need another changeset that had fields for writer and editor that were IDs instead of associations.
  4. How can I save a document with two new contributors without adding duplicate contributors to the database?

Any help would be greatly appreciated. Thank you.

EDIT:

This makes the proper associations:

  schema "documents" do
    field :title, :string
    field :content, :string
    has_one :writer, Contributor, foreign_key: :writer_id
    has_one :editor, Contributor, foreign_key: :editor_id

    timestamps()
end

@doc false
def changeset(document, attrs) do
  document
  |> cast(attrs, [:title, :content])
  |> validate_required([:title, :content])
end

schema "contributors" do
  field :name, :string
  field :address, {:array, :string}
  field :email, {:array, :string}
  has_many :attn_profile, AttnProfile
  belongs_to :writer, Contract, foreign_key: :writer_id
  belongs_to :editor, Contract, foreign_key: :editor_id

  timestamps()
end

@doc false
def changeset(document, attrs) do
  document
  |> cast(attrs, [:title, :content])
  |> validate_required([:title, :content])
end

def change do
  create table(:contributors) do
  add :name, :string
  add :address, {:array, :string}
  add :email, {:array, :string}
  add :writer_id, references(:documents, on_delete: :nothing)
  add :editor_id, references(:documents, on_delete: :nothing)

  timestamps()
end

create unique_index(:contributors, [:writer_id])
create unique_index(:contributors, [:editor_id])
end