Repository: elixir-ecto/ecto
Pull request #2417 (2018-02-12 06:40:54)

Associations Guide proofread
Read through the 👌 excellent 👌 associations guide today to update my knowledge on how to work with the different association types. I noticed a couple of minor inconsistencies in the guide, and so I've fixed them up in this PR.

I've also added the `examples/ecto_assoc` directory. It contains the code from the guide, which I think might be helpful. I did this because the "Getting Started" does the same thing with its `examples/friends` directory.

Thanks :heart:

ebert[bot]: Ebert has finished reviewing this Pull Request and has found:

* 6 possible new issues (including those that may have been commented here).

You can see more details about this review at https://ebertapp.io/github/elixir-ecto/ecto/pulls/2417.

<!-- Ebert auto generated comment -->


Patch:
diff --git a/examples/ecto_assoc/.gitignore b/examples/ecto_assoc/.gitignore
new file mode 100644
index 00000000..12179ea3
--- /dev/null
+++ b/examples/ecto_assoc/.gitignore
@@ -0,0 +1,20 @@
+# The directory Mix will write compiled artifacts to.
+/_build/
+
+# If you run "mix test --cover", coverage assets end up here.
+/cover/
+
+# The directory Mix downloads your dependencies sources to.
+/deps/
+
+# Where 3rd-party dependencies like ExDoc output generated docs.
+/doc/
+
+# Ignore .fetch files in case you like to edit your project deps locally.
+/.fetch
+
+# If the VM crashes, it generates a dump, let's ignore it too.
+erl_crash.dump
+
+# Also ignore archive artifacts (built via "mix archive.build").
+*.ez
diff --git a/examples/ecto_assoc/README.md b/examples/ecto_assoc/README.md
new file mode 100644
index 00000000..8210cba4
--- /dev/null
+++ b/examples/ecto_assoc/README.md
@@ -0,0 +1,21 @@
+# EctoAssoc
+
+**TODO: Add description**
+
+## Installation
+
+If [available in Hex](https://hex.pm/docs/publish), the package can be installed
+by adding `ecto_assoc` to your list of dependencies in `mix.exs`:
+
+```elixir
+def deps do
+  [
+    {:ecto_assoc, "~> 0.1.0"}
+  ]
+end
+```
+
+Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc)
+and published on [HexDocs](https://hexdocs.pm). Once published, the docs can
+be found at [https://hexdocs.pm/ecto_assoc](https://hexdocs.pm/ecto_assoc).
+
diff --git a/examples/ecto_assoc/config/config.exs b/examples/ecto_assoc/config/config.exs
new file mode 100644
index 00000000..1d1a8b05
--- /dev/null
+++ b/examples/ecto_assoc/config/config.exs
@@ -0,0 +1,9 @@
+use Mix.Config
+
+config :ecto_assoc, EctoAssoc.Repo,
+  adapter: Ecto.Adapters.Postgres,
+  database: "ecto_assoc_repo",
+  hostname: "localhost"
+
+config :ecto_assoc,
+  ecto_repos: [EctoAssoc.Repo]
diff --git a/examples/ecto_assoc/lib/ecto_assoc.ex b/examples/ecto_assoc/lib/ecto_assoc.ex
new file mode 100644
index 00000000..7f4a3bca
--- /dev/null
+++ b/examples/ecto_assoc/lib/ecto_assoc.ex
@@ -0,0 +1,18 @@
+defmodule EctoAssoc do
+  @moduledoc """
+  Documentation for EctoAssoc.
+  """
+
+  @doc """
+  Hello world.
+
+  ## Examples
+
+      iex> EctoAssoc.hello
+      :world
+
+  """
+  def hello do
+    :world
+  end
+end
diff --git a/examples/ecto_assoc/lib/ecto_assoc/application.ex b/examples/ecto_assoc/lib/ecto_assoc/application.ex
new file mode 100644
index 00000000..52a36bb9
--- /dev/null
+++ b/examples/ecto_assoc/lib/ecto_assoc/application.ex
@@ -0,0 +1,21 @@
+defmodule EctoAssoc.Application do
+  # See https://hexdocs.pm/elixir/Application.html
+  # for more information on OTP Applications
+  @moduledoc false
+
+  use Application
+
+  def start(_type, _args) do
+    # List all child processes to be supervised
+    children = [
+      EctoAssoc.Repo
+      # Starts a worker by calling: EctoAssoc.Worker.start_link(arg)
+      # {EctoAssoc.Worker, arg},
+    ]
+
+    # See https://hexdocs.pm/elixir/Supervisor.html
+    # for other strategies and supported options
+    opts = [strategy: :one_for_one, name: EctoAssoc.Supervisor]
+    Supervisor.start_link(children, opts)
+  end
+end
diff --git a/examples/ecto_assoc/lib/ecto_assoc/avatar.ex b/examples/ecto_assoc/lib/ecto_assoc/avatar.ex
new file mode 100644
index 00000000..2d457001
--- /dev/null
+++ b/examples/ecto_assoc/lib/ecto_assoc/avatar.ex
@@ -0,0 +1,9 @@
+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
diff --git a/examples/ecto_assoc/lib/ecto_assoc/post.ex b/examples/ecto_assoc/lib/ecto_assoc/post.ex
new file mode 100644
index 00000000..92601ef3
--- /dev/null
+++ b/examples/ecto_assoc/lib/ecto_assoc/post.ex
@@ -0,0 +1,10 @@
+defmodule EctoAssoc.Post do
+  use Ecto.Schema
+
+  schema "posts" do
+    field :header, :string
+    field :body, :string
+    belongs_to :user, EctoAssoc.User
+    many_to_many :tags, EctoAssoc.Tag, join_through: "posts_tags", on_replace: :delete
+  end
+end
diff --git a/examples/ecto_assoc/lib/ecto_assoc/repo.ex b/examples/ecto_assoc/lib/ecto_assoc/repo.ex
new file mode 100644
index 00000000..15a161ea
--- /dev/null
+++ b/examples/ecto_assoc/lib/ecto_assoc/repo.ex
@@ -0,0 +1,3 @@
+defmodule EctoAssoc.Repo do
+  use Ecto.Repo, otp_app: :ecto_assoc
+end
diff --git a/examples/ecto_assoc/lib/ecto_assoc/tag.ex b/examples/ecto_assoc/lib/ecto_assoc/tag.ex
new file mode 100644
index 00000000..5baad00a
--- /dev/null
+++ b/examples/ecto_assoc/lib/ecto_assoc/tag.ex
@@ -0,0 +1,8 @@
+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
diff --git a/examples/ecto_assoc/lib/ecto_assoc/user.ex b/examples/ecto_assoc/lib/ecto_assoc/user.ex
new file mode 100644
index 00000000..aa82deee
--- /dev/null
+++ b/examples/ecto_assoc/lib/ecto_assoc/user.ex
@@ -0,0 +1,10 @@
+defmodule EctoAssoc.User do
+  use Ecto.Schema
+
+  schema "users" do
+    field :name, :string
+    field :email, :string
+    has_one :avatar, EctoAssoc.Avatar
+    has_many :posts, EctoAssoc.Post
+  end
+end
diff --git a/examples/ecto_assoc/mix.exs b/examples/ecto_assoc/mix.exs
new file mode 100644
index 00000000..d380edd1
--- /dev/null
+++ b/examples/ecto_assoc/mix.exs
@@ -0,0 +1,29 @@
+defmodule EctoAssoc.Mixfile do
+  use Mix.Project
+
+  def project do
+    [
+      app: :ecto_assoc,
+      version: "0.1.0",
+      elixir: "~> 1.5",
+      start_permanent: Mix.env == :prod,
+      deps: deps()
+    ]
+  end
+
+  # Run "mix help compile.app" to learn about applications.
+  def application do
+    [
+      extra_applications: [:logger],
+      mod: {EctoAssoc.Application, []}
+    ]
+  end
+
+  # Run "mix help deps" to learn about dependencies.
+  defp deps do
+    [
+      {:ecto, "~> 2.0"},
+      {:postgrex, "~> 0.11"}
+    ]
+  end
+end
diff --git a/examples/ecto_assoc/mix.lock b/examples/ecto_assoc/mix.lock
new file mode 100644
index 00000000..8dcf0b08
--- /dev/null
+++ b/examples/ecto_assoc/mix.lock
@@ -0,0 +1,6 @@
+%{"connection": {:hex, :connection, "1.0.4", "a1cae72211f0eef17705aaededacac3eb30e6625b04a6117c1b2db6ace7d5976", [], [], "hexpm"},
+  "db_connection": {:hex, :db_connection, "1.1.3", "89b30ca1ef0a3b469b1c779579590688561d586694a3ce8792985d4d7e575a61", [], [{:connection, "~> 1.0.2", [hex: :connection, repo: "hexpm", optional: false]}, {:poolboy, "~> 1.5", [hex: :poolboy, repo: "hexpm", optional: true]}, {:sbroker, "~> 1.0", [hex: :sbroker, repo: "hexpm", optional: true]}], "hexpm"},
+  "decimal": {:hex, :decimal, "1.4.1", "ad9e501edf7322f122f7fc151cce7c2a0c9ada96f2b0155b8a09a795c2029770", [], [], "hexpm"},
+  "ecto": {:hex, :ecto, "2.2.8", "a4463c0928b970f2cee722cd29aaac154e866a15882c5737e0038bbfcf03ec2c", [], [{:db_connection, "~> 1.1", [hex: :db_connection, repo: "hexpm", optional: true]}, {:decimal, "~> 1.2", [hex: :decimal, repo: "hexpm", optional: false]}, {:mariaex, "~> 0.8.0", [hex: :mariaex, repo: "hexpm", optional: true]}, {:poison, "~> 2.2 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: true]}, {:poolboy, "~> 1.5", [hex: :poolboy, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.13.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:sbroker, "~> 1.0", [hex: :sbroker, repo: "hexpm", optional: true]}], "hexpm"},
+  "poolboy": {:hex, :poolboy, "1.5.1", "6b46163901cfd0a1b43d692657ed9d7e599853b3b21b95ae5ae0a777cf9b6ca8", [], [], "hexpm"},
+  "postgrex": {:hex, :postgrex, "0.13.5", "3d931aba29363e1443da167a4b12f06dcd171103c424de15e5f3fc2ba3e6d9c5", [], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 1.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: false]}], "hexpm"}}
diff --git a/examples/ecto_assoc/priv/repo/migrations/20180212061634_create_user.exs b/examples/ecto_assoc/priv/repo/migrations/20180212061634_create_user.exs
new file mode 100644
index 00000000..297457ec
--- /dev/null
+++ b/examples/ecto_assoc/priv/repo/migrations/20180212061634_create_user.exs
@@ -0,0 +1,10 @@
+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
diff --git a/examples/ecto_assoc/priv/repo/migrations/20180212061657_create_avatar.exs b/examples/ecto_assoc/priv/repo/migrations/20180212061657_create_avatar.exs
new file mode 100644
index 00000000..d19138b2
--- /dev/null
+++ b/examples/ecto_assoc/priv/repo/migrations/20180212061657_create_avatar.exs
@@ -0,0 +1,10 @@
+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
diff --git a/examples/ecto_assoc/priv/repo/migrations/20180212061741_avatar_belongs_to_user.exs b/examples/ecto_assoc/priv/repo/migrations/20180212061741_avatar_belongs_to_user.exs
new file mode 100644
index 00000000..89189eb4
--- /dev/null
+++ b/examples/ecto_assoc/priv/repo/migrations/20180212061741_avatar_belongs_to_user.exs
@@ -0,0 +1,9 @@
+defmodule EctoAssoc.Repo.Migrations.AvatarBelongsToUser do
+  use Ecto.Migration
+
+  def change do
+    alter table(:avatars) do
+      add :user_id, references(:users)
+    end
+  end
+end
diff --git a/examples/ecto_assoc/priv/repo/migrations/20180212062219_create_post.exs b/examples/ecto_assoc/priv/repo/migrations/20180212062219_create_post.exs
new file mode 100644
index 00000000..f1fe0fae
--- /dev/null
+++ b/examples/ecto_assoc/priv/repo/migrations/20180212062219_create_post.exs
@@ -0,0 +1,10 @@
+defmodule EctoAssoc.Repo.Migrations.CreatePosts do
+  use Ecto.Migration
+
+  def change do
+    create table(:posts) do
+      add :header, :string
+      add :body, :string
+    end
+  end
+end
diff --git a/examples/ecto_assoc/priv/repo/migrations/20180212062300_post_belongs_to_user.exs b/examples/ecto_assoc/priv/repo/migrations/20180212062300_post_belongs_to_user.exs
new file mode 100644
index 00000000..f1cb500b
--- /dev/null
+++ b/examples/ecto_assoc/priv/repo/migrations/20180212062300_post_belongs_to_user.exs
@@ -0,0 +1,9 @@
+defmodule EctoAssoc.Repo.Migrations.PostBelongsToUser do
+  use Ecto.Migration
+
+  def change do
+    alter table(:posts) do
+      add :user_id, references(:users)
+    end
+  end
+end
diff --git a/examples/ecto_assoc/priv/repo/migrations/20180212062851_create_tags.exs b/examples/ecto_assoc/priv/repo/migrations/20180212062851_create_tags.exs
new file mode 100644
index 00000000..b4e29f53
--- /dev/null
+++ b/examples/ecto_assoc/priv/repo/migrations/20180212062851_create_tags.exs
@@ -0,0 +1,9 @@
+defmodule EctoAssoc.Repo.Migrations.CreateTag do
+  use Ecto.Migration
+
+  def change do
+    create table(:tags) do
+      add :name, :string
+    end
+  end
+end
diff --git a/examples/ecto_assoc/priv/repo/migrations/20180212062923_create_posts_tags.exs b/examples/ecto_assoc/priv/repo/migrations/20180212062923_create_posts_tags.exs
new file mode 100644
index 00000000..89cd98b6
--- /dev/null
+++ b/examples/ecto_assoc/priv/repo/migrations/20180212062923_create_posts_tags.exs
@@ -0,0 +1,12 @@
+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
diff --git a/guides/Associations.md b/guides/Associations.md
index e6d05114..fff300f9 100644
--- a/guides/Associations.md
+++ b/guides/Associations.md
@@ -2,6 +2,10 @@
 
 This guide assumes you worked through the [Getting Started guide](Getting%20Started.md) and want to learn more about associations.
 
+If you want to see the code from this guide, you can view it [at
+ecto/examples/ecto_assoc on GitHub](https://github.com/elixir-
+lang/ecto/tree/master/examples/ecto_assoc).
+
 There are three kinds of associations:
 
   * one-to-one
@@ -189,7 +193,7 @@ defmodule EctoAssoc.Repo.Migrations.AvatarBelongsToUser do
 end
 ```
 
-This adds a `user_id` column to the DB which references an entry in the users table.
+This adds a `user_id` column to the DB which references an entry in the users table. We will need to run `mix ecto.migrate` to run this migration.
 
 For the `Avatar` we add a `belongs_to` field to the schema:
 
@@ -280,14 +284,14 @@ Let's assume we have two schemas: `User` and `Post`. The `User` schema was defin
 Let's start with the migration:
 
 ```
-mix ecto.gen.migration create_post
+mix ecto.gen.migration create_posts
 ```
 
 with the following columns:
 
 ```elixir
-# priv/repo/migrations/*_create_post.exs
-defmodule EctoAssoc.Repo.Migrations.CreatePost do
+# priv/repo/migrations/*_create_posts.exs
+defmodule EctoAssoc.Repo.Migrations.CreatePosts do
   use Ecto.Migration
 
   def change do
@@ -341,6 +345,8 @@ defmodule EctoAssoc.Repo.Migrations.PostBelongsToUser do
 end
 ```
 
+We will need to run `mix ecto.migrate` here again to run these latest migrations.
+
 For the `Post` we add a `belongs_to` field to the schema:
 
 ```elixir
@@ -355,7 +361,7 @@ defmodule EctoAssoc.Post do
 end
 ```
 
-`belongs_to` is a macro which uses a foreign key (in this case `user_id`) to make the associated schema accessible through the `Post`. The user can be accessed via `post.user`.
+`belongs_to` is a macro which uses a foreign key (in this case `user_id`) to make the associated schema accessible through the `Post`. We saw this previously for the link from avatars to their users. The user in this case can be accessed via `post.user`.
 
 For the `User` we add a `has_many` field to the schema:
 
@@ -451,14 +457,14 @@ Let's assume we have two schemas: `Post` and `Tag`. The `Post` schema was define
 Let's start with the tag migration:
 
 ```
-mix ecto.gen.migration create_tag
+mix ecto.gen.migration create_tags
 ```
 
 with the following columns:
 
 ```elixir
-# priv/repo/migrations/*create_tag.exs
-defmodule EctoAssoc.Repo.Migrations.CreateTag do
+# priv/repo/migrations/*create_tags.exs
+defmodule EctoAssoc.Repo.Migrations.CreateTags do
   use Ecto.Migration
 
   def change do
@@ -516,7 +522,11 @@ defmodule EctoAssoc.Repo.Migrations.CreatePostsTags do
 end
 ```
 
-On the DB level, this creates a new table `posts_tags` with two columns that point at the `tag_id` and `post_id`. We also create a unique index, such that the association is always unique.
+On the DB level, this creates a new table `posts_tags` with two columns that point at the `tag_id` and `post_id`. This type of table is often referred to as a "join table", as it joins two tables together.
+
+Let's run `mix ecto.migrate` to run these migrations.
+
+We also create a unique index, such that the association is always unique. There can only be a single record with the same `post_id` and `tag_id` combination in this table.
 
 For the `Post` we use the [`many_to_many`](Ecto.Schema.html#many_to_many/3) macro to associate the `Tag` through the
 new `posts_tags` table.
@@ -529,6 +539,7 @@ defmodule EctoAssoc.Post do
   schema "posts" do
     field :header, :string
     field :body, :string
+    belongs_to :user, EctoAssoc.User
     # the following line was added
     many_to_many :tags, EctoAssoc.Tag, join_through: "posts_tags"
   end
@@ -553,6 +564,18 @@ end
 
 ### Persistence
 
+Start iex:
+
+```
+$ iex -S mix
+```
+
+For convenience we alias some modules:
+
+```elixir
+iex> alias EctoAssoc.{Repo, Post, Tag}
+```
+
 Let's create some tags:
 
 ```elixir
@@ -587,7 +610,7 @@ Ok, but tag and post are not associated, yet. We might expect, as done in `one-t
 Another option is to use Ecto changesets, which provide many conveniences for dealing with *changes*. For example:
 
 ```elixir
-iex> post_changeset = Ecto.Changeset.change(post)
+iex> post_changeset = post |> Repo.preload(:tags) |> Ecto.Changeset.change
 iex> post_with_tags = Ecto.Changeset.put_assoc(post_changeset, :tags, [clickbait_tag, misc_tag])
 iex> post = Repo.update!(post_with_tags)
 %EctoAssoc.Post{__meta__: #Ecto.Schema.Metadata<:loaded, "posts">,
@@ -600,6 +623,8 @@ iex> post = Repo.update!(post_with_tags)
    posts: #Ecto.Association.NotLoaded<association :posts is not loaded>}]}
 ```
 
+Note that in this example that we need to `Repo.preload` the `tags` association before we can change it.
+
 Let's examine the post:
 
 ```elixir
@@ -645,7 +670,7 @@ iex> tag = Repo.get(Tag, 1) |> Repo.preload(:posts)
 The advantage of using [`Ecto.Changeset`](Ecto.Changeset.html) is that it is responsible for tracking the changes between your data structures and the associated data. For example, if you want to remove the "clickbait" tag from the post, one way to do so is by calling [`Ecto.Changeset.put_assoc/3`](Ecto.Changeset.html#put_assoc/4) once more but without the "clickbait" tag.  This will not work right now, because the `:on_replace` option for the `many_to_many` relationship defaults to `:raise`.  Go ahead and try it.  When you try to call `put_assoc`, a runtime error will be raised:
 
 ```elixir
-iex> post_changeset = Ecto.Changeset.change(post)
+iex> post_changeset = post |> Repo.preload(:tags) |> Ecto.Changeset.change
 iex> post_with_tags = Ecto.Changeset.put_assoc(post_changeset, :tags, [misc_tag])
 ** (RuntimeError) you are attempting to change relation :tags of
 EctoAssoc.Post but the `:on_replace` option of
@@ -694,7 +719,7 @@ On the other hand, it probably *doesn't* make much sense to be able to remove re
 With the `:on_replace` option changed, Ecto will compare the data you gave with the tags currently in the post and conclude the association between the post and the "clickbait" tag must be removed, as follows:
 
 ```elixir
-iex> post_changeset = Ecto.Changeset.change(post)
+iex> post_changeset = post |> Repo.preload(:tags) |> Ecto.Changeset.change
 iex> post_with_tags = Ecto.Changeset.put_assoc(post_changeset, :tags, [misc_tag])
 iex> post = Repo.update!(post_with_tags)
 ```


Test patch:
diff --git a/examples/ecto_assoc/test/ecto_assoc_test.exs b/examples/ecto_assoc/test/ecto_assoc_test.exs
new file mode 100644
index 00000000..33353be6
--- /dev/null
+++ b/examples/ecto_assoc/test/ecto_assoc_test.exs
@@ -0,0 +1,8 @@
+defmodule EctoAssocTest do
+  use ExUnit.Case
+  doctest EctoAssoc
+
+  test "greets the world" do
+    assert EctoAssoc.hello() == :world
+  end
+end
diff --git a/examples/ecto_assoc/test/test_helper.exs b/examples/ecto_assoc/test/test_helper.exs
new file mode 100644
index 00000000..869559e7
--- /dev/null
+++ b/examples/ecto_assoc/test/test_helper.exs
@@ -0,0 +1,1 @@
+ExUnit.start()