LoginSignup
9
6

More than 1 year has passed since last update.

Elixir Ecto Association

Last updated at Posted at 2018-09-08

(追記)本記事は2022/10/23 に更新しました

Ecto にはテーブル同士を関連付ける Ecto Association の機能があります。関連付けには以下の3つのタイプが考えられます。

  • one-to-one
  • one-to-many
  • many-to-many

Ecto Association は以下の公式サイトで説明されています。今回はこのガイドを実際に実行し、補足説明を加えていきたいと思います。
Ecto Association Guide

0.Ectoの設定

まずは Ecto の設定を、実際にプロジェクトを作成しながら行っていきます。mix で ecto_assoc というプロジェクトを作成します。オプションの --sup はこのアプリケーションが supervision tree を持っていることを示しています。

mix new ecto_assoc --sup

プロジェクトディレクトリに移ります。

cd ecto_assoc

次に必要なパッケージをインストールします。以下のファイルのdepsを変更します。postgrex はPostgreSQLのドライバです。

mix.exs
defp deps do
  [{:ecto, "~> 2.0"},
   {:postgrex, "~> 0.11"}]
end

インストールします。

mix deps.get

まず以下のコマンドで **Repo (lib/ecto_assoc/repo.ex)**を作成します。

lib/ecto_assoc/repo.ex
mix ecto.gen.repo -r EctoAssoc.Repo

repo.ex を確認します。EctoAssoc.Repo はデータベースへの query に使われます。ここではEcto.Repo を使うことが宣言されています。otp_app はどのアプリがデータベースへアクセスしているかを教えています。ここでは :ecto_assoc となっているのでこのプロジェクトの config.exs を参照し、その接続情報をもとにデータベースにアクセスします。

lib/ecto_assoc/repo.ex
defmodule EctoAssoc.Repo do
  use Ecto.Repo, otp_app: :ecto_assoc
end

さらに config/config.exs の DB 設定を確認します。上で指定した :ecto_assoc の設定です。必要であれば、username/password などを自分の環境に合わせます。最後の行は、アプリケーションに対して repo について教えます。

config/config.exs
config :ecto_assoc, EctoAssoc.Repo,
  adapter: Ecto.Adapters.Postgres,
  database: "ecto_assoc_repo",
  username: "user",
  password: "pass",
  hostname: "localhost"

config :ecto_assoc, ecto_repos: [EctoAssoc.Repo]

次に lib/ecto_assoc/application.ex を編集します。EctoAssoc.Repoapplicationsupervision tree の中の supervisor としてセットします。これによって、Ecto process を起動して、アプリのqueryを受け取り実行できるようになります。

lib/ecto_assoc/application.ex
#
  import Supervisor.Spec
  def start(_type, _args) do
    
    children = [
      supervisor(EctoAssoc.Repo, [])
    ]
#

最後に次のコマンドで DB を作成します。

 mix ecto.create

1.One-to-one (User-Avatar)

まず One-to-oneEcto Association を見ていきます。まず usersテーブルavatarsテーブルを作成します。次に両テーブルに関連性を与えます。最後に実際の実行結果を確認したいと思います。

1-1.User

usersテーブルを作成する migrations を作ります。migrations とは Elixir の記述だけでデータベースのテーブル操作をしてしまおうとするものです。

mix ecto.gen.migration create_user

以下のような空の migrations が作成されます。

priv/repo/migrations/20221023071420_create_user.exs
efmodule EctoAssoc.Repo.Migrations.CreateUser do
  use Ecto.Migration

  def change do

  end
end

これを以下のように修正します。

priv/repo/migrations/20221023071420_create_user.exs
defmodule EctoAssoc.Repo.Migrations.CreateUser do
  use Ecto.Migration

  def change do
    create table(:users) do
      add :name, :string
      add :email, :string
    end
  end
end

User schemaを作成します

lib/ecto_assoc/user.ex
defmodule EctoAssoc.User do
  use Ecto.Schema

  schema "users" do
    field :name, :string
    field :email, :string
  end
end

1-2.Avatar

avatars テーブルを作成する migrations を作ります。

mix ecto.gen.migration create_avatar

作成されたmigrationsを以下のように修正します。

priv/repo/migrations/20221023072408_create_avatar.exs
defmodule EctoAssoc.Repo.Migrations.CreateAvatar do
  use Ecto.Migration

  def change do
    create table(:avatars) do
      add :nick_name, :string
      add :pic_url, :string
    end
  end
end

Avatar schemaを作成します

lib/ecto_assoc/avatar.ex
defmodule EctoAssoc.Avatar do
  use Ecto.Schema

  schema "avatars" do
    field :nick_name, :string
    field :pic_url, :string
  end
end

以上で準備が整いましたので、DBとテーブルを作成したいと思います。

mix ecto.create
mix ecto.migrate

1-3.Associationsを追加

avatars テーブルを変更する migration を作成します。

mix ecto.gen.migration avatar_belongs_to_user

どのように変更するかを記述します。ここでは user_idusers テーブルを参照する外部キーとして追加するように変更します。

priv/repo/migrations/20221023073258_avatar_belongs_to_user.exs
defmodule EctoAssoc.Repo.Migrations.CreateAvatar do
  use Ecto.Migration

  def change do
    alter table(:avatars) do
      add :user_id, references(:users)
    end
  end
end

Avatar schema を修正(追加)します。ここでは belongs_to field を追加します。これにより avatar.userusers テーブルにアクセスできるようになります。

lib/ecto_assoc/avatar.ex
defmodule EctoAssoc.Avatar do
  use Ecto.Schema

  schema "avatars" do
    field :nick_name, :string
    field :pic_url, :string
    belongs_to :user, EctoAssoc.User  # 追加
  end
end

User schema を修正(追加)します。これによる usersテーブルの変更はありませんが、user.avatar によって avatars テーブルへアクセスできるようになります。

lib/ecto_assoc/user.ex
defmodule EctoAssoc.User do
  use Ecto.Schema

  schema "users" do
    field :name, :string
    field :email, :string
    has_one :avatar, EctoAssoc.Avatar  # 追加
  end
end

migrate を実行して、実際にテーブルを変更します。

mix ecto.migrate

1-4.実行結果の確認

iex -S mix

### Repo, User, Avatarモジュール名の短縮でアクセス可能にします。
iex(1)> alias EctoAssoc.{Repo, User, Avatar}
[EctoAssoc.Repo, EctoAssoc.User, EctoAssoc.Avatar]


### Userを、Avater無しで、作成します。
iex(2)> user = %User{name: "John Doe", email: "john.doe@example.com"}
%EctoAssoc.User{
  __meta__: #Ecto.Schema.Metadata<:built, "users">,
  avatar: #Ecto.Association.NotLoaded<association :avatar is not loaded>,
  email: "john.doe@example.com",
  id: nil,
  name: "John Doe"
}

### Userをテーブルに挿入します。insert!(ビックリマーク)は例外を発生します。
### この時点で初めてUserはテーブルで実体化され、idが決まります。
iex(3)> user = Repo.insert!(user)
%EctoAssoc.User{
  __meta__: #Ecto.Schema.Metadata<:loaded, "users">,
  avatar: #Ecto.Association.NotLoaded<association :avatar is not loaded>,
  email: "john.doe@example.com",
  id: 1,
  name: "John Doe"
}


### Avaterを作成します。
iex(4)> avatar = %Avatar{nick_name: "Elixir", pic_url: "http://elixir-lang.org/images/logo.png"}
%EctoAssoc.Avatar{
  __meta__: #Ecto.Schema.Metadata<:built, "avatars">,
  id: nil,
  nick_name: "Elixir",
  pic_url: "http://elixir-lang.org/images/logo.png",
  user: #Ecto.Association.NotLoaded<association :user is not loaded>,
  user_id: nil
}


### Userを、Avater付きで、作成します。
### schemaで追加した「has_one :avatar」に値を指定する形です。
iex(5)> user = %User{name: "John Doe", email: "john.doe@example.com", avatar: avatar}
%EctoAssoc.User{
  __meta__: #Ecto.Schema.Metadata<:built, "users">,
  avatar: %EctoAssoc.Avatar{
    __meta__: #Ecto.Schema.Metadata<:built, "avatars">,
    id: nil,
    nick_name: "Elixir",
    pic_url: "http://elixir-lang.org/images/logo.png",
    user: #Ecto.Association.NotLoaded<association :user is not loaded>,
    user_id: nil
  },
  email: "john.doe@example.com",
  id: nil,
  name: "John Doe"
}


### Userをテーブルに挿入します。
### Userがid=2で作成され、その中のid=1のAVaterがuser_id=2で作られているように見えるのがわかります。
iex(6)>  user = Repo.insert!(user)
%EctoAssoc.User{
  __meta__: #Ecto.Schema.Metadata<:loaded, "users">,
  avatar: %EctoAssoc.Avatar{
    __meta__: #Ecto.Schema.Metadata<:loaded, "avatars">,
    id: 1,
    nick_name: "Elixir",
    pic_url: "http://elixir-lang.org/images/logo.png",
    user: #Ecto.Association.NotLoaded<association :user is not loaded>,
    user_id: 2
  },
  email: "john.doe@example.com",
  id: 2,
  name: "John Doe"
}


### Userテーブルのエントリーを表示します。
### %User structの association(has_one :avatar)である :avatar をpreloadします。
### id=2のUserの中に埋め込まれる形でAvaterがpreloadされます。
iex(7)> Repo.all(User) |> Repo.preload(:avatar)
[
  %EctoAssoc.User{
    __meta__: #Ecto.Schema.Metadata<:loaded, "users">,
    avatar: nil,
    email: "john.doe@example.com",
    id: 1,
    name: "John Doe"
  },
  %EctoAssoc.User{
    __meta__: #Ecto.Schema.Metadata<:loaded, "users">,
    avatar: %EctoAssoc.Avatar{
      __meta__: #Ecto.Schema.Metadata<:loaded, "avatars">,
      id: 1,
      nick_name: "Elixir",
      pic_url: "http://elixir-lang.org/images/logo.png",
      user: #Ecto.Association.NotLoaded<association :user is not loaded>,
      user_id: 2
    },
    email: "john.doe@example.com",
    id: 2,
    name: "John Doe"
  }
]

2.One-to-many (User-Post)

 次に One-to-manyEcto Association を見ていきます。まず usersテーブルpostsテーブルを作成します。usersテーブルは前のものを再利用しますので、実際作るのはpostsのみになります。

2-1.Post

posts テーブルを作成する migrations を作ります。

mix ecto.gen.migration create_post

作成された migrations を以下のように修正します。

priv/repo/migrations/20221023092809_create_post.exs
defmodule EctoAssoc.Repo.Migrations.CreatePost do
  use Ecto.Migration

  def change do
    create table(:posts) do
      add :header, :string
      add :body, :string
    end
  end
end

Post schema を作成します

lib/ecto_assoc/post.ex
defmodule EctoAssoc.Post do
  use Ecto.Schema

  schema "posts" do
    field :header, :string
    field :body, :string
  end
end

以上で準備が整いましたので、テーブルを作成したいと思います。

mix ecto.migrate

2-2.Associationsを追加

postsテーブルを変更する migration を作成します。

mix ecto.gen.migration post_belongs_to_user

どのように変更するかを記述します。ここでは user_idusers テーブルを参照する外部キーとして追加するように変更します。

priv/repo/migrations/20221023093443_post_belongs_to_user.exs
defmodule EctoAssoc.Repo.Migrations.PostBelongsToUser do
  use Ecto.Migration

  def change do
    alter table(:posts) do
      add :user_id, references(:users)
    end
  end
end

Post schema を修正(追加)します。ここでは belongs_to field を追加します。これにより post.userusers テーブルにアクセスできるようになります。

lib/ecto_assoc/post.ex
defmodule EctoAssoc.Post do
  use Ecto.Schema

  schema "posts" do
    field :header, :string
    field :body, :string
    belongs_to :user, EctoAssoc.User  # 追加
  end
end

User schema を修正(追加)します。これによる usersテーブルの変更はありませんが、user.posts によって posts テーブルへアクセスできるようになります。

lib/ecto_assoc/user.ex
defmodule EctoAssoc.User do
  use Ecto.Schema

  schema "users" do
    field :name, :string
    field :email, :string
    has_many :posts, EctoAssoc.Post  # 追加
  end
end

migrate を実行して、実際にテーブルを変更します。

mix ecto.migrate

2-3.【オマケ】psqlコマンドでテーブルを確認

 ここまでのDBの様子を、psqlコマンドで確認しておきます。Ecto.Repoを通さずに、生のテーブルも確認しておきたいと思います。

### psqlを起動します。
sudo -u postgres psql

### 以下のコマンドでDB名 ecto_assoc_repo を確認します。
postgres=# \l

### DB ecto_assoc_repo に接続します。
postgres=# \c ecto_assoc_repo

### テーブル一覧を表示します。
ecto_assoc_repo=# \z
                                  Access privileges
 Schema |       Name        |   Type   | Access privileges | Column access privi
leges
--------+-------------------+----------+-------------------+--------------------
------
 public | avatars           | table    |                   |
 public | avatars_id_seq    | sequence |                   |
 public | posts             | table    |                   |
 public | posts_id_seq      | sequence |                   |
 public | schema_migrations | table    |                   |
 public | users             | table    |                   |
 public | users_id_seq      | sequence |                   |
(7 rows)


### postsテーブルの詳細を表示します。
ecto_assoc_repo=# \d posts
                                 Table "public.posts"
 Column  |          Type          |                     Modifiers

---------+------------------------+---------------------------------------------
-------
 id      | bigint                 | not null default nextval('posts_id_seq'::reg
class)
 header  | character varying(255) |
 body    | character varying(255) |
 user_id | bigint                 |
Indexes:
    "posts_pkey" PRIMARY KEY, btree (id)
Foreign-key constraints:
    "posts_user_id_fkey" FOREIGN KEY (user_id) REFERENCES users(id)

2-4.実行結果の確認

iex -S mix

### Repo, User, Postモジュール名の短縮でアクセス可能にします。
iex(1)> alias EctoAssoc.{Repo, User, Post}
[EctoAssoc.Repo, EctoAssoc.User, EctoAssoc.Post]


### Userを、Post無しで、作成します。
iex(2)> user = %User{name: "John Doe", email: "john.doe@example.com"}
%EctoAssoc.User{
  __meta__: #Ecto.Schema.Metadata<:built, "users">,
  email: "john.doe@example.com",
  id: nil,
  name: "John Doe",
  posts: #Ecto.Association.NotLoaded<association :posts is not loaded>
}


### Userをテーブルに挿入します。insert!(ビックリマーク)は例外を発生します。
### この時点で初めてUserはテーブルで実体化され、idが決まります。
iex(3)> user = Repo.insert!(user)
%EctoAssoc.User{
  __meta__: #Ecto.Schema.Metadata<:loaded, "users">,
  email: "john.doe@example.com",
  id: 1,
  name: "John Doe",
  posts: #Ecto.Association.NotLoaded<association :posts is not loaded>
}


### postを、Userに関連付ける形で、作成します。Ecto.build_assoc/3 を使います。
### schemaで追加した「has_many :posts」に値を指定する形です。
### もちろん one_to_one で行った方法でも同じことが可能です。
iex(4)> post = Ecto.build_assoc(user, :posts, %{header: "Clickbait header", body: "No real content"})
%EctoAssoc.Post{
  __meta__: #Ecto.Schema.Metadata<:built, "posts">,
  body: "No real content",
  header: "Clickbait header",
  id: nil,
  user: #Ecto.Association.NotLoaded<association :user is not loaded>,
  user_id: 1
}


### postをテーブルに挿入します。insert!(ビックリマーク)は例外を発生します。
### この時点で初めてpostはテーブルで実体化され、idが決まります。user_idが関連付けられます。
iex(5)> Repo.insert!(post)
%EctoAssoc.Post{
  __meta__: #Ecto.Schema.Metadata<:loaded, "posts">,
  body: "No real content",
  header: "Clickbait header",
  id: 1,
  user: #Ecto.Association.NotLoaded<association :user is not loaded>,
  user_id: 1
}


### 2つめのpostを、Userに関連付ける形で、作成します。
### schemaで追加した「has_many :posts」に値を指定する形です。
iex(6)> post = Ecto.build_assoc(user, :posts, %{header: "5 ways to improve your Ecto", body: "Add url of this tutorial"})
%EctoAssoc.Post{
  __meta__: #Ecto.Schema.Metadata<:built, "posts">,
  body: "Add url of this tutorial",
  header: "5 ways to improve your Ecto",
  id: nil,
  user: #Ecto.Association.NotLoaded<association :user is not loaded>,
  user_id: 1
}


### 2つめのpostをテーブルに挿入します。
### この時点で初めてpostはテーブルで実体化され、idが決まります。user_idが関連付けられます。
iex(7)> Repo.insert!(post)
%EctoAssoc.Post{
  __meta__: #Ecto.Schema.Metadata<:loaded, "posts">,
  body: "Add url of this tutorial",
  header: "5 ways to improve your Ecto",
  id: 2,
  user: #Ecto.Association.NotLoaded<association :user is not loaded>,
  user_id: 1
}


### Userテーブルのエントリーを表示します。
### %User structの association(has_many :posts)である :posts をpreloadします。
### :postsは必要に応じて自動的にlazy-loadされることは無く、明示的にpreloadする必要があります。
### id=1のUserの中にpostsが埋め込まれる形になります。
iex(8)> Repo.get(User, user.id) |> Repo.preload(:posts)
%EctoAssoc.User{
  __meta__: #Ecto.Schema.Metadata<:loaded, "users">,
  email: "john.doe@example.com",
  id: 1,
  name: "John Doe",
  posts: [
    %EctoAssoc.Post{
      __meta__: #Ecto.Schema.Metadata<:loaded, "posts">,
      body: "No real content",
      header: "Clickbait header",
      id: 1,
      user: #Ecto.Association.NotLoaded<association :user is not loaded>,
      user_id: 1
    },
    %EctoAssoc.Post{
      __meta__: #Ecto.Schema.Metadata<:loaded, "posts">,
      body: "Add url of this tutorial",
      header: "5 ways to improve your Ecto",
      id: 2,
      user: #Ecto.Association.NotLoaded<association :user is not loaded>,
      user_id: 1
    }
  ]
}
iex(9)>

3.Many-to-many (Post-Tag)

 次に Many-to-manyEcto Association を見ていきます。まずpostsテーブルtagsテーブルを作成します。postsテーブルは前のものを再利用しますので、実際作るのはtagsのみになります。

3-1.Tag

tags テーブルを作成する migrations を作ります。

mix ecto.gen.migration create_tag

作成された migrations を以下のように修正します。

priv/repo/migrations/20180908055544_create_tag.exs
defmodule EctoAssoc.Repo.Migrations.CreateTag do
  use Ecto.Migration

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

Tag schema を作成します

lib/ecto_assoc/tag.ex
defmodule EctoAssoc.Tag do
  use Ecto.Schema

  schema "tags" do
    field :name, :string
  end
end

以上で準備が整いましたので、テーブルを作成したいと思います。

mix ecto.migrate

3-2.Associationsを追加

Many-to-many の関係を記述するためには、これまでのように既存のテーブルを変更するのではなく、新しいテーブル posts_tags を追加します。posts_tagsテーブルを作成するmigrationを作成します。

mix ecto.gen.migration create_posts_tags

どのように変更するかを記述します。

priv/repo/migrations/20180908060734_create_posts_tags.exs
defmodule EctoAssoc.Repo.Migrations.CreatePostsTags do
  use Ecto.Migration

  def change do
    create table(:posts_tags) do
      add :tag_id, references(:tags)
      add :post_id, references(:posts)
    end

    create unique_index(:posts_tags, [:tag_id, :post_id])
  end
end

Post schema を修正(追加)します。

lib/ecto_assoc/post.ex
defmodule EctoAssoc.Post do
  use Ecto.Schema

  schema "posts" do
    field :header, :string
    field :body, :string
    many_to_many :tags, EctoAssoc.Tag, join_through: "posts_tags"  # 追加
  end
end

Tag schema を修正(追加)します。

lib/ecto_assoc/tag.ex
defmodule EctoAssoc.Tag do
  use Ecto.Schema

  schema "tags" do
    field :name, :string
    many_to_many :posts, EctoAssoc.Post, join_through: "posts_tags" # 追加
  end
end

migrate を実行して、実際にテーブルを追加します。

mix ecto.migrate

##3-3.実行結果の確認

iex -S mix

### Repo, Post、Tagモジュール名の短縮でアクセス可能にします。
iex(1)> alias EctoAssoc.{Repo, Tag, Post}
[EctoAssoc.Repo, EctoAssoc.Tag, EctoAssoc.Post]

まずTagとPostを作ります。

### Tagをテーブルに挿入します。 3つのtag "clickbait"と"misc"、"ecto"を挿入します。
iex(2)> clickbait_tag = Repo.insert! %Tag{name: "clickbait"}
%EctoAssoc.Tag{
  __meta__: #Ecto.Schema.Metadata<:loaded, "tags">,
  id: 1,
  name: "clickbait",
  posts: #Ecto.Association.NotLoaded<association :posts is not loaded>
}

iex(3)> misc_tag = Repo.insert! %Tag{name: "misc"}
%EctoAssoc.Tag{
  __meta__: #Ecto.Schema.Metadata<:loaded, "tags">,
  id: 2,
  name: "misc",
  posts: #Ecto.Association.NotLoaded<association :posts is not loaded>
}

iex(4)> ecto_tag = Repo.insert! %Tag{name: "ecto"}
%EctoAssoc.Tag{
  __meta__: #Ecto.Schema.Metadata<:loaded, "tags">,
  id: 3,
  name: "ecto",
  posts: #Ecto.Association.NotLoaded<association :posts is not loaded>
}


### Postを作成します。
iex(5)> post = %Post{header: "Clickbait header", body: "No real content"}
%EctoAssoc.Post{
  __meta__: #Ecto.Schema.Metadata<:built, "posts">,
  body: "No real content",
  header: "Clickbait header",
  id: nil,
  tags: #Ecto.Association.NotLoaded<association :tags is not loaded>
}


### postをテーブルに挿入します。
iex(6)> post = Repo.insert!(post)
%EctoAssoc.Post{
  __meta__: #Ecto.Schema.Metadata<:loaded, "posts">,
  body: "No real content",
  header: "Clickbait header",
  id: 1,
  tags: #Ecto.Association.NotLoaded<association :tags is not loaded>
}

以上でTagとPostを作りました。しかしAssociationsは作成していません。psqlで現状を確認してみましょう。

ecto_assoc_repo=# select * from tags;
 id |   name
----+-----------
  1 | clickbait
  2 | misc
  3 | ecto
(3 rows)

ecto_assoc_repo=# select * from posts;
 id |      header      |      body       | user_id
----+------------------+-----------------+---------
  1 | Clickbait header | No real content |
(1 row)

ecto_assoc_repo=# select * from posts_tags; # Associationsは空です。
 id | tag_id | post_id
----+--------+---------
(0 rows)

それではAssociationsを作成していきましょう。Ecto.Changesetを使いますので少し複雑になります。Ecto.Changesetを使うメリットは関連付けられたデータの変更に対して責任を持ってもらえることです。

### postをChangesetでラッピングします。
iex(7)> post_changeset = post |> Repo.preload(:tags) |> Ecto.Changeset.change
#Ecto.Changeset<action: nil, changes: %{}, errors: [], data: #EctoAssoc.Post<>,
 valid?: true>


### put_assoc は association を change として changeset の中に入れます。
### ここでassociationは以下のPost schemaを指します。
### Post schema : many_to_many :tags, EctoAssoc.Tag, join_through: "posts_tags"
iex(8)> post_with_tags = Ecto.Changeset.put_assoc(post_changeset, :tags, [clickbait_tag, misc_tag])
#Ecto.Changeset<
  action: nil,
  changes: %{
    tags: [
      #Ecto.Changeset<action: :update, changes: %{}, errors: [],
       data: #EctoAssoc.Tag<>, valid?: true>,
      #Ecto.Changeset<action: :update, changes: %{}, errors: [],
       data: #EctoAssoc.Tag<>, valid?: true>
    ]
  },
  errors: [],
  data: #EctoAssoc.Post<>,
  valid?: true
>

さて現状でもposts_tagsテーブルは空ですが、以下のコマンドでposts_tagsが更新されます。

iex(9)> post = Repo.update!(post_with_tags)
%EctoAssoc.Post{
  __meta__: #Ecto.Schema.Metadata<:loaded, "posts">,
  body: "No real content",
  header: "Clickbait header",
  id: 1,
  tags: [
    %EctoAssoc.Tag{
      __meta__: #Ecto.Schema.Metadata<:loaded, "tags">,
      id: 1,
      name: "clickbait",
      posts: #Ecto.Association.NotLoaded<association :posts is not loaded>
    },
    %EctoAssoc.Tag{
      __meta__: #Ecto.Schema.Metadata<:loaded, "tags">,
      id: 2,
      name: "misc",
      posts: #Ecto.Association.NotLoaded<association :posts is not loaded>
    }
  ]
}

posts_tagsテーブルを確認すると確かにエントリーがあります。つまりPost schemaのmany_to_manyの定義から、posts_tagsテーブルにpost_id=1の2エントリー (tag_id=1, post_id=1)と(tag_id=2, post_id=1)が挿入されたのを確認できます。

ecto_assoc_repo=# select * from posts_tags;
 id | tag_id | post_id
----+--------+---------
  1 |      1 |       1
  2 |      2 |       1
(2 rows)

それではMany-manyの関係性を実際に確かめてみましょう。

iex(10)>  post = Repo.get(Post, post.id) |> Repo.preload(:tags)
%EctoAssoc.Post{
  __meta__: #Ecto.Schema.Metadata<:loaded, "posts">,
  body: "No real content",
  header: "Clickbait header",
  id: 1,
  tags: [
    %EctoAssoc.Tag{
      __meta__: #Ecto.Schema.Metadata<:loaded, "tags">,
      id: 1,
      name: "clickbait",
      posts: #Ecto.Association.NotLoaded<association :posts is not loaded>
    },
    %EctoAssoc.Tag{
      __meta__: #Ecto.Schema.Metadata<:loaded, "tags">,
      id: 2,
      name: "misc",
      posts: #Ecto.Association.NotLoaded<association :posts is not loaded>
    }
  ]
}


iex(11)> post.header
"Clickbait header"
iex(12)> post.body
"No real content"
iex(13)> post.tags
[
  %EctoAssoc.Tag{
    __meta__: #Ecto.Schema.Metadata<:loaded, "tags">,
    id: 1,
    name: "clickbait",
    posts: #Ecto.Association.NotLoaded<association :posts is not loaded>
  },
  %EctoAssoc.Tag{
    __meta__: #Ecto.Schema.Metadata<:loaded, "tags">,
    id: 2,
    name: "misc",
    posts: #Ecto.Association.NotLoaded<association :posts is not loaded>
  }
]



iex(14)>  Enum.map(post.tags, & &1.name)
["clickbait", "misc"]

逆方向も確かめておきます。

iex(15)> tag = Repo.get(Tag, 1) |> Repo.preload(:posts)

%EctoAssoc.Tag{
  __meta__: #Ecto.Schema.Metadata<:loaded, "tags">,
  id: 1,
  name: "clickbait",
  posts: [
    %EctoAssoc.Post{
      __meta__: #Ecto.Schema.Metadata<:loaded, "posts">,
      body: "No real content",
      header: "Clickbait header",
      id: 1,
      tags: #Ecto.Association.NotLoaded<association :tags is not loaded>
    }
  ]
}

以上で終わりますが、最後にオマケです。

4.【オマケ】ecto関連のコマンド

開発時は、以下のようにしてDBをクリアして、再度作り直すことができます。

mix ecto.drop
mix ecto.create
mix ecto.migrate

mix help でecto関連のコマンドを確認できます。英語で出力されていますが、まあ仕方ないですね。

# mix help | grep ecto
mix ecto               # Prints Ecto help information
mix ecto.create        # Creates the repository storage
mix ecto.drop          # Drops the repository storage
mix ecto.dump          # Dumps the repository database structure
mix ecto.gen.migration # Generates a new migration for the repo
mix ecto.gen.repo      # Generates a new repository
mix ecto.load          # Loads previously dumped database structure
mix ecto.migrate       # Runs the repository migrations
mix ecto.migrations    # Displays the repository migration status
mix ecto.rollback      # Rolls back the repository migrations
mix phx.new.ecto       # Creates a new Ecto project within an umbrella project

以上です。

■ Elixir/Phoenixの基礎についてまとめた過去記事
Elixir Ecto チュートリアル - Qiita
Elixir Ecto のまとめ - Qiita
Elixir Ecto Association - Qiita
Phoenix1.6の基本的な仕組み - Qiita

9
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
6