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

Elixir/Phoenix Ecto:递归关联下批量软删除所有子资源

软删除父文件夹时同步归档所有子层级文件夹与文件的实现方案

问题背景

我拥有folders和files两张数据表,对应的Ecto Schema定义如下:

Folder Schema:

schema "folders" do
  field :name, :string
  field :archived, :boolean

  belongs_to :folder, Folder
  has_many :folders, Folder
  has_many :files, File
end

File Schema:

schema "files" do
  field :name, :string
  field :archived, :boolean

  belongs_to :folder, Folder
end

archived字段用于实现软删除,父文件夹可包含无限层级的子文件夹及文件。需求是:当软删除父文件夹时,其所有层级的子文件夹及文件需同步将archived字段设为true。


方案一:PostgreSQL递归触发器(推荐,数据库层保证一致性)

这种方式能从底层保证逻辑执行,无论通过Ecto还是其他工具操作数据库,都不会出现遗漏。

1. 创建递归函数获取所有子文件夹ID

编写SQL递归函数,输入父文件夹ID,返回所有层级的子文件夹ID(包含自身):

CREATE OR REPLACE FUNCTION get_all_child_folder_ids(parent_id INTEGER)
RETURNS SETOF INTEGER AS $$
BEGIN
  RETURN QUERY
  WITH RECURSIVE folder_tree AS (
    SELECT id FROM folders WHERE id = parent_id
    UNION ALL
    SELECT f.id FROM folders f
    JOIN folder_tree ft ON f.folder_id = ft.id
    WHERE f.archived = FALSE
  )
  SELECT id FROM folder_tree;
END;
$$ LANGUAGE plpgsql;

2. 创建触发器函数

编写触发器逻辑,仅当文件夹的archived状态从false变为true时,同步归档所有子层级资源:

CREATE OR REPLACE FUNCTION archive_folder_hierarchy()
RETURNS TRIGGER AS $$
BEGIN
  IF OLD.archived = FALSE AND NEW.archived = TRUE THEN
    -- 归档所有子文件夹
    UPDATE folders
    SET archived = TRUE
    WHERE id IN (SELECT get_all_child_folder_ids(NEW.id));
    
    -- 归档所有关联文件(当前文件夹+所有子文件夹下的文件)
    UPDATE files
    SET archived = TRUE
    WHERE folder_id IN (SELECT get_all_child_folder_ids(NEW.id));
  END IF;
  RETURN NEW;
END;
$$ LANGUAGE plpgsql;

3. 绑定触发器到folders表

给folders表的archived字段更新事件绑定触发器:

CREATE TRIGGER trigger_archive_folder_hierarchy
AFTER UPDATE OF archived ON folders
FOR EACH ROW
EXECUTE FUNCTION archive_folder_hierarchy();

完成后,只要将任意文件夹的archived设为true,数据库会自动递归处理所有子层级的归档操作。


方案二:Ecto递归处理(应用层实现)

如果不想在数据库层面编写逻辑,可以在Elixir代码中通过递归查询+事务实现,需注意数据一致性问题。

实现代码示例

defmodule MyApp.Folder do
  use Ecto.Schema
  import Ecto.Query
  alias MyApp.Repo
  alias MyApp.Folder
  alias MyApp.File

  # 软删除文件夹并归档所有子层级,包裹在事务中保证原子性
  def archive_folder(folder_id) do
    Repo.transaction(fn ->
      # 递归获取所有子文件夹ID(包含自身)
      child_folder_ids = get_all_child_folder_ids(folder_id)

      # 批量归档所有子文件夹
      from(f in Folder, where: f.id in ^child_folder_ids)
      |> Repo.update_all(set: [archived: true])

      # 批量归档所有关联文件
      from(f in File, where: f.folder_id in ^child_folder_ids)
      |> Repo.update_all(set: [archived: true])

      # 归档当前文件夹
      Repo.get!(Folder, folder_id)
      |> Ecto.Changeset.change(archived: true)
      |> Repo.update!()
    end)
  end

  # 递归查询所有子文件夹ID的私有函数
  defp get_all_child_folder_ids(folder_id) do
    query = from f in Folder, where: f.folder_id == ^folder_id and not f.archived, select: f.id
    child_ids = Repo.all(query)
    [folder_id] ++ Enum.flat_map(child_ids, &get_all_child_folder_ids/1)
  end
end

注意事项

  • 必须通过事务执行,确保所有更新要么全部成功,要么全部回滚
  • 若文件夹层级极深,递归查询可能存在性能瓶颈,此时数据库层方案更优
  • 并发场景下需考虑加锁,避免同一资源被同时修改

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.24 11:41:19