save multiple has_many association at once Phoenix 1.3

Cyzanfar Source

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: "[email protected]",
  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: "[email protected]",
  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"]}]}}

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?

Edit:

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 seperate the web related part from the application, not sure though):

  def create_event(attrs \\ %{}) do
    %Event{}
    |> Event.changeset(attrs)
    |> Repo.insert()
  end
elixirphoenix-frameworkecto

Answers

answered 8 months ago Cyzanfar #1

I figured out a way to save a map containing nested association but since I'm new to Phoenix and Elixir (functional programming in general) I'm not sure this is the right/best practice approach.

%{      
  city: "Oklahoma City",
  email: "[email protected]",
  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: "[email protected]",
  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"
}

event_controller.ex

  def create(conn, %{"mandrill_events" => event_params}) do
    params = parse_incoming event_params
    with {:ok, %Event{} = event} <- Messages.create_event(params) do
      event
      |> Messages.add_event_details(params[:event_details])

      conn
      |> put_status(:created)
      |> put_resp_header("location", event_path(conn, :show, event))
      |> render("show.json", event: event)
    end
  end

Then in my Message context:

  # persist an `event`

  def create_event(%{event: event_params} \\ %{}) do
    %Event{}
    |> Event.changeset(event_params)
    |> Repo.insert()
  end

  # persist a collection of `event_details`

  def add_event_details(%Event{} = event, details) do
    Enum.map(details, fn(event_detail) ->
      event_detail
      |> Map.put("event_id", event.id)
      |> create_event_detail
    end)
  end

comments powered by Disqus