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.
- 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?
- 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?
- 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.
- 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