Ecto.Schema.many_to_many

You're seeing just the macro many_to_many, go back to Ecto.Schema module for more information.
Link to this macro

many_to_many(name, queryable, opts \\ [])

View Source (macro)

Indicates a many-to-many association with another schema.

The association happens through a join schema or source, containing foreign keys to the associated schemas. For example, the association below:

# from MyApp.Post
many_to_many :tags, MyApp.Tag, join_through: "posts_tags"

is backed by relational databases through a join table as follows:

[Post] <-> [posts_tags] <-> [Tag]
  id   <--   post_id
              tag_id    -->  id

More information on the migration for creating such a schema is shown below.

Options

  • :join_through - Specifies the source of the associated data. It may be a string, like "posts_tags", representing the underlying storage table or an atom, like MyApp.PostTag, representing a schema. This option is required.

  • :join_keys - Specifies how the schemas are associated. It expects a keyword list with two entries, the first being how the join table should reach the current schema and the second how the join table should reach the associated schema. In the example above, it defaults to: [post_id: :id, tag_id: :id]. The keys are inflected from the schema names.

  • :on_delete - The action taken on associations when the parent record is deleted. May be :nothing (default) or :delete_all. Using this option is DISCOURAGED for most relational databases. Instead, in your migration, set references(:parent_id, on_delete: :delete_all). Opposite to the migration option, this option cannot guarantee integrity and it is only triggered for Ecto.Repo.delete/2 (and not on Ecto.Repo.delete_all/2). This option can only remove data from the join source, never the associated records, and it never cascades.

  • :on_replace - The action taken on associations when the record is replaced when casting or manipulating parent changeset. May be :raise (default), :mark_as_invalid, or :delete. :delete will only remove data from the join source, never the associated records. See Ecto.Changeset's section on related data for more info.

  • :defaults - Default values to use when building the association. It may be a keyword list of options that override the association schema or a atom/{module, function, args} that receives the struct and the owner as arguments. For example, if you set Post.many_to_many :tags, defaults: [public: true], then when using Ecto.build_assoc(post, :tags), the tag will have tag.public == true. Alternatively, you can set it to Post.many_to_many :tags, defaults: :update_tag, which will invoke Post.update_tag(tag, post), or set it to a MFA tuple such as {Mod, fun, [arg3, arg4]}, which will invoke Mod.fun(tag, post, arg3, arg4)

  • :join_defaults - The same as :defaults but it applies to the join schema instead. This option will raise if it is given and the :join_through value is not a schema.

  • :unique - When true, checks if the associated entries are unique whenever the association is cast or changed via the parent record. For instance, it would verify that a given tag cannot be attached to the same post more than once. This exists mostly as a quick check for user feedback, as it does not guarantee uniqueness at the database level. Therefore, you should also set a unique index in the database join table, such as: create unique_index(:posts_tags, [:post_id, :tag_id])

  • :where - A filter for the association. See "Filtering associations" in has_many/3

  • :join_where - A filter for the join table. See "Filtering associations" in has_many/3

  • :preload_order - Sets the default order_by of the association. It is used when the association is preloaded. For example, if you set Post.many_to_many :tags, Tag, join_through: "posts_tags", preload_order: [asc: :foo], whenever the :tags associations is preloaded, the comments will be order by the :foo field. See Ecto.Query.order_by/3 for more examples.

Using Ecto.assoc/2

One of the benefits of using many_to_many is that Ecto will avoid loading the intermediate whenever possible, making your queries more efficient. For this reason, developers should not refer to the join table of many_to_many in queries. The join table is accessible in few occasions, such as in Ecto.assoc/2. For example, if you do this:

post
|> Ecto.assoc(:tags)
|> where([t, _pt, p], p.public == t.public)

It may not work as expected because the posts_tags table may not be included in the query. You can address this problem in multiple ways. One option is to use ...:

post
|> Ecto.assoc(:tags)
|> where([t, ..., p], p.public == t.public)

Another and preferred option is to rewrite to an explicit join, which ellide the intermediate bindings as they are resolved only later on:

# keyword syntax
from t in Tag,
  join: p in assoc(t, :post), on: p.id == ^post.id

# pipe syntax
Tag
|> join([t], :inner, p in assoc(t, :post), on: p.id == ^post.id)

If you need to access the join table, then you likely want to use has_many/3 with the :through option instead.

Removing data

If you attempt to remove associated many_to_many data, Ecto will always remove data from the join schema and never from the target associations be it by setting :on_replace to :delete, :on_delete to :delete_all or by using changeset functions such as Ecto.Changeset.put_assoc/3. For example, if a Post has a many to many relationship with Tag, setting :on_delete to :delete_all will only delete entries from the "posts_tags" table in case Post is deleted.

Migration

How your migration should be structured depends on the value you pass in :join_through. If :join_through is simply a string, representing a table, you may define a table without primary keys and you must not include any further columns, as those values won't be set by Ecto:

create table(:posts_tags, primary_key: false) do
  add :post_id, references(:posts)
  add :tag_id, references(:tags)
end

However, if your :join_through is a schema, like MyApp.PostTag, your join table may be structured as any other table in your codebase, including timestamps:

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

Because :join_through contains a schema, in such cases, autogenerated values and primary keys will be automatically handled by Ecto.

Examples

defmodule Post do
  use Ecto.Schema
  schema "posts" do
    many_to_many :tags, Tag, join_through: "posts_tags"
  end
end

# Let's create a post and a tag
post = Repo.insert!(%Post{})
tag = Repo.insert!(%Tag{name: "introduction"})

# We can associate at any time post and tags together using changesets
post
|> Repo.preload(:tags) # Load existing data
|> Ecto.Changeset.change() # Build the changeset
|> Ecto.Changeset.put_assoc(:tags, [tag]) # Set the association
|> Repo.update!

# In a later moment, we may get all tags for a given post
post = Repo.get(Post, 42)
tags = Repo.all(assoc(post, :tags))

# The tags may also be preloaded on the post struct for reading
[post] = Repo.all(from(p in Post, where: p.id == 42, preload: :tags))
post.tags #=> [%Tag{...}, ...]

Join Schema Example

You may prefer to use a join schema to handle many_to_many associations. The decoupled nature of Ecto allows us to create a "join" struct which belongs_to both sides of the many to many association.

In our example, a User has and belongs to many Organizations:

defmodule MyApp.Repo.Migrations.CreateUserOrganization do
  use Ecto.Migration

  def change do
    create table(:users_organizations) do
      add :user_id, references(:users)
      add :organization_id, references(:organizations)

      timestamps()
    end
  end
end

defmodule UserOrganization do
  use Ecto.Schema

  @primary_key false
  schema "users_organizations" do
    belongs_to :user, User
    belongs_to :organization, Organization
    timestamps() # Added bonus, a join schema will also allow you to set timestamps
  end

  def changeset(struct, params \\ %{}) do
    struct
    |> Ecto.Changeset.cast(params, [:user_id, :organization_id])
    |> Ecto.Changeset.validate_required([:user_id, :organization_id])
    # Maybe do some counter caching here!
  end
end

defmodule User do
  use Ecto.Schema

  schema "users" do
    many_to_many :organizations, Organization, join_through: UserOrganization
  end
end

defmodule Organization do
  use Ecto.Schema

  schema "organizations" do
    many_to_many :users, User, join_through: UserOrganization
  end
end

# Then to create the association, pass in the ID's of an existing
# User and Organization to UserOrganization.changeset
changeset = UserOrganization.changeset(%UserOrganization{}, %{user_id: id, organization_id: id})

case Repo.insert(changeset) do
  {:ok, assoc} -> # Assoc was created!
  {:error, changeset} -> # Handle the error
end