- 后端
- ORM
【免费下载链接】ecto
A toolkit for data mapping and language integrated query.
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这段代码体现了两个关键设计:
把问题拆成接收普通数据结构的小函数,从而可以使用 Elixir 处理数据的所有工具:
order_by参数适合直接模式匹配:匹配"published_at_desc"、"published_at"返回对应的排序动态片段,其余情况返回[](不排序);where子句用Enum.reduce/3从一个恒真的dynamic(true)起步,遍历参数逐条and上新的条件,遇到不相关的参数原样返回累加器。
嵌套插值的威力:
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 配合的三个要点:
- 命名绑定(named binding):join 通过
as: :authors指定别名。动态片段使用dynamic([authors: a], a.name)即可通过别名引用 join 出来的表,无需关心它在绑定列表中的具体位置; - 绑定粒度:作者过滤的动态片段绑定到
:authors,而分类、发布时间的片段仍绑定到主查询[p]——动态片段各自携带自己的绑定信息,插值时由 Ecto 负责协调(对应fully_expand/partially_expand中对 binding 的展开与合并逻辑,见 dynamic.ex); - 可扩展性:未来新增过滤维度,只需在
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 的动态查询能力是分层递进的:
- 查询级组合:关键字语法与管道语法把"查询"当作可组合的值,适合复用固定的筛选/排序逻辑;
- 数据结构插值:
where(author: "José")、order_by(^list)让片段本身成为可动态指定的数据,但只覆盖相等比较等有限形态; - 动态片段:
dynamic/2覆盖无法用数据结构表达的比较、join、排序等场景,且片段可以互相嵌套插值,最终通过where(^dynamic)等方式并入查询; - 拆分与测试:把动态查询构建拆成接收普通数据的小函数,配合
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.
相关推荐
Flink 动态表(Dynamic Tables)与连续查询(Continuous Queries)完全指南
Flink 动态表(Dynamic Tables)与连续查询(Continuous Queries)完全指南 Flink 的 Table API 与 SQL 将
后端大数据流处理批处理在 Diesel 中运行时动态查询未知表结构:diesel-dynamic-schema 完整实战指南
在 Diesel 中运行时动态查询未知表结构:diesel dynamic schema 完整实战指南 本指南围绕仓库中的 diesel_dynamic_sch
后端数据库AngularFirestore 集合查询指南:AngularFire Compat API 的查询构建、动态查询与集合组查询
AngularFirestore 集合查询指南:AngularFire Compat API 的查询构建、动态查询与集合组查询 本篇指南围绕 AngularFi
后端
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考