关联关系 #

一、关联概述 #

1.1 关联类型 #

类型 说明
belongs_to 属于某个记录
has_one 拥有一个记录
has_many 拥有多个记录
many_to_many 多对多关系
embeds_one 嵌入一个记录
embeds_many 嵌入多个记录

1.2 关联结构 #

text
User (用户)
├── has_one: Profile (个人资料)
├── has_many: Posts (文章)
├── has_many: Comments (评论)
└── many_to_many: Roles (角色)

Post (文章)
├── belongs_to: Author (作者/用户)
├── has_many: Comments (评论)
└── many_to_many: Tags (标签)

二、belongs_to #

2.1 定义belongs_to #

elixir
defmodule Hello.Blog.Post do
  use Ecto.Schema

  schema "posts" do
    field :title, :string
    field :body, :text
    belongs_to :author, Hello.Accounts.User
  end
end

2.2 数据库迁移 #

elixir
create table(:posts) do
  add :title, :string
  add :body, :text
  add :author_id, references(:users, on_delete: :delete_all)

  timestamps()
end

2.3 使用belongs_to #

elixir
def get_post_with_author(id) do
  Repo.get!(Post, id) |> Repo.preload(:author)
end

def create_post(attrs, user) do
  %Post{author_id: user.id}
  |> Post.changeset(attrs)
  |> Repo.insert()
end

三、has_one #

3.1 定义has_one #

elixir
defmodule Hello.Accounts.User do
  use Ecto.Schema

  schema "users" do
    field :email, :string
    has_one :profile, Hello.Accounts.Profile
  end
end

defmodule Hello.Accounts.Profile do
  use Ecto.Schema

  schema "profiles" do
    field :bio, :string
    field :location, :string
    belongs_to :user, Hello.Accounts.User
  end
end

3.2 数据库迁移 #

elixir
create table(:profiles) do
  add :bio, :text
  add :location, :string
  add :user_id, references(:users, on_delete: :delete_all)

  timestamps()
end

3.3 使用has_one #

elixir
def get_user_with_profile(id) do
  Repo.get!(User, id) |> Repo.preload(:profile)
end

def create_profile(user, attrs) do
  %Profile{user_id: user.id}
  |> Profile.changeset(attrs)
  |> Repo.insert()
end

四、has_many #

4.1 定义has_many #

elixir
defmodule Hello.Accounts.User do
  use Ecto.Schema

  schema "users" do
    field :email, :string
    has_many :posts, Hello.Blog.Post
    has_many :comments, Hello.Blog.Comment
  end
end

defmodule Hello.Blog.Post do
  use Ecto.Schema

  schema "posts" do
    field :title, :string
    belongs_to :author, Hello.Accounts.User
    has_many :comments, Hello.Blog.Comment
  end
end

4.2 使用has_many #

elixir
def get_user_posts(user_id) do
  Repo.get!(User, user_id)
  |> Repo.preload(:posts)
  |> Map.get(:posts)
end

def list_posts_by_user(user_id) do
  from(p in Post, where: p.author_id == ^user_id)
  |> Repo.all()
end

五、many_to_many #

5.1 定义many_to_many #

elixir
defmodule Hello.Blog.Post do
  use Ecto.Schema

  schema "posts" do
    field :title, :string
    field :body, :text
    many_to_many :tags, Hello.Blog.Tag, join_through: "posts_tags"
  end
end

defmodule Hello.Blog.Tag do
  use Ecto.Schema

  schema "tags" do
    field :name, :string
    many_to_many :posts, Hello.Blog.Post, join_through: "posts_tags"
  end
end

5.2 数据库迁移 #

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

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

5.3 使用many_to_many #

elixir
def get_post_with_tags(id) do
  Repo.get!(Post, id) |> Repo.preload(:tags)
end

def add_tag_to_post(post, tag) do
  post
  |> Repo.preload(:tags)
  |> Ecto.Changeset.change()
  |> Ecto.Changeset.put_assoc(:tags, [tag | post.tags])
  |> Repo.update()
end

def sync_tags(post, tags) do
  post
  |> Repo.preload(:tags)
  |> Ecto.Changeset.change()
  |> Ecto.Changeset.put_assoc(:tags, tags)
  |> Repo.update()
end

六、嵌入关系 #

6.1 embeds_one #

elixir
defmodule Hello.Accounts.Address do
  use Ecto.Schema

  embedded_schema do
    field :street, :string
    field :city, :string
    field :zip, :string
    field :country, :string
  end
end

defmodule Hello.Accounts.User do
  use Ecto.Schema

  schema "users" do
    field :email, :string
    embeds_one :address, Hello.Accounts.Address
  end
end

6.2 数据库迁移 #

elixir
create table(:users) do
  add :email, :string
  add :address, :map

  timestamps()
end

6.3 使用embeds_one #

elixir
def create_user_with_address(attrs) do
  %User{}
  |> User.changeset(attrs)
  |> Ecto.Changeset.cast_embed(:address, with: &Address.changeset/2)
  |> Repo.insert()
end

def update_address(user, address_attrs) do
  user
  |> Ecto.Changeset.change()
  |> Ecto.Changeset.put_embed(:address, address_attrs)
  |> Repo.update()
end

6.4 embeds_many #

elixir
defmodule Hello.Accounts.PhoneNumber do
  use Ecto.Schema

  embedded_schema do
    field :type, :string
    field :number, :string
  end
end

defmodule Hello.Accounts.User do
  use Ecto.Schema

  schema "users" do
    field :email, :string
    embeds_many :phone_numbers, Hello.Accounts.PhoneNumber
  end
end

七、关联预加载 #

7.1 基本预加载 #

elixir
def list_posts_with_author do
  Repo.all(Post) |> Repo.preload(:author)
end

def list_posts_with_comments do
  from(p in Post, preload: [:comments])
  |> Repo.all()
end

7.2 多重预加载 #

elixir
def list_posts_full do
  from(p in Post, preload: [:author, :comments, :tags])
  |> Repo.all()
end

7.3 嵌套预加载 #

elixir
def list_posts_with_comment_authors do
  from(p in Post, preload: [comments: :author])
  |> Repo.all()
end

7.4 条件预加载 #

elixir
def list_posts_with_recent_comments do
  from(p in Post,
    preload: [
      comments: from(c in Comment,
        where: c.inserted_at > ago(7, "day"),
        order_by: [desc: c.inserted_at]
      )
    ]
  )
  |> Repo.all()
end

八、关联操作 #

8.1 put_assoc #

elixir
def create_post_with_author(attrs, user) do
  %Post{}
  |> Post.changeset(attrs)
  |> Ecto.Changeset.put_assoc(:author, user)
  |> Repo.insert()
end

def add_comment_to_post(post, comment_attrs) do
  comment_changeset = Ecto.Changeset.change(%Comment{}, comment_attrs)

  post
  |> Repo.preload(:comments)
  |> Ecto.Changeset.change()
  |> Ecto.Changeset.put_assoc(:comments, [comment_changeset | post.comments])
  |> Repo.update()
end

8.2 cast_assoc #

elixir
defmodule Hello.Blog.Post do
  use Ecto.Schema
  import Ecto.Changeset

  schema "posts" do
    field :title, :string
    has_many :comments, Hello.Blog.Comment
  end

  def changeset(post, attrs) do
    post
    |> cast(attrs, [:title])
    |> cast_assoc(:comments)
  end
end

8.3 关联删除 #

elixir
defmodule Hello.Blog.Post do
  use Ecto.Schema

  schema "posts" do
    field :title, :string
    has_many :comments, Hello.Blog.Comment, on_replace: :delete
  end
end

九、关联查询 #

9.1 关联过滤 #

elixir
def posts_by_author_name(name) do
  from(p in Post,
    join: u in assoc(p, :author),
    where: ilike(u.name, ^"%#{name}%")
  )
  |> Repo.all()
end

9.2 has_many关联查询 #

elixir
def users_with_posts_count do
  from(u in User,
    left_join: p in assoc(u, :posts),
    group_by: u.id,
    select: {u, count(p.id)}
  )
  |> Repo.all()
end

9.3 关联排序 #

elixir
def posts_with_comments_ordered do
  from(p in Post,
    preload: [comments: ^from(c in Comment, order_by: [desc: c.inserted_at])]
  )
  |> Repo.all()
end

十、总结 #

10.1 关联类型 #

类型 说明 数据库
belongs_to 属于 外键在本表
has_one 拥有一个 外键在关联表
has_many 拥有多个 外键在关联表
many_to_many 多对多 中间表
embeds_one 嵌入一个 JSON字段
embeds_many 嵌入多个 JSON字段

10.2 常用操作 #

操作 说明
preload 预加载关联
put_assoc 设置关联
cast_assoc 转换关联
assoc 关联查询

10.3 下一步 #

现在你已经了解了关联关系,接下来让我们学习 变更集验证,深入了解数据验证!

最后更新:2026-03-28