Ecto.Schema.has_many

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

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

View Source (macro)

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

The current schema has zero or more records of the other schema. The other schema often has a belongs_to field with the reverse association.

Options

  • :foreign_key - Sets the foreign key, this should map to a field on the other schema, defaults to the underscored name of the current schema suffixed by _id

  • :references - Sets the key on the current schema to be used for the association, defaults to the primary key on the schema

  • :through - Allow this association to be defined in terms of existing associations. Read the section on :through associations for more info

  • :on_delete - The action taken on associations when parent record is deleted. May be :nothing (default), :nilify_all and :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) and it never cascades. If posts has many comments, which has many tags, and you delete a post, only comments will be deleted. If your database does not support references, cascading can be manually implemented by using Ecto.Multi or Ecto.Changeset.prepare_changes/2.

  • :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, :nilify, or :delete. See Ecto.Changeset's section about ":on_replace" 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.has_many :comments, defaults: [public: true], then when using Ecto.build_assoc(post, :comments), the comment will have comment.public == true. Alternatively, you can set it to Post.has_many :comments, defaults: :update_comment, which will invoke Post.update_comment(comment, post), or set it to a MFA tuple such as {Mod, fun, [arg3, arg4]}, which will invoke Mod.fun(comment, post, arg3, arg4)

  • :where - A filter for the association. See "Filtering associations" below. It does not apply to :through associations.

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

Examples

defmodule Post do
  use Ecto.Schema
  schema "posts" do
    has_many :comments, Comment
  end
end

# Get all comments for a given post
post = Repo.get(Post, 42)
comments = Repo.all assoc(post, :comments)

# The comments can come preloaded on the post struct
[post] = Repo.all(from(p in Post, where: p.id == 42, preload: :comments))
post.comments #=> [%Comment{...}, ...]

has_many can be used to define hierarchical relationships within a single schema, for example threaded comments.

defmodule Comment do
  use Ecto.Schema
  schema "comments" do
    field :content, :string
    field :parent_id, :integer
    belongs_to :parent, Comment, foreign_key: :parent_id, references: :id, define_field: false
    has_many :children, Comment, foreign_key: :parent_id, references: :id
  end
end

Filtering associations

It is possible to specify a :where option that will filter the records returned by the association. Querying, joining or preloading the association will use the given conditions as shown next:

defmodule Post do
  use Ecto.Schema

  schema "posts" do
    has_many :public_comments, Comment,
      where: [public: true]
  end
end

The :where option expects a keyword list where the key is an atom representing the field and the value is either:

  • nil - which specifies the field must be nil
  • {:not, nil} - which specifies the field must not be nil
  • {:in, list} - which specifies the field must be one of the values in a list
  • {:fragment, expr} - which specifies a fragment string as the filter (see Ecto.Query.API.fragment/1) with the field's value given to it as the only argument
  • or any other value which the field is compared directly against

Note the values above are distinctly different from the values you would pass to where when building a query. For example, if you attempt to build a query such as

from Post, where: [id: nil]

it will emit an error. This is because queries can be built dynamically, and therefore passing nil can lead to security errors. However, the :where values for an association are given at compile-time, which is less dynamic and cannot leverage the full power of Ecto queries, which explains why they have different APIs.

Important! Please use this feature only when strictly necessary, otherwise it is very easy to end-up with large schemas with dozens of different associations polluting your schema and affecting your application performance. For instance, if you are using associations only for different querying purposes, then it is preferable to build and compose queries. For instance, instead of having two associations, one for comments and another for deleted comments, you might have a single comments association and filter it instead:

posts
|> Ecto.assoc(:comments)
|> Comment.deleted()

Or when preloading:

from posts, preload: [comments: ^Comment.deleted()]

has_many/has_one :through

Ecto also supports defining associations in terms of other associations via the :through option. Let's see an example:

defmodule Post do
  use Ecto.Schema

  schema "posts" do
    has_many :comments, Comment
    has_one :permalink, Permalink

    # In the has_many :through example below, the `:comments`
    # in the list [:comments, :author] refers to the
    # `has_many :comments` in the Post own schema and the
    # `:author` refers to the `belongs_to :author` of the
    # Comment's schema (the module below).
    # (see the description below for more details)
    has_many :comments_authors, through: [:comments, :author]

    # Specify the association with custom source
    has_many :tags, {"posts_tags", Tag}
  end
end

defmodule Comment do
  use Ecto.Schema

  schema "comments" do
    belongs_to :author, Author
    belongs_to :post, Post
    has_one :post_permalink, through: [:post, :permalink]
  end
end

In the example above, we have defined a has_many :through association named :comments_authors. A :through association always expects a list and the first element of the list must be a previously defined association in the current module. For example, :comments_authors first points to :comments in the same module (Post), which then points to :author in the next schema, Comment.

This :through association will return all authors for all comments that belongs to that post:

# Get all comments authors for a given post
post = Repo.get(Post, 42)
authors = Repo.all assoc(post, :comments_authors)

:through associations can also be preloaded. In such cases, not only the :through association is preloaded but all intermediate steps are preloaded too:

[post] = Repo.all(from(p in Post, where: p.id == 42, preload: :comments_authors))
post.comments_authors #=> [%Author{...}, ...]

# The comments for each post will be preloaded too
post.comments #=> [%Comment{...}, ...]

# And the author for each comment too
hd(post.comments).author #=> %Author{...}

When the :through association is expected to return one or zero items, has_one :through should be used instead, as in the example at the beginning of this section:

# How we defined the association above
has_one :post_permalink, through: [:post, :permalink]

# Get a preloaded comment
[comment] = Repo.all(Comment) |> Repo.preload(:post_permalink)
comment.post_permalink #=> %Permalink{...}

Note :through associations are read-only. For example, you cannot use Ecto.Changeset.cast_assoc/3 to modify through associations.