关联关系 #
一、关联概述 #
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