You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何为Ecto Schema配置双向自关联多对多关系

合并任务双向关联为单个字段的实现方案

方案一:利用数据库视图简化关联(推荐)

原有的task_links表通过left_task_id < right_task_id约束避免了重复关联,我们可以创建一个视图来展开双向关联,让Ecto直接通过单个many_to_many关联获取所有相关任务。

1. 创建数据库视图

在迁移文件中添加视图创建语句:

def change do
  execute """
  CREATE VIEW task_related_links AS
  SELECT left_task_id AS task_id, right_task_id AS related_task_id, creator_id, inserted_at, updated_at FROM task_links
  UNION
  SELECT right_task_id AS task_id, left_task_id AS related_task_id, creator_id, inserted_at, updated_at FROM task_links;
  """
end

2. 更新Task Schema的关联

修改Task Schema,替换原来的两个many_to_many关联为单个关联:

schema "tasks" do
  field :title, :integer
  field :description, :string
  belongs_to :assignee, User
  many_to_many :related_tasks, Task, 
    join_through: "task_related_links",
    join_keys: [task_id: :id, related_task_id: :id]
  timestamps()
end

使用方式

现在查询任务时,直接preload:related_tasks就能获取所有关联任务:

task = Repo.get(Task, task_id) |> Repo.preload(:related_tasks)
# task.related_tasks 包含所有双向关联的任务

如果需要对关联进行写入操作,因为视图本身不可写,可以继续使用原有的TaskLinks schema来创建关联,视图仅用于查询。

方案二:内存合并关联数据(快速实现)

如果不想修改数据库结构,可保留原有两个关联,通过内存合并得到单个字段的结果。

1. 保留原有关联并添加虚拟字段

在Task Schema中添加虚拟字段存储合并后的关联:

schema "tasks" do
  field :title, :integer
  field :description, :string
  belongs_to :assignee, User
  many_to_many :right_links, Task, join_through: TaskLinks, join_keys: [left_task_id: :id, right_task_id: :id]
  many_to_many :left_links, Task, join_through: TaskLinks, join_keys: [right_task_id: :id, left_task_id: :id]
  # 虚拟字段,用于存储合并后的关联任务
  field :related_tasks, {:array, :map}, virtual: true
  timestamps()
end

2. 自动合并关联数据

通过Ecto的after_load回调自动完成合并,避免每次查询手动处理:

@impl true
def after_load(task) do
  related_tasks = Enum.uniq(task.left_links ++ task.right_links)
  %{task | related_tasks: related_tasks}
end

查询时只要preload了:left_links和:right_links,related_tasks字段会自动填充合并后的结果:

task = Repo.get(Task, task_id) |> Repo.preload([:left_links, :right_links])

方案三:自定义查询获取关联任务

通过Ecto查询语法直接从数据库获取所有关联任务,避免加载两个独立的关联集合。

定义查询函数

在Task模块中添加获取关联任务的函数:

def get_related_tasks_query(task_id) do
  from t in Task,
    join: tl in TaskLinks,
    where: tl.left_task_id == ^task_id or tl.right_task_id == ^task_id,
    where: t.id != ^task_id,
    select: t,
    distinct: true
end

使用方式

直接调用函数获取关联任务:

related_tasks = Task.get_related_tasks_query(task_id) |> Repo.all()

如果需要和主任务一起加载,可以用自定义preload:

task = 
  Task
  |> Repo.get(task_id)
  |> Repo.preload([related_tasks: Task.get_related_tasks_query(task_id)])

内容的提问来源于stack exchange,提问作者Ian Mac

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.23 11:27:06