☰
Ecto 动态查询(Dynamic Queries)完整指南:用 `dynamic/2` 构建可组合、可测试的运行时查询
2026/10/7 9:34:46 网站建设 项目流程
  • 后端
  • ORM

【免费下载链接】ecto

A toolkit for data mapping and language integrated query.

项目地址:https://gitcode.com/gh_mirrors/ec/ecto
点击查看免费下载

Ecto 的查询 API 从设计之初就强调通过 Elixir 语法写出预编译、安全高效的查询;而动态查询(Dynamic Queries)则是 Ecto 面向搜索、筛选面板、报表生成等"查询条件在运行时才能确定"的场景提供的核心机制。读完本文,你将掌握关键字语法与管道语法的组合方式、以数据结构为中心的查询写法,以及用Ecto.Query.dynamic/2构建、插值、测试动态查询片段的完整实战方案。

一、Ecto 查询的两种表达方式与组合能力

Ecto 查询既可以写成关键字(keywords)语法,也可以写成基于管道的(pipe-based)语法。两者在功能上完全等价,并可以互相组合。

关键字语法通过from构建查询:

import Ecto.Query from p in Post, where: p.author == "José" and p.category == "Elixir", where: p.published_at > ^minimum_date, order_by: [desc: p.published_at]

管道语法则把查询当作数据流逐级加工:

import Ecto.Query Post |> where([p], p.author == "José" and p.category == "Elixir") |> where([p], p.published_at > ^minimum_date) |> order_by([p], desc: p.published_at)

两种 API 都是可组合的。假设想把"按发布时间过滤并排序"抽象成一个函数,关键字语法可以这样写:

def most_recent_from(query, minimum_date) do from p in query, where: p.published_at > ^minimum_date, order_by: [desc: p.published_at] end

管道语法对应为:

def most_recent_from(query, minimum_date) do query |> where([p], p.published_at > ^minimum_date) |> order_by([p], desc: p.published_at) end

从源码看,这两种写法最终都汇聚到同一套构建管线:where、order_by等宏会经由 filter.ex 的filter!/2系列函数把表达式规范化后合并进查询结构。组合的本质是"查询即值"——查询本身可以作为输入被继续加工,这也正是后续动态查询的基石。

不过,上面的例子组合的是"查询级"操作:每次调用where、order_by都是追加一个完整的条件或排序。而真实业务里(比如一个基于已有文章提供搜索功能的 Web 应用),用户可能同时指定作者名、文章分类、发布区间等多个条件,我们需要的是让where和order_by内部的内容本身也动态可拼装。

另外,很多开发者偏爱管道语法,但每次都要重复写绑定p,相比关键字语法显得啰嗦。为了解决这两个痛点,Ecto 提供了以数据结构为中心的 API,以及强大的动态查询(dynamic query)机制。

二、以数据结构为中心:让查询片段成为一等公民

Ecto 为关键字语法和管道语法都提供了更简洁的数据结构式 API。看这个例子:

from p in Post, where: [author: "José", category: "Elixir"], where: p.published_at > ^minimum_date, order_by: [desc: :published_at]

管道版本:

Post |> where(author: "José", category: "Elixir") |> where([p], p.published_at > ^minimum_date) |> order_by(desc: :published_at)

注意,大多数表达式中我们不再需要写p这个选择器。Ecto 的所有查询构造都接受数据结构作为输入,而且这些数据结构本身可以来自运行时变量:

where = [author: "José", category: "Elixir"] order_by = [desc: :published_at] Post |> where(^where) |> where([p], p.published_at > ^minimum_date) |> order_by(^order_by)

where会把键值对翻译成key == value比较,因此诸如p.published_at > ^minimum_date这类基于序的比较(>、<、>=、<=)无法用键值结构表达,仍需写成原来的样子。也就是说:数据结构 API 覆盖相等比较与字段级排序,但覆盖面有边界。

三、Dynamic fragments:Ecto.Query.dynamic/2宏

对于无法依赖数据结构、却仍然希望动态构建查询的场景,Ecto 提供了Ecto.Query.dynamic/2宏。它的核心价值是:允许你按条件构建查询片段,再把它插值进主查询。

沿用上面的例子:我们可能按需用发布日期过滤文章。常规写法是把条件判断放在函数体内:

query = Post |> where(^where) |> order_by(^order_by) query = if published_at = params["published_at"] do where(query, [p], p.published_at < ^published_at) else query end

而用 dynamic fragments 可以这样写:

where = [author: "José", category: "Elixir"] order_by = [desc: :published_at] filter_published_at = if published_at = params["published_at"] do dynamic([p], p.published_at < ^published_at) else true end Post |> where(^where) |> where(^filter_published_at) |> order_by(^order_by)

要点:

  • dynamic/2接收一个绑定列表和一个表达式,返回一个Ecto.Query.DynamicExpr结构体(定义见 dynamic.ex 的build/3),内部以闭包形式保存表达式、插值参数、子查询与 select 别名;
  • 动态片段可以插值进查询(where(^filter_published_at)),也可以插值进另一个动态片段(如dynamic([p], p.foo == ^1 and ^dynamic)),从而支持无限层级的表达式组合;
  • 空条件时使用布尔字面量true——从 filter.ex 的实现可以看到,filter!/2对布尔值、关键字列表和DynamicExpr都有对应的规范化分支,true会被直接接纳为恒真条件,不会产生任何 SQL 开销。

借助动态片段,我们可以把"参数处理"与"查询生成"彻底解耦:先集中处理参数,再一次性插值进查询。下面看一个更复杂的完整案例。

四、构建动态查询:小函数 + 模式匹配 + reduce

回到最初的场景:构建一个搜索功能,用户可以从多个维度配置如何遍历所有文章——选择排序方式、按作者和分类过滤、筛选某日期之后发布的文章。

Ecto 的解决思路是把问题拆成一组小函数,每个函数负责构建数据结构或动态片段,最后统一插值进查询:

def filter(params) do Post |> order_by(^filter_order_by(params["order_by"])) |> where(^filter_where(params)) end def filter_order_by("published_at_desc"), do: [desc: dynamic([p], p.published_at)] def filter_order_by("published_at"), do: [asc: dynamic([p], p.published_at)] def filter_order_by(_), do: [] def filter_where(params) do Enum.reduce(params, dynamic(true), fn {"author", value}, dynamic -> dynamic([p], ^dynamic and p.author == ^value) {"category", value}, dynamic -> dynamic([p], ^dynamic and p.category == ^value) {"published_at", value}, dynamic -> dynamic([p], ^dynamic and p.published_at > ^value) {_, _}, dynamic -> # Not a where parameter dynamic end) end

这段代码体现了两个关键设计:

  1. 把问题拆成接收普通数据结构的小函数,从而可以使用 Elixir 处理数据的所有工具:

    • order_by参数适合直接模式匹配:匹配"published_at_desc"、"published_at"返回对应的排序动态片段,其余情况返回[](不排序);
    • where子句用Enum.reduce/3从一个恒真的dynamic(true)起步,遍历参数逐条and上新的条件,遇到不相关的参数原样返回累加器。
  2. 嵌套插值的威力:dynamic([p], ^dynamic and p.author == ^value)把前一个累加的动态片段插值进新片段。测试用例 dynamic_test.exs 明确验证了这一行为——例如三个片段层层嵌套插值后,fully_expand/2得到的是&0.foo() == ^0 and (&0.bar1() == ^1 or &0.bar2() == ^2 or &0.bar3() == ^3) and &0.baz() == ^4这样完整展开的表达式树,且插值参数按序收集为[{"foo", ...}, {"bar1", ...}, ...]。

动态查询的测试

拆分带来的另一大好处是测试变得简单:每个函数可以独立测试,即使它返回的是动态片段。例如:

test "filter published at based on the given date" do assert dynamic_match?( filter_where(%{}), "true" ) assert dynamic_match?( filter_where(%{"published_at" => "2010-04-17"}), "true and q.published_at > ^\"2010-04-17\"" ) end defp dynamic_match?(dynamic, string) do inspect(dynamic) == "dynamic([q], #{string})" end

这里的小助手通过比对inspect(dynamic)的输出来断言动态片段的内容。inspect会渲染出形如dynamic([q], true and q.published_at > ^"2010-04-17")的字符串,测试因此可以精确锁定片段的结构。仓库内的动态查询测试(dynamic_test.exs、query_test.exs)也展示了类似思路:用Macro.to_string(expr)与fully_expand/2返回的表达式和参数列表做断言,验证合并两个动态、合并含子查询的动态、命名绑定等场景。

五、Dynamic 与 joins:动态处理关联查询

动态机制同样适用于 join。对上例做两个扩展:第一,新增按作者名排序("author_name"与"author_name_desc");第二,作者存放在独立表中,因此filter_where中的作者过滤需要经由 join 表。

最终方案如下:

def filter(params) do Post # 1. Add named join binding |> join(:inner, [p], assoc(p, :authors), as: :authors) |> order_by(^filter_order_by(params["order_by"])) |> where(^filter_where(params)) end # 2. Returned dynamic with join binding def filter_order_by("published_at_desc"), do: [desc: dynamic([p], p.published_at)] def filter_order_by("published_at"), do: dynamic([p], p.published_at) def filter_order_by("author_name_desc"), do: [desc: dynamic([authors: a], a.name)] def filter_order_by("author_name"), do: dynamic([authors: a], a.name) def filter_order_by(_), do: [] # 3. Change the authors clause inside reduce def filter_where(params) do Enum.reduce(params, dynamic(true), fn {"author", value}, dynamic -> dynamic([authors: a], ^dynamic and a.name == ^value) {"category", value}, dynamic -> dynamic([p], ^dynamic and p.category == ^value) {"published_at", value}, dynamic -> dynamic([p], ^dynamic and p.published_at > ^value) {_, _}, dynamic -> # Not a where parameter dynamic end) end

这里出现了动态查询与 join 配合的三个要点:

  1. 命名绑定(named binding):join 通过as: :authors指定别名。动态片段使用dynamic([authors: a], a.name)即可通过别名引用 join 出来的表,无需关心它在绑定列表中的具体位置;
  2. 绑定粒度:作者过滤的动态片段绑定到:authors,而分类、发布时间的片段仍绑定到主查询[p]——动态片段各自携带自己的绑定信息,插值时由 Ecto 负责协调(对应fully_expand/partially_expand中对 binding 的展开与合并逻辑,见 dynamic.ex);
  3. 可扩展性:未来新增过滤维度,只需在filter_where的Enum.reduce/3中追加一个子句,主查询无需改动。

关于绑定还有一种写法值得注意:dynamic([p, ..., c], c.text == "Test Comment"),即用...表示"沿用前面所有绑定,再追加新绑定"。测试 query_test.exs 验证了动态片段用于 join 的:on条件时,...会正确取得新绑定;dynamic_test.exs 也覆盖了[..., c]绑定在嵌套插值中正确解析为&1.bar() == ^1的场景。

六、小结:动态查询的设计脉络与适用边界

回看整篇,Ecto 的动态查询能力是分层递进的:

  1. 查询级组合:关键字语法与管道语法把"查询"当作可组合的值,适合复用固定的筛选/排序逻辑;
  2. 数据结构插值:where(author: "José")、order_by(^list)让片段本身成为可动态指定的数据,但只覆盖相等比较等有限形态;
  3. 动态片段:dynamic/2覆盖无法用数据结构表达的比较、join、排序等场景,且片段可以互相嵌套插值,最终通过where(^dynamic)等方式并入查询;
  4. 拆分与测试:把动态查询构建拆成接收普通数据的小函数,配合inspect(dynamic)或fully_expand/2的展开结果即可独立测试每个片段。

这套机制在仓库源码中的落点是明确的:dynamic/2宏定义在 query.ex,宏体委托给Ecto.Query.Builder.Dynamic.build/3;运行时插值由fully_expand/2与partially_expand/2完成(dynamic.ex);where、having、join 的:on等过滤构造统一经由 filter.ex 的filter!/2分支处理DynamicExpr,并在展开后把参数、子查询与表达式组装进Ecto.Query.BooleanExpr。阅读 dynamic_test.exs 与 query_test.exs 中的动态查询用例,可以进一步确认这些行为细节。

  • 后端
  • ORM

【免费下载链接】ecto

A toolkit for data mapping and language integrated query.

项目地址:https://gitcode.com/gh_mirrors/ec/ecto
点击查看免费下载
上一篇:Raspberry Pi 4 UEFI Firmware性能测试:启动速度与系统稳定性实测
下一篇:OpenChamber 1.8.0 深度解析:SSH 远程实例与安全隧道实战指南

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询