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

