Ecto 中多态关联的创建与 IEx 优化
1. 多态关联的创建方法
在数据建模中,有时需要将一个记录关联到多个不同类型的记录,这就涉及到多态关联。下面介绍三种创建多态关联的方法。
1.1 方法一:使用多个外键
- 优点 :这可能是最简单的方法,代码阅读者也容易理解。
-
缺点
:
- 会产生未使用的外键列。笔记表需要为每个可关联的不同表设置外键字段,但单个笔记记录只能关联到另一个记录,所以对于任何给定的笔记记录,只有一个外键列会有值。如果要关联的表数量很多,会变得难以管理。
- 需要考虑数据完整性。笔记表中的外键字段必须允许为空值,但如果所有字段都为空或两个以上字段不为空,则是不正确的。
- 解决方案 :可以在笔记模式中添加自定义验证,确保只有一个字段被填充。如果使用支持检查约束的数据库,在创建表时添加约束。例如,使用 Postgres 时,可以在迁移文件中添加以下代码:
priv/repo/migrations/20180620125250_add_notes_tables.exs
fk_check = """
(CASE WHEN artist_id IS NULL THEN 0 ELSE 1 END) +
(CASE WHEN album_id IS NULL THEN 0 ELSE 1 END) +
(CASE WHEN track_id IS NULL THEN 0 ELSE 1 END) = 1
"""
create constraint(:notes_with_fk_fields, :only_one_fk, check: fk_check)
- 适用场景 :通常是最佳方法,但如果要关联的表数量达到几十个或更多,可能需要考虑其他方法。
1.2 方法二:使用抽象模式
-
操作步骤
:
- 创建迁移文件 :为每个关联的表创建单独的笔记表。
priv/repo/migrations/20180620125250_add_notes_tables.exs
create table(:notes_for_artists) do
add :note, :text, null: false
add :author, :string, null: false
add :assoc_id, references(:artists)
timestamps()
end
create table(:notes_for_albums) do
add :note, :text, null: false
add :author, :string, null: false
add :assoc_id, references(:albums)
timestamps()
end
create table(:notes_for_tracks) do
add :note, :text, null: false
add :author, :string, null: false
add :assoc_id, references(:tracks)
timestamps()
end
- **定义抽象模式**:创建一个共享的模式结构体。
lib/music_db/note.ex
schema "abstract table: notes" do
field :note, :string
field :author, :string
field :assoc_id, :integer
timestamps()
end
- **建立关联**:在每个关联的模式定义中添加关联。
# lib/music_db/artist.ex
has_many :notes, {"notes_for_artists", MusicDB.Note}, foreign_key: :assoc_id
# lib/music_db/album.ex
has_many :notes, {"notes_for_albums", MusicDB.Note}, foreign_key: :assoc_id
# lib/music_db/track.ex
has_many :notes, {"notes_for_tracks", MusicDB.Note}, foreign_key: :assoc_id
- 优点 :避免了第一种方法中大量未使用的列,表设计更清晰。
-
缺点
:
- 笔记的列需要在每个关联表中重复。如果要添加或删除列,必须确保同时更改所有表。
-
不能直接创建笔记记录,必须从父记录开始,使用
build_assoc、cast_assoc或put_assoc来创建子记录。
1.3 方法三:使用多对多关联
-
操作步骤
:
- 创建笔记表 :
priv/repo/migrations/20180620125250_add_notes_tables.exs
create table(:notes_with_joins) do
add :note, :text, null: false
add :author, :string, null: false
timestamps()
end
- **创建连接表**:
priv/repo/migrations/20180815192832_add_notes_join_tables.exs
create table(:artists_notes) do
add :artist_id, references(:artists)
add :note_id, references(:notes_with_joins)
end
create index(:artists_notes, :artist_id)
create index(:artists_notes, :note_id)
create table(:albums_notes) do
add :album_id, references(:albums)
add :note_id, references(:notes_with_joins)
end
create index(:albums_notes, :album_id)
create index(:albums_notes, :note_id)
create table(:tracks_notes) do
add :track_id, references(:tracks)
add :note_id, references(:notes_with_joins)
end
create index(:tracks_notes, :track_id)
create index(:tracks_notes, :note_id)
- **设置笔记模式**:
lib/music_db/note.ex
schema "notes_with_joins" do
field :note, :string
field :author, :string
many_to_many :artists, MusicDB.Artist, join_through: "artists_notes"
many_to_many :albums, MusicDB.Album, join_through: "albums_notes"
many_to_many :tracks, MusicDB.Track, join_through: "tracks_notes"
timestamps()
end
- **添加关联到其他模式**:
# lib/music_db/artist.ex
many_to_many :notes, MusicDB.Note, join_through: "artists_notes"
# lib/music_db/album.ex
many_to_many :notes, MusicDB.Note, join_through: "albums_notes"
# lib/music_db/track.ex
many_to_many :notes, MusicDB.Note, join_through: "tracks_notes"
- 优点 :解决了前两种方法的一些缺点,关联定义在单独的表中,但只需要一个笔记表,无需担心在多个表中重复列定义。
-
缺点
:
- “多对多”在这种情况下是个误称,单个笔记不应关联到多个记录,但使用多对多关联会使这种情况成为可能,需要确保代码不会意外关联。
-
由于
has_many和many_to_many的差异,不能以最直观的方式处理关联。
2. 三种方法对比
| 方法 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 多个外键 | 简单易理解 | 产生未使用外键列,需考虑数据完整性 | 关联表数量较少 |
| 抽象模式 | 表设计清晰 | 列重复,不能直接创建记录 | 关联表数量适中 |
| 多对多关联 | 避免列重复 | 可能关联多个记录,操作不直观 | 关联表数量较多 |
3. 多态关联创建流程
graph LR
A[选择关联方法] --> B{关联表数量少?}
B -- 是 --> C[多个外键]
B -- 否 --> D{关联表数量适中?}
D -- 是 --> E[抽象模式]
D -- 否 --> F[多对多关联]
4. IEx 优化建议
Elixir 的交互式控制台 IEx 为使用 Ecto 的项目提供了便捷的数据查询和修改方式。而且,IEx 具有可定制性,通过
.iex.exs
文件可以设置常用的导入、别名、变量或函数,从而简化操作,节省输入时间。
4.1 添加导入和别名
- 添加别名 :为常用的模块添加别名,减少输入量。例如,对于 MusicDB 应用,可以这样设置:
priv/examples/optimizing_iex.exs
alias MusicDB.{
Repo,
Artist,
Album,
Track,
Genre,
Log
}
这样,原本需要输入
album = MusicDB.Repo.get(MusicDB.Album, 1) |> MusicDB.Repo.preload(:tracks)
,现在只需输入
album = Repo.get(Album, 1) |> Repo.preload(:tracks)
。
-
导入 Ecto.Query 模块
:在 IEx 中尝试查询时需要使用该模块。可以使用
import_if_available
避免因模块不可用而报错。
- 若偏好查询的关键字语法,可只导入
from
函数:
import_if_available Ecto.Query, only: [from: 2]
- 若偏好宏语法,则需要导入整个模块:
import_if_available Ecto.Query
-
导入 Ecto.Changeset 模块
:如果需要在 IEx 中修改数据,导入该模块可获得重要的
change和cast函数以及验证和其他实用工具。
import_if_available Ecto.Changeset
4.2 添加辅助函数
可以在
.iex.exs
文件中定义辅助函数,简化常见操作。
- 更新记录的辅助函数 :创建一个辅助函数来处理更新记录的大部分样板代码。
priv/examples/optimizing_iex.exs
defmodule H do
def update(schema, changes) do
schema
|> Ecto.Changeset.change(changes)
|> Repo.update
end
end
使用示例:
artist = Repo.get_by(Artist, name: "Miles Davis")
H.update(artist, name: "Miles Dewey Davis III", birth_date: ~D[1926-05-26])
需要注意的是,这种方法在开发环境中可行,但在生产环境中使用时需要更谨慎,因为没有进行验证和约束检查,可能会引入不良数据。
- 加载专辑及其曲目 :添加一个函数来一次性加载专辑并预加载其曲目。
defmodule H do
def load_album(id) do
Repo.get(Album, id) |> Repo.preload(:tracks)
end
def load_album(title) when is_binary(title) do
Repo.get_by(Album, title: title) |> Repo.preload(:tracks)
end
end
5. IEx 优化步骤总结
| 步骤 | 操作内容 | 代码示例 |
|---|---|---|
| 1 | 添加模块别名 |
alias MusicDB.{Repo, Artist, Album, Track, Genre, Log}
|
| 2 | 导入 Ecto.Query 模块 |
import_if_available Ecto.Query, only: [from: 2]
或
import_if_available Ecto.Query
|
| 3 | 导入 Ecto.Changeset 模块 |
import_if_available Ecto.Changeset
|
| 4 | 定义辅助函数 |
defmodule H do def update(schema, changes) ... end
等
|
6. IEx 优化流程
graph LR
A[启动 IEx 优化] --> B[添加模块别名]
B --> C{是否需要查询操作?}
C -- 是 --> D[导入 Ecto.Query]
C -- 否 --> E{是否需要修改数据?}
D --> E
E -- 是 --> F[导入 Ecto.Changeset]
E -- 否 --> G[定义辅助函数?]
F --> G
G -- 是 --> H[定义常用辅助函数]
G -- 否 --> I[完成优化]
H --> I
综上所述,在 Ecto 中创建多态关联有多种方法可供选择,应根据关联表的数量和具体需求来决定。同时,通过优化 IEx 可以提高开发效率,使数据查询和修改操作更加便捷。合理运用这些技巧,能让开发工作更加高效和轻松。
超级会员免费看
88

被折叠的 条评论
为什么被折叠?



