Search code examples
elixirphoenix-frameworkecto

belong_to multiple parents


Hi I'm new to Elixir and models/DB/APIs in general and I'm a bit lost in how to achieve the following:

I want to create an Event when I do a POST in my API to /v1/events sending both tag_id and gate_id but I don't know how to handle this in my controller.

I've done this with simpler models where they just belong_to one parent and I just pipe the build_assoc in the changeset, however, I'm lost on how to do it with multiple parents. I even tried a sketchy solution to manually create a changeset manually setting the gate_id and tag_id like this Event.changeset(%Event{"gate_id" => gate_id, "tag_id" => tag_id}, event_params) however it errored out saying gate_id and tag_id are not part of the model.

So two questions: What is the correct way to handle this situation? Am I even close or my models are wrong?

Also does a POST to /v1/events passing both of the ids sounds good or am I also lost in that one?

Models for reference

Tag model:

defmodule Tracker.Tag do
  use Tracker.Web, :model

  schema "tags" do
    field :epc, :string
    field :name, :string
    field :is_active, :boolean, default: false
    field :is_inside, :boolean, default: false

    has_many :events, Tracker.Event

    timestamps
  end

 @required_fields ~w(epc name is_active is_inside)
  @optional_fields ~w()

  def changeset(model, params \\ :empty) do
    model
    |> cast(params, @required_fields, @optional_fields)
    |> cast_assoc(:events, required: false)
  end
end

Gate Model:

defmodule Tracker.Gate do
  use Tracker.Web, :model

  schema "gates" do
    field :name, :string
    field :direction_in, :boolean, default: false
    field :antenna, :integer
    belongs_to :reader, Tracker.Reader

    has_many :events, Tracker.Event

    timestamps
  end

  @required_fields ~w(name direction_in antenna)
  @optional_fields ~w()

  def changeset(model, params \\ :empty) do
    model
    |> cast(params, @required_fields, @optional_fields)
    |> cast_assoc(:events, required: false)
  end
end

Update: More info

Event Model:

defmodule Tracker.Event do
  use Tracker.Web, :model

  schema "events" do
    belongs_to :tag, Tracker.Tag
    belongs_to :gate, Tracker.Gate

    timestamps
  end

  @required_fields ~w()
  @optional_fields ~w()

  def changeset(model, params \\ :empty) do
    model
    |> cast(params, @required_fields, @optional_fields)
  end
end

Events Migration:

defmodule Tracker.Repo.Migrations.CreateV1.Event do
  use Ecto.Migration

  def change do
    create table(:events) do
      add :tag_id, references(:tags, on_delete: :nothing)
      add :gate_id, references(:gates, on_delete: :nothing)

      timestamps
    end
    create index(:events, [:tag_id])
    create index(:events, [:gate_id])

  end
end

Event controller create function (kind of lost on this one)

  def create(conn, %{"event" => event_params}) do
    gate_id = conn.assigns.gate_id
    tag_id = conn.assigns.tag_id

    changeset = Event.changeset(%Event{"gate_id" => gate_id, "tag_id" => tag_id}, event_params)

    case Repo.insert(changeset) do
      {:ok, event} ->
        conn
        |> put_status(:created)
        |> put_resp_header("location", v1_event_path(conn, :show, event))
        |> render("show.json", event: event)
      {:error, changeset} ->
        conn
        |> put_status(:unprocessable_entity)
        |> render(Tracker.ChangesetView, "error.json", changeset: changeset)
    end
  end

And the latest error I'm getting is

== Compilation error on file web/controllers/v1/event_controller.ex ==
** (CompileError) web/controllers/v1/event_controller.ex:22: unknown key "gate_id" for struct Tracker.Event

Solution

  • Can you post your error and your migration for Event as well? I believe you need to add tag_id and gate_id to required fields in your model:

    defmodule Tracker.Event do
      use Tracker.Web, :model
    
      schema "events" do
        belongs_to :tag, Tracker.Tag
        belongs_to :gate, Tracker.Gate
    
        timestamps
      end
    
      @required_fields ~w(tag_id gate_id)
      @optional_fields ~w()
    
      def changeset(model, params \\ :empty) do
        model
        |> cast(params, @required_fields, @optional_fields)
      end
    end