Ectoでhas_many through

# 概要
タグ情報的な`n : m`の関係を作るだけなら、`many_to_many`を使えば多分楽に作れるんだろうけど、「中間テーブルにも追加で属性値もたせたりしたいんだい」、という場合、まぁ`has_many through`を使うんだと思うんですが、Railsに慣れていると若干ハマったのでメモ。
あ、ちなみに書いてる人は、Rails使いですが、Elixir/Phoenixはまだまだ慣れてません。
もっとエレガントに書けんだぜ？的なご意見ご感想は、歓迎いたします。

# 前提
`users`と`teams`が`n : m`です。
中間テーブルは`user_belongings`としています。

# migration
## users
メールアドレスが、ユニークな毎度のやつです。

``` ex
defmodule App.Repo.Migrations.CreateUsers do
  use Ecto.Migration

  def change do
    create table(:users) do
      add :email, :string
      add :first_name, :string
      add :last_name, :string
      timestamps
    end

    create index(:users, [:email], unique: true)
  end
end
```

## teams
こっちは名前でユニークです。
そういや、Ectoのmigrationでスキーマコメントってどうするんだ？

``` ex
defmodule Nta.Repo.Migrations.CreateTeams do
  use Ecto.Migration

  def change do
    create table(:teams) do
      add :name, :string
      timestamps
    end

    create index(:teams, [:name], unique: true)
  end
end
```

## user_belongings
デフォルトのプライマリキーは外して、`user_id`と`team_id`を複合プライマリキーとしています。
今の所は、他に属性値がないですが、あとで追加される予定です。

``` ex
defmodule App.Repo.Migrations.CreateUserBelongings do
  use Ecto.Migration

  def change do
    create table(:user_belongings, primary_key: false) do
      add :user_id, references(:users)
      add :team_id, references(:teams)

      timestamps
    end

    create index(:user_belongings, [:user_id, :team_id],　primary_key: true)
    create index(:user_belongings, [:user_id])
    create index(:user_belongings, [:team_id])
  end
end
```

# model
## User
``` web/models/user.ex
defmodule App.User do
  use Ecto.Schema
  
  @primary_key {:id, :id, autogenerate: true}

  schema "users" do
    field :email, :string
    field :first_name, :string
    field :last_name, :string

    timestamps()

    has_many :belongings, App.UserBelonging
    has_many :teams, through: [:belongings, :team]
  end
end
```

## Team
``` web/models/team.ex
defmodule App.Team do
  use Ecto.Schema
  
  @primary_key {:id, :id, autogenerate: true}

  schema "teams" do
    field :name, :string

    timestamps()

    has_many :belongings, App.UserBelonging
    has_many :users, through: [:belongings, :user]
  end
end
```

## UserBelonging
``` web/models/user_belonging.ex
defmodule App.UserBelonging do
  use Ecto.Schema
  
  @primary_key false

  schema "user_belongings" do
    timestamps()
    belongs_to :user, App.User, primary_key: :user_id
    belongs_to :team, App.Team, primary_key: :team_id
  end
end
```
複合プライマリキーはこんな感じで書くらしい。

# 実行
``` ex
iex> {:ok, user} = %User{email: "hoge@hoge.com", last_name: "hoge", first_name: "hoge" } |> insert
iex> user |> preload(:teams)
%App.User{__meta__: #Ecto.Schema.Metadata<:loaded, "users">, belongings: [],
 email: "hoge@hoge.com", first_name: "hoge", id: 1,
 inserted_at: ~N[2017-10-08 08:45:10.973353], last_name: "hoge", teams: [],
 updated_at: ~N[2017-10-08 08:45:11.106633]}
iex> user |> Ecto.assoc([:belongings, :team]) |> all
[]
iex> user |> Ecto.assoc(:teams) |> all
[]
```

とりあえず、`user`経由での`:teams`は、どうやら取得できているようです。
まぁまだ一つも関連がないので、空リストですが。
では、teamを作って、userと関連づけてみます。

``` ex
iex> {:ok, team1} = %Team{name: "fuge1"} |> insert 
iex> user |> preload(:teams) |> Ecto.Changeset.change |> Ecto.Changeset.put_assoc(:teams, [team1])
** (ArgumentError) cannot put assoc `teams`, assoc `teams` not found. Make sure it is spelled correctly and properly pluralized (or singularized)
    (ecto) lib/ecto/changeset.ex:751: Ecto.Changeset.relation!/4
    (ecto) lib/ecto/changeset.ex:1089: Ecto.Changeset.put_relation/5
```

`teams`なんてassoc定義されてねーよ、と怒られました……。
どうも、[この辺り](https://hexdocs.pm/ecto/Ecto.Schema.html#has_many/3-has_many-has_one-through)を読むと、
> In fact, given :through associations are read-only, using the Ecto.assoc/2 format is the preferred mechanism for working with through associations. 

まぁちゃんと読んでないですが（ぉ）、ダメっぽい雰囲気は感じられました。
Railsは勝手に色々やってくれるのですが、それに慣れてるとこういうところで詰まります。
というわけで`UserBelonging`を直に操作しないといけないらしいです。

## 追加

```ex

iex> belongings = user |> Ecto.assoc(:belongings) |> all
iex> new_belonging = team1 |> Ecto.build_assoc(:belongings)
iex> belongings = belongings ++ [new_belonging]
iex> user |> preload(:belongings) |> Ecto.Changeset.change |> Ecto.Changeset.put_assoc(:belongings, belongings) |> update
{:ok,
 %App.User{__meta__: #Ecto.Schema.Metadata<:loaded, "users">,
  belongings: [%App.UserBelonging{__meta__: #Ecto.Schema.Metadata<:loaded, "user_belongings">,
    inserted_at: ~N[2017-10-08 09:17:17.943882],
    team: #Ecto.Association.NotLoaded<association :team is not loaded>,
    team_id: 1, updated_at: ~N[2017-10-08 09:17:17.943894],
    user: #Ecto.Association.NotLoaded<association :user is not loaded>,
    user_id: 1}], email: "hoge@hoge.com", first_name: "hoge", id: 1,
  inserted_at: ~N[2017-10-08 08:45:10.973353], last_name: "hoge",
  teams: #Ecto.Association.NotLoaded<association :teams is not loaded>,
  updated_at: ~N[2017-10-08 08:45:11.106633]}}

iex> user |> Ecto.assoc(:teams) |> all
[%App.Team{__meta__: #Ecto.Schema.Metadata<:loaded, "teams">,
  belongings: #Ecto.Association.NotLoaded<association :belongings is not loaded>,
  id: 1, inserted_at: ~N[2017-10-08 08:45:11.000000], 
  name: "fuge1", updated_at: ~N[2017-10-08 08:45:11.000000]
  users: #Ecto.Association.NotLoaded<association :users is not loaded>}]
```

無事、関連の保存と取得が出来ているようです。

## さらに追加
では、次に関連をさらに追加してみます。

``` ex
iex> {:ok, team2} = %Team{name: "fuge2"} |> insert
iex> belongings = user |> Ecto.assoc(:belongings) |> all
iex> new_belonging = team2 |> Ecto.build_assoc(:belongings)
iex> belongings = belongings ++ [new_belonging]
iex> user |> preload(:belongings) |> Ecto.Changeset.change |> Ecto.Changeset.put_assoc(:belongings, belongings) |> update
iex> user |> Ecto.assoc(:teams) |> all
[%App.Team{__meta__: #Ecto.Schema.Metadata<:loaded, "teams">,
  belongings: #Ecto.Association.NotLoaded<association :belongings is not loaded>,
  id: 1, inserted_at: ~N[2017-10-08 08:45:11.000000], name: "fuge1", updated_at: ~N[2017-10-08 08:45:11.000000], users: #Ecto.Association.NotLoaded<association :users is not loaded>},
 %App.Team{__meta__: #Ecto.Schema.Metadata<:loaded, "teams">,
  belongings: #Ecto.Association.NotLoaded<association :belongings is not loaded>,
  id: 2, inserted_at: ~N[2017-10-08 08:45:11.000000], name: "fuge2", updated_at: ~N[2017-10-08 08:45:11.000000], users: #Ecto.Association.NotLoaded<association :users is not loaded>}]
```
問題なしですね。

## 削除
では、最後に関連を削除してみます。

``` ex
iex> user |> preload(:belongings) |> Ecto.Changeset.change |> Ecto.Changeset.put_assoc(:belongings, []) |> update
** (RuntimeError) you are attempting to change relation :belongings of
App.User, but there is missing data.

If you are attempting to update an existing entry, please make sure
you include the entry primary key (ID) alongside the data.

If you have a relationship with many children, at least the same N
children must be given on update. By default it is not possible to
orphan embed nor associated records, attempting to do so results in
this error message.

If you don't desire the current behavior or if you are using embeds
without a primary key, it is possible to change this behaviour by
setting `:on_replace` when defining the relation. See `Ecto.Changeset`'s
section on related data for more info.

    (ecto) lib/ecto/changeset/relation.ex:176: Ecto.Changeset.Relation.on_replace/2
    (ecto) lib/ecto/changeset/relation.ex:299: Ecto.Changeset.Relation.reduce_delete_changesets/5
    (ecto) lib/ecto/changeset.ex:1092: Ecto.Changeset.put_relation/5

```

怒られました。
どうやら、アソシエーション定義時にon_replaceオプションを渡してやらないといけないようです。

というわけで、UserとTeamの両方で、
`has_many :belongings, App.UserBelonging`
に`on_replace: :delete`の記述を足してやります。

``` ex
iex> user |> preload(:belongings) |> Ecto.Changeset.change |> Ecto.Changeset.put_assoc(:belongings, []) |> update
iex> user |> Ecto.assoc(:teams) |> all
[]
```

無事、削除もできるようになりました。