Ecto.Schema.has_many
has_many
, go back to Ecto.Schema module for more information.
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, setreferences(:parent_id, on_delete: :delete_all)
. Opposite to the migration option, this option cannot guarantee integrity and it is only triggered forEcto.Repo.delete/2
(and not onEcto.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 usingEcto.Multi
orEcto.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
. SeeEcto.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 setPost.has_many :comments, defaults: [public: true]
, then when usingEcto.build_assoc(post, :comments)
, the comment will havecomment.public == true
. Alternatively, you can set it toPost.has_many :comments, defaults: :update_comment
, which will invokePost.update_comment(comment, post)
, or set it to a MFA tuple such as{Mod, fun, [arg3, arg4]}
, which will invokeMod.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 defaultorder_by
of the association. It is used when the association is preloaded. For example, if you setPost.has_many :comments, preload_order: [asc: :content]
, whenever the:comments
associations is preloaded, the comments will be order by the:content
field. SeeEcto.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 (seeEcto.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.