Search code examples
elixirecto

Elixir Ecto: How to set a belongs_to association in a changeset


I am a bit stuck in how to actually set an association with a changeset. I have this code in my model:

defmodule MyApp.MemberApplication do
  use MyApp.Web, :model
  use Ecto.Schema
  use Arc.Ecto.Schema

  alias MyApp.Repo
  alias MyApp.MemberApplication

  schema "applications" do
    field :name, :string
    field :email, :string
    field :time_accepted, Ecto.DateTime
    field :time_declined, Ecto.DateTime
    belongs_to :accepted_by, MyApp.Admin
    belongs_to :declined_by, MyApp.Admin

    timestamps()
  end

  def set_accepted_changeset(struct, params \\ %{}) do
    struct
    |> cast(params, [:time_accepted, :accepted_by_id])
    |> cast_assoc(params, :accepted_by)
    |> set_time_accepted
  end

  defp set_time_accepted(changeset) do
    datetime = :calendar.universal_time() |> Ecto.DateTime.from_erl()
    put_change(changeset, :time_accepted, datetime)
  end
end

I want to save an association to the Admin that performed a certain operation (accepting or declining an member_application) and a timestamp. The generation of the timestamp works but when I try to save an association I always get the error

** (FunctionClauseError) no function clause matching in Ecto.Changeset.cast_assoc/3

This is how I want to set the association:

iex(26)> application = Repo.get(MemberApplication, 10)
iex(27)> admin = Repo.get(Admin, 16)
iex(28)> changeset = MemberApplication.set_accepted_changeset(application, %{accepted_by: admin})

Solution

  • Thanks @Dogbert. This is how I got it to work

    defmodule MyApp.MemberApplication do
      use MyApp.Web, :model
      use Ecto.Schema
      use Arc.Ecto.Schema
    
      alias MyApp.Repo
      alias MyApp.MemberApplication
    
      schema "applications" do
        field :name, :string
        field :email, :string
        field :time_accepted, Ecto.DateTime
        field :time_declined, Ecto.DateTime
        belongs_to :accepted_by, MyApp.Admin
        belongs_to :declined_by, MyApp.Admin
    
        timestamps()
      end
    
      def set_accepted_changeset(struct, params \\ %{}) do
        struct
        |> cast(params, [:time_accepted, :accepted_by_id])
        # Change cast_assoc 
        |> cast_assoc(:accepted_by)
        |> set_time_accepted
      end
    
      defp set_time_accepted(changeset) do
        datetime = :calendar.universal_time() |> Ecto.DateTime.from_erl
        put_change(changeset, :time_accepted, datetime)
      end
    end
    

    And then preload the association and set the ID directly. Or do it directly in the query:

    iex(26)> application = Repo.get(MemberApplication, 10)
    iex(27)> application = Repo.preload(application, :accepted_by)
    iex(28)> admin = Repo.get(Admin, 16)
    iex(29)> changeset = MemberApplication.set_accepted_changeset(application, %{accepted_by_id: admin.id})