Changeset composite primary key for deleting


I am trying to write a subject-follower relationship schema. I have this migration

defmodule TweetClone.Repo.Migrations.CreateUserRelationships do
  use Ecto.Migration

  def change do
    create table(:user_relationships, primary_key: false) do
      add :subject_id,
            on_delete: :delete_all,
            on_update: :update_all,
            primary_key: true

      add :follower_id,
            on_delete: :delete_all,
            on_update: :update_all,
            primary_key: true


    create unique_index(:user_relationships, [:subject_id, :follower_id])

and this schema

defmodule TweetClone.UserRelationships.UserRelationship do
  use Ecto.Schema
  import Ecto.Changeset

  alias TweetClone.Accounts.User

  @primary_key false
  schema "user_relationships" do
    belongs_to :follower, User, primary_key: true
    belongs_to :subject, User, primary_key: true


  @doc false
  def changeset(user_relationship, %{subject: subject, follower: follower}) do
    |> change
    |> put_assoc(:subject, subject)
    |> put_assoc(:follower, follower)
    |> validate_required([:subject, :follower])

The intent is that UserRelationship not to have a auto generated id but a composite made from the foreign keys subject_id and follower_id

It works fine and when I want to delete that row I do

  def delete_user_relationship(subject_id, follower_id) do
    query =
      from u in UserRelationship,
        where: u.subject_id == ^subject_id,
        where: u.follower_id == ^follower_id


and the row is deleted.

When I try do delete based on the changeset I use this function

  def delete_user_relationship(subject, follower) do
    attrs = %{
      subject: subject,
      follower: follower

    |> UserRelationship.changeset(attrs)
    |> Repo.delete()
iex(29)> UserRelationships.delete_user_relationship( user3, user1)
** (Ecto.NoPrimaryKeyValueError) struct `%TweetClone.UserRelationships.UserRelationship{__meta__: #Ecto.Schema.Metadata<:built, "user_relationships">, follower: #Ecto.Association.NotLoaded<association :follower is not loaded>, follower_id: nil, inserted_at: nil, subject: #Ecto.Association.NotLoaded<association :subject is not loaded>, subject_id: nil, updated_at: nil}` is missing primary key value
    (ecto 3.4.2) lib/ecto/repo/schema.ex:903: anonymous fn/3 in Ecto.Repo.Schema.add_pk_filter!/2
    (elixir 1.10.1) lib/enum.ex:2111: Enum."-reduce/3-lists^foldl/2-0-"/3
    (ecto 3.4.2) lib/ecto/repo/schema.ex:427: anonymous fn/10 in Ecto.Repo.Schema.do_delete/4

So it complains that our changeset is missing a primary key.

I tried to change the UserRelationship schema to pass only the user’s primary keys instead of the full users structs

  @primary_key false
  schema "user_relationships" do
    field :subject_id, :integer, primary_key: true
    field :follower_id, :integer, primary_key: true
    belongs_to :follower, User
    belongs_to :subject, User


but during compilation it complains that

== Compilation error in file lib/tweetclone/user_relationships/user_relationship.ex ==
** (ArgumentError) field/association :follower_id is already set on schema
    (ecto 3.4.2) lib/ecto/schema.ex:2001: Ecto.Schema.put_struct_field/3
    (ecto 3.4.2) lib/ecto/schema.ex:1758: Ecto.Schema.define_field/4
    (ecto 3.4.2) lib/ecto/schema.ex:1841: Ecto.Schema.__belongs_to__/4
    lib/tweetclone/user_relationships/user_relationship.ex:11: (module)
    (stdlib 3.11.2) erl_eval.erl:680: :erl_eval.do_apply/6
    (elixir 1.10.1) lib/kernel/parallel_compiler.ex:233: anonymous fn/4 in Kernel.ParallelCompiler.spawn_workers/7

How can I fix the UserRelationship schema so I can simply pass the users primary keys and be assigned as the composite primary key in the changeset? Or even passing the full User structs and be able to delete with the changeset?

I found a satisfactory solution to my problem. Turns out I didn’t need to delete by using a changeset. I just wasn’t satisfied with the return from the query based delete {1, nil}

I realized that one can do

 %{%UserRelationship{} | subject_id: 1, follower_id: 3} 
|> Repo.delete(stale_error_field: :subject, stale_error_message: "relationship does not exist")

or even better, with type safety

|> Repo.load(%{subject_id: 1, follower_id: 3}) 
|> Repo.delete(stale_error_field: :subject, stale_error_message: "relationship does not exist")

This returns {:ok, Ecto.Schema.t()} | {:error, Ecto.Changeset.t()}