Hello, I have some troubles with inserting into a many-to-many association with one extra field.
I started using the keyword many_to_many in the schema but read somewhere that it didn’t support the extra field so I switched it up to has_many with a through parameter.
Anyway, here is the code:
defmodule Myapp.Accounts.User do
use Ecto.Schema
import Ecto.Changeset
schema "users" do
field :email, :string
field :password, :string, virtual: true, redact: true
field :hashed_password, :string, redact: true
field :confirmed_at, :naive_datetime
has_many :user_groups, MyApp.Join.UserGroup
has_many :groups, through: [:user_groups, :group]
timestamps(type: :utc_datetime)
end
@doc false
def changeset(user, attrs) do
user
|> cast(attrs, [:email])
|> validate_required([:email])
end
end
defmodule Planview.Join.UserGroup do
use Ecto.Schema
schema "user_groups" do
belongs_to :group, Myapp.Groups.Group
belongs_to :user, Myapp.Accounts.User
field :is_admin, :boolean
timestamps()
end
def changeset(struct, params \\ %{}) do
struct
|> Ecto.Changeset.cast(params, [:is_admin])
|> Ecto.Changeset.cast_assoc(:user, required: true)
end
end
defmodule Planview.Groups.Group do
use Ecto.Schema
import Ecto.Changeset
alias Planview.Repo
alias Planview.Join.UserGroup
alias Planview.Accounts
schema "groups" do
field :name, :string
has_many :properties, Planview.Properties.Property
has_many :user_groups, Planview.Join.UserGroup
has_many :users, through: [:user_groups, :user]
timestamps(type: :utc_datetime)
end
@doc false
def changeset(group, attrs) do
group
|> cast(attrs, [:name])
|> validate_required([:name])
end
end
# groups.ex
@doc false
def create_new_group(attrs, user_id, is_admin \\ false) do
# Start a transaction to ensure data integrity
multi = Ecto.Multi.new()
|> Ecto.Multi.insert(:group, Group.changeset(%Group{}, attrs))
|> Ecto.Multi.insert(:user_group, fn %{group: %Group{id: group_id}} ->
# Assuming get_user! returns a user struct or raises an error if not found
user = Myapp.Accounts.get_user!(user_id)
group = Repo.get!(Group, group_id)
# Create the association with is_admin set
Myapp.Join.UserGroup.changeset(%Planview.Join.UserGroup{}, %{user: user, group: group, is_admin: is_admin})
end)
|> Repo.transaction()
case multi do
{:ok, multi_result} ->
{:ok, multi_result}
{:error, changeset} ->
{:error, changeset}
{:error, _, changeset, _} ->
{:error, changeset}
end
end
My real problem occurs when I am calling create_new_group
because it doesn’t insert anything and crashes. I have tried to redo it several times but fails each time.
This is my current error:
** (CaseClauseError) no case clause matching: {:error, :user_group, #Ecto.Changeset<action: :insert, changes: %{is_admin: true}, errors: [user: {"can't be blank", [validation: :required]}], data: #Myapp.Join.UserGroup<>, valid?: false>, %{group: %Myapp.Groups.Group{__meta__: #Ecto.Schema.Metadata<:loaded, "groups">, id: 10, name: "My group 1", properties: #Ecto.Association.NotLoaded<association :properties is not loaded>, user_groups: #Ecto.Association.NotLoaded<association :user_groups is not loaded>, users: #Ecto.Association.NotLoaded<association :users is not loaded>, inserted_at: ~U[2024-02-05 09:06:49Z], updated_at: ~U[2024-02-05 09:06:49Z]}}}
(planview 0.1.0) lib/myapp_web/live/group_live/form_component.ex:73: MyappWeb.GroupLive.FormComponent.save_group/3
I was able to use the many_to_many before and create a association, but I failed to grok how to do it when I wanted to add the is_admin
to the association. The documentation is kind of sparse on how to do it as well.
Can anyone explain to an elixir noob on what I am doing wrong here? How can I insert a new group with a proper relationship with the user?
Thanks for your sage advice and better wisdom!