如何为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
相关产品推荐
相关产品推荐

