Save multiple has_many association at once Phoenix 1.3

phoenix
ecto
Tags: #<Tag:0x00007f1149949a88> #<Tag:0x00007f1149949808>

#1

I have two models:

 defmodule TransactionApi.Messages.Event do
  use Ecto.Schema
  import Ecto.Changeset
  alias TransactionApi.Messages.Event
  alias TransactionApi.Messages.EventDetail

  schema "events" do
    field :city, :string
    field :email, :string
    field :ip, :string
    field :sender, :string
    field :status, :string
    field :subject, :string
    field :template, :string
    field :ts, :utc_datetime
    field :uniq_id, :string
    field :user_agent, :string

    has_many :event_details, EventDetail

    timestamps()
  end

  @doc false
  def changeset(%Event{} = event, attrs) do
    event
    |> cast(attrs, [:sender, :uniq_id, :ts, :template, :subject, :email, :status, :ip, :city, :user_agent])
    |> cast_assoc(:event_details)
    |> validate_required([:sender, :uniq_id, :ts, :subject, :email, :status])
  end
end

defmodule TransactionApi.Messages.EventDetail do
  use Ecto.Schema
  import Ecto.Changeset
  alias TransactionApi.Messages.EventDetail
  alias TransactionApi.Messages.Event


  schema "event_details" do
    field :ts, :utc_datetime
    field :url, :string

    belongs_to :event, Event, foreign_key: :event_id
    timestamps()
  end

  @doc false
  def changeset(%EventDetail{} = event_detail, attrs) do
    event_detail
    |> cast(attrs, [:url, :ts, :event_id])
    |> validate_required([:ts, :event_id])
  end
end

I want to save the Event and it’s associated EventDetail in my event controller:

  def create(conn, %{"mandrill_events" => event_params}) do
    params = parse_incoming event_params
    with {:ok, %Event{} = event} <- Messages.create_event(params) do
      conn
      |> put_status(:created)
      |> put_resp_header("location", event_path(conn, :show, event))
      |> render("show.json", event: event)
    end
  end

This is how the params map I built looks like:

%{      
  city: "Oklahoma City",
  email: "example.webhook@mandrillapp.com",
  event: "open",
  event_details: [
    %{"ts" => #DateTime<2013-04-04 21:31:51Z>, "url" => "http://mandrill.com"},
    %{"ts" => #DateTime<2013-04-04 21:31:51Z>}
  ],
  ip: "127.0.0.1",
  sender: "example.sender@mandrillapp.com",
  status: "sent",
  subject: "This an example webhook message",
  tags: ["webhook-example"],
  template: nil,
  ts: #DateTime<2018-02-12 12:33:48Z>,
  uniq_id: "exampleaaaaaaaaaaaaaaaaaaaaaaaaa",
  user_agent: "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.6; en-US; rv:1.9.1.8) Gecko/20100317 Postbox/1.1.3"
}

But my api returns an error : {"errors":{"event_details":[{"event_id":["can't be blank"]},{"event_id":["can't be blank"]}]}}

In Phoenix 1.3 it seems that they’ve added a create_[table_name] located in the context of the model and deals with changeset and insertion (I think that change came with wanting to separate the web related part from the application, not sure though):

def create_event(%{event: event_params, event_details: event_details_params} \\ %{}) do
  event_changeset =  %Event{} |> Event.changeset(event_params)
  events =
    Multi.new
    |> Multi.insert(:event, event_changeset)
    |> Multi.run(:event_details, fn %{event: event} ->
      event_details_changeset =
        %EventDetail{event_id: event.id}
        |> EventDetail.changeset(event_details_params)
      Repo.insert(event_details_changeset)
    end)
    Repo.transaction events
  end

How do I make sure the associated EventDetail are correctly persisted with a foreign_key reference to the Event table, what’s the “best practice” approach here?

Right now I’m getting this error

Request: POST /api/events
** (exit) an exception was raised:
    ** (MatchError) no match of right hand side value: #Ecto.Changeset<action: nil, changes: %{city: "Oklahoma City", email: "example.webhook@mandrillapp.com", ip: "127.0.0.1", sender: "example.sender@mandrillapp.com", status: "sent", subject: "This an example webhook message", ts: #DateTime<2018-02-12 19:29:18Z>, uniq_id: "exampleaaaaaaaaaaaaaaaaaaaaaaaaa", user_agent: "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.6; en-US; rv:1.9.1.8) Gecko/20100317 Postbox/1.1.3"}, errors: [], data: #TransactionApi.Messages.Event<>, valid?: true>
        (transaction_api) lib/transaction_api/messages/event.ex:30: TransactionApi.Messages.Event.changeset/2
        (transaction_api) lib/transaction_api/messages/messages.ex:55: TransactionApi.Messages.create_event/1
        (transaction_api) lib/transaction_api_web/controllers/event_controller.ex:27: TransactionApiWeb.EventController.create/2
        (transaction_api) lib/transaction_api_web/controllers/event_controller.ex:1: TransactionApiWeb.EventController.action/2
        (transaction_api) lib/transaction_api_web/controllers/event_controller.ex:1: TransactionApiWeb.EventController.phoenix_controller_pipeline/2
        (transaction_api) lib/transaction_api_web/endpoint.ex:1: TransactionApiWeb.Endpoint.instrument/4
        (phoenix) lib/phoenix/router.ex:278: Phoenix.Router.__call__/1
        (transaction_api) lib/transaction_api_web/endpoint.ex:1: TransactionApiWeb.Endpoint.plug_builder_call/2
        (transaction_api) lib/plug/debugger.ex:99: TransactionApiWeb.Endpoint."call (overridable 3)"/2
        (transaction_api) lib/transaction_api_web/endpoint.ex:1: TransactionApiWeb.Endpoint.call/2 
        (plug) lib/plug/adapters/cowboy/handler.ex:15: Plug.Adapters.Cowboy.Handler.upgrade/4
        (cowboy) /Users/cyrusghazanfar/Desktop/elixir/transaction_api/deps/cowboy/src/cowboy_protocol.erl:442: :cowboy_protocol.execute/4

#2
  def changeset(%EventDetail{} = event_detail, attrs) do
    event_detail
    |> cast(attrs, [:url, :ts, :event_id])
    |> validate_required([:ts, :event_id])
  end

What if you don’t cast and require :event_id? I think ecto takes care of that when cast_assoc is used.

btw, you probably shouldn’t cast foreign keys if they come from users …