Ecto关联关系全面指南:从基础到高级应用

Ecto关联关系全面指南:从基础到高级应用

ecto A toolkit for data mapping and language integrated query. ecto 项目地址: https://gitcode.com/gh_mirrors/ec/ecto

引言

在数据库设计中,表与表之间的关联关系是核心概念。Ecto作为Elixir生态中强大的数据库包装器和查询语言,提供了清晰而强大的方式来处理这些关联关系。本文将深入探讨Ecto中的各种关联类型及其使用方法。

基本概念

在开始之前,我们需要明确几个关键概念:

  • 内部数据:直接硬编码在Elixir代码中的数据或逻辑
  • 外部数据:来自用户输入(如表单、API等)的数据,通常需要通过Ecto.Changeset进行规范化、修剪和验证

一对多关系(has_many/belongs_to)

模型定义

在电影和角色的例子中,一部电影可以有多个角色,而每个角色属于一部电影:

# 电影模型
defmodule Movie do
  use Ecto.Schema

  schema "movies" do
    field :title, :string
    field :release_date, :date
    has_many :characters, Character  # 定义一对多关系
  end
end

# 角色模型
defmodule Character do
  use Ecto.Schema

  schema "characters" do
    field :name, :string
    field :age, :integer
    belongs_to :movie, Movie  # 定义从属关系
  end
end

迁移文件

外键应定义在belongs_to的一方:

defmodule MyApp.Migrations.CreateMoviesAndCharacters do
  use Ecto.Migration

  def change do
    create table("movies") do
      add :title, :string, null: false
      add :release_date, :date
      timestamps()
    end

    create table("characters") do
      add :name, :string, null: false
      add :age, :integer
      add :movie_id, references(:movies, on_delete: :delete_all), null: false
      timestamps()
    end
  end
end

一对一关系(has_one/belongs_to)

模型定义

一对一关系与一对多类似,但限制为只有一个关联记录:

# 电影模型
defmodule Movie do
  use Ecto.Schema

  schema "movies" do
    field :title, :string
    field :release_date, :date
    has_one :screenplay, Screenplay  # 定义一对一关系
  end
end

# 剧本模型
defmodule Screenplay do
  use Ecto.Schema

  schema "screenplays" do
    field :lead_writer, :string
    belongs_to :movie, Movie  # 定义从属关系
  end
end

多对多关系

多对多关系有两种实现方式:通过连接表或连接模型。

通过连接表

# 电影模型
defmodule Movie do
  use Ecto.Schema

  schema "movies" do
    field :title, :string
    field :release_date, :date
    many_to_many :actors, Actor, join_through: "movies_actors"
  end
end

# 演员模型
defmodule Actor do
  use Ecto.Schema

  schema "actors" do
    field :name, :string
    many_to_many :movies, Movie, join_through: "movies_actors"
  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

# 连接模型
defmodule UserOrganization do
  use Ecto.Schema

  @primary_key false
  schema "users_organizations" do
    belongs_to :user, User
    belongs_to :organization, Organization
    timestamps()
  end
end

查询关联记录

预加载关联数据

# 在查询时预加载
query = from m in Movie, preload: :characters
Repo.all(query)

# 对已加载记录预加载
movies = Repo.all(Movie)
movies = Repo.preload(movies, :characters)

使用JOIN优化查询

# 常规JOIN
query =
  from m in Movie,
  join: c in Character,
  on: m.id == c.movie_id,
  preload: [characters: c]

# 使用assoc宏
query =
  from m in Movie,
  join: c in assoc(m, :characters),
  preload: [characters: c]

插入关联记录

为已有父记录插入子记录

# 使用内部数据
Repo.get_by!(Movie, title: "The Shawshank Redemption")
|> Ecto.build_assoc(:characters, name: "Red", age: 60)
|> Repo.insert()

# 使用外部数据
params = %{"name" => "Red", "age" => 60}
Repo.get_by!(Movie, title: "The Shawshank Redemption")
|> Ecto.build_assoc(:characters)
|> cast(params, [:name, :age])
|> Repo.insert()

同时插入父子记录

# 使用内部数据
Repo.insert(
  %Movie{
    title: "The Shawshank Redemption",
    release_date: ~D[1994-10-14],
    characters: [
      %Character{name: "Andy Dufresne", age: 50},
      %Character{name: "Red", age: 60}
    ]
  }
)

# 使用外部数据
params = %{
  "title" => "Shawshank Redemption",
  "release_date" => "1994-10-14",
  "characters" => [
    %{"name" => "Andy Dufresne", "age" => "50"},
    %{"name" => "Red", "age" => "60"}
  ]
}

%Movie{}
|> cast(params, [:title, :release_date])
|> cast_assoc(:characters)
|> Repo.insert()

更新关联记录

单独更新记录

movie =
  Repo.get_by!(Movie, title: "The Shawshank Redemption")
  |> Repo.preload(:screenplay)

movie.screenplay
|> change(%{lead_writer: "Frank Darabont"})
|> Repo.update()

批量更新关联记录

# 使用put_assoc
movie =
  Repo.get_by!(Movie, title: "The Shawshank Redemption")
  |> Repo.preload(:characters)

characters =
  Enum.map(movie.characters, fn character ->
    change(character, age: character.age + 1)
  end)

movie
|> change()
|> put_assoc(:characters, characters)
|> Repo.update()

# 使用update_all(更高效)
movie = Repo.get_by!(Movie, title: "The Shawshank Redemption")
movie
|> Ecto.assoc(:characters)
|> Repo.update_all(inc: [age: 1])

最佳实践

  1. 性能考虑:对于批量操作,优先使用基于查询的方法(如update_all)而非加载所有记录到内存
  2. 数据验证:处理外部数据时始终使用Changeset进行验证
  3. 关联管理:理解:on_replace选项的行为,它决定了当关联被替换时如何处理现有记录
  4. 查询优化:合理使用preload和join来平衡查询次数和查询复杂度

通过掌握这些关联关系技术,你可以在Elixir应用中构建复杂的数据模型,同时保持代码的清晰和高效。

ecto A toolkit for data mapping and language integrated query. ecto 项目地址: https://gitcode.com/gh_mirrors/ec/ecto

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

沈瑗研

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值