Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

has_many, through associations in Ecto

I'm still trying to grok how to deal with creating/updating has_many, through: associations in Ecto. I've re-read José's post on associations as well as the docs, but I'm still struggling.

What I have is this:

web/models/dish.ex

defmodule Mp.Dish do
  use Mp.Web, :model

  schema "dishes" do
    # ...
    has_many :dish_dietary_prefs, Mp.DishDietaryPref, on_delete: :delete_all,
      on_replace: :delete
    has_many :dietary_prefs, through: [:dish_dietary_prefs, :dietary_pref]
  end

  # ...
end

web/models/dietary_pref.ex

defmodule Mp.DietaryPref do
  use Mp.Web, :model

  schema "dietary_prefs" do
    # ...
    has_many :dish_dietary_prefs, Mp.DishDietaryPref, on_delete: :delete_all,
      on_replace: :delete
    has_many :dishes, through: [:dish_dietary_prefs, :dish]
  end

  # ...
end

web/models/dish_dietary_pref.ex

defmodule Mp.DishDietaryPref do
  use Ecto.Schema

  schema "dish_dietary_prefs" do
    belongs_to :dish, Mp.Dish
    belongs_to :dietary_pref, Mp.DietaryPref
  end
end

I have a JSON endpoint that receives parameters for a Dish, inside which I have a key called dietary_prefs that is passed as a comma-delimited string, so, for example:

[info] POST /api/vendors/4/dishes
[debug] Processing by Mp.Api.DishController.create/2
  Parameters: %{"dish" => %{"dietary_prefs" => "2,1"}, "vendor_id" => "4"}

(With additional parameters for "dish" removed for this SO post.)


How do I handle this in my controller? Specifically, I want this behavior:

  1. For POST requests (create actions), create the necessary records in dish_dietary_prefs to associate this new Dish with the given DietaryPrefs. The comma-delimited string are ids for DietaryPref records.
  2. For PUT/PATCH requests (updates), create/destroy the necessary records in dish_dietary_prefs to update the associations (users can re-assign dishes to different dietary prefs).
  3. For DELETE requests, destroy dish_dietary_prefs. I think this case is already handled with the on_delete configuration in the models.

I already have the logic in my controller to create/update dishes for a given vendor (which is just a simple has_many/belongs_to relationship), but I still can't figure out how to create/update/destroy these associations for a given dish.

Any help would be greatly appreciated.


If I will "need to receive the IDs and manually build the intermediate association for each" DietaryPref I am associating to the Dish, could I get an example of how I would do that to the above specification in my controller?


UPDATE: Just seeing that Ecto 2.0.0-beta.1 is out, and that is supports many_to_many, which looks like it would be a solution to my problem. Anyone have an example of using it in action, like I've described above?

like image 491
neezer Avatar asked Mar 02 '16 19:03

neezer


People also ask

What is a changeset Ecto?

9.0) Changesets allow filtering, casting, validation and definition of constraints when manipulating structs. There is an example of working with changesets in the introductory documentation in the Ecto module.

Is Ecto an ORM?

Unlike ActiveRecord, Ecto is not an ORM, but a library that enables the use of Elixir to write queries and interact with the database. Ecto is a domain specific language for writing queries and interacting with databases in Elixir.

What is ecto repo?

Defines a repository. A repository maps to an underlying data store, controlled by the adapter. For example, Ecto ships with a Postgres adapter that stores data into a PostgreSQL database.

What is Elixir schema?

Schemas are maps from database tables into Elixir structs; the module provides everything you need to create them. Changeset. Changesets help validate the data that you want to insert into the database or modify.


1 Answers

Thanks to the inimitable Jedi-Master José Valim himself, I have got this figured out (in Ecto 2.0.0-beta.1):

Here's my final controller:

def create(conn, %{"dish" => dish_params }, vendor) do
  dietary_prefs = get_dietary_pref_changeset(dish_params["dietary_prefs"])

  changeset = vendor
  |> build_assoc(:dishes)
  |> Repo.preload(:dietary_prefs)
  |> Dish.changeset(dish_params)
  |> Ecto.Changeset.put_assoc(:dietary_prefs, dietary_prefs)

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

def update(conn, %{"id" => id, "dish" => dish_params}, vendor) do
  dish = Repo.get!(vendor_dishes(vendor), id)
  dietary_prefs = get_dietary_pref_changeset(dish_params["dietary_prefs"])

  changeset = dish
  |> Repo.preload(:dietary_prefs)
  |> Dish.changeset(dish_params)
  |> Ecto.Changeset.put_assoc(:dietary_prefs, dietary_prefs)

  case Repo.update(changeset) do
    { :ok, dish } ->
      render(conn, "show.json", dish: dish)
    { :error, changeset } ->
      conn
      |> put_status(:unprocessable_entity)
      |> render(ChangesetView, "error.json", changeset: changeset)
  end
end

defp vendor_dishes(vendor) do
  assoc(vendor, :dishes)
end

defp parse_dietary_pref_ids(ids) do
  ids
  |> String.split(",")
  |> Enum.map(fn(x) -> Integer.parse(x) |> Kernel.elem(0) end)
end

defp get_dietary_prefs_with_ids(ids) do
  from(dp in DietaryPref, where: dp.id in ^ids) |> Repo.all
end

defp get_dietary_pref_changeset(param) do
  param
  |> parse_dietary_pref_ids
  |> get_dietary_prefs_with_ids
  |> Enum.map(&Ecto.Changeset.change/1)
end

https://groups.google.com/forum/#!topic/elixir-ecto/3cAi6nrsawk

like image 61
neezer Avatar answered Oct 23 '22 04:10

neezer