BenFenner

BenFenner

How to remove/delete a many-to-many relationhip?

I’m relatively new to Elixir/Phoenix (I have much more familiarity with RoR) but I’ve made it quite far in my new application by following tutorials and the documentation.

I am building a sort of one-stop shop for user accounts, where users can be created, and given permissions to one or more web sites (AKA resources) that I work on. Users can have many resources they belong to, and resources have many users that belong to it, so I went with a many-to-many relationship.

I created the user_resources table in the database as a full table with an auto-generated ID. Since a user can’t belong to a resource twice (or vice versa), I’ve created what I believe is a good unique index.

defmodule MyApp.Repo.Migrations.CreateUserResources do
  use Ecto.Migration

  def change do
    create table(:user_resources) do
      add :user_id, references(:users)
      add :resource_id, references(:resources)
      add :inserted_by, :integer
      add :updated_by, :integer

      timestamps()
    end

    create unique_index(:user_resources, [:user_id, :resource_id])
  end
end

I also set up a schema for this relationship. I much prefer my relationships to be as fully fleshed out as possible, so that’s what I’ve done here.

defmodule MyApp.Account.UserResource do
  use Ecto.Schema
  import Ecto.Changeset


  schema "user_resources" do
    field :resource_id, :integer
    field :user_id, :integer
    field :inserted_by, :integer
    field :updated_by, :integer

    timestamps()
  end

  @doc false
  def changeset(user_resource, attrs) do
    user_resource
    |> cast(attrs, [:user_id, :resource_id, :inserted_by, :updated_by])
    |> validate_required([:user_id, :resource_id, :inserted_by, :updated_by])
  end
end

I can successfully add a User to a Resource in my controller like this:

  def add_user(conn, %{"resource_id" => resource_id, "user_id" => user_id}) do
    resource = Domain.get_resource!(resource_id)
    user = Account.get_user!(user_id)
    Domain.add_user(resource, user)

    conn
    |> put_flash(:info, "User added to resource successfully.")
    |> redirect(to: resource_path(conn, :show, resource_id))
  end

Which calls add_user in the Domain Context, which does the real work:

  def add_user(%Resource{} = resource, user) do
    resource
    |> Repo.preload(:users)
    |> Ecto.Changeset.change()
    |> Ecto.Changeset.put_assoc(:users, [user])
    |> Repo.update!
  end

This gives me a user_resources record in the database, and with the User in an Account Context, I have this function which gives me back a list of the users associated with the given resource:

  def list_resource_users(resource) do
    Repo.all(assoc(resource, :users))
  end

So far, so good.

Now I want to remove the association, so I’m following the advice of this post: Many-to-many associations in phoenix and ecto - #20 by idi527

That part of my resource controller looks like this:

  def remove_user(conn, %{"resource_id" => resource_id, "user_id" => user_id}) do
    Domain.remove_user(resource_id, user_id)

    conn
    |> put_flash(:info, "User removed from resource successfully.")
    |> redirect(to: resource_path(conn, :show, resource_id))
  end

And the function defined in the Domain Context looks like this:

  def remove_user(resource_id, user_id) do
    "user_resources"
    |> where(resource_id: ^resource_id)
    |> where(user_id: ^user_id)
    |> Repo.delete()
  end

However, when I initiate that code, I get this error:
function Ecto.Query.__changeset__/0 is undefined or private

I have tried many different things, and gotten a few small improvements, but I think there must be something very simple I’m missing that might stand out to someone more experienced?

I appreciate any help I can get, and I’ll try to provide more info as I work on things.

Most Liked

kokolegorille

kokolegorille

That might override when setting new users

mikemccall

mikemccall

As @kokolegorille said put_assoc will set the users to whatever is in the list. ie. [user]. Or in other words remove any users not in that list. You will need to include all of the users when using put_assoc or manage the association separately for single records.

Something along the lines of:

resource = Repo.preload(resource, :users)
resource
|> Ecto.Changeset.put_assoc(:users, [user | resource.users])

This function should be used when working with the entire association at once (and not a single element of a many-style association) and using data internal to the application.
put_assoc/4

mikemccall

mikemccall

Repo.delete/2 expects a changeset_or_struct as the first argument. It looks like you’re piping an #Ecto.Query ( ie.queryable) into it. I believe delete_all/2 is what you’re looking for.

Where Next?

Popular in Questions Top

gshaw
What is the idiomatic way of matching for not nil in Elixir? E.g., First way: defp halt_if_not_signed_in(conn, signed_in_account) when...
New
myronmarston
The Elixir Typespec docs show the following syntax for keyword lists in typespecs: # ... | [key: type] # keyword lists...
New
shahryarjb
Hello, I have map which I want to convert it to string like this: the map: %{last_name: "tavakkoli", name: "shahryar"} the string I ne...
New
vac
Hi, I’m quite new in Elixir and I’m trying to format a string to a PEM format. I have the certificate value like MIIDBTCCAe2...... and I...
New
Qqwy
Original source of discussion: This topic on the Pragmatic Programmers’ Functional Web Development with Elixir, OTP, and Phoenix forum. ...
New
aalberti333
As the title describes, I’m trying to run Enum.map() over a list of key/value pairs, where the value is a map. My data looks like this: ...
New
RisingFromAshes
I’ve read in another post that it may be possible with a router helper - but I couldn’t find an appropriate one, and tbh, I’m still just ...
New
fayddelight
I tried installing elixir 1.11.2 erlang 23.3.4 via asdf in my zsh shell. Enabled the versions locally and globally. When I list them ...
New
jason.o
In the code below, if the create action is not set to accept “extra_key” as an input, it errors out with a message shown above. Is there ...
New
senggen
Erlang/OTP 25 [erts-13.2.2] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] 15:22:35.803 [error] gen_event {lager_file_backend...
New

Other popular topics Top

lastday4you
I wanted to check elixir version in phoenix because i found that my elixir is 1.5 but when i use Enum.chunk_by it said the function is un...
New
stefanluptak
Hello everybody, usually, I use a 29" ultra-wide monitor for VSCode which can easily accomodate explorer (files panel) + file with code ...
New
lessless
I believe there are people here who are dealing with CSV files import on the daily basis, and since Excel is a really popular tool there ...
New
sen
Hi All, I set a environment variables in dev.exs , like below code. when i start server, how can i set the ${enable} value? thanks. d...
New
Patoshizzle
After calling mix ecto.create I get this error: 17:00:32.162 [error] GenServer #PID<0.412.0> terminating ** (Postgrex.Error) FATAL...
New
SoCreat
i’m a new one to elixir which editor can i use vs code? or atom? Thanks! :smiley:
New
fayddelight
I tried installing elixir 1.11.2 erlang 23.3.4 via asdf in my zsh shell. Enabled the versions locally and globally. When I list them ...
New
ashish173
I am using Ecto timestamps with postgres, I can see the timestamps() use the :naive_dateime but for my use case I wanted to store the ti...
New
rms.mrcs
Hi, I need to transform a list of numbers into a map where the keys are the indexes and the values are the original values of the list. ...
New
svb
Hi! Currently I want to submit a form by pressing the Enter key. However, since my input field is of type “textarea” this is just adds a...
New

We're in Beta

About us Mission Statement