完整指南:用 `dynamic/2` 构建可组合、可测试的运行时查询)
后端ORM【免费下载链接】ectoA 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 fragmentsEcto.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 bindingjoin 通过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.exwhere、having、join 的:on等过滤构造统一经由 filter.ex 的filter!/2分支处理DynamicExpr并在展开后把参数、子查询与表达式组装进Ecto.Query.BooleanExpr。阅读 dynamic_test.exs 与 query_test.exs 中的动态查询用例可以进一步确认这些行为细节。赞分享后端ORM【免费下载链接】ectoA toolkit for data mapping and language integrated query.项目地址https://gitcode.com/gh_mirrors/ec/ecto点击查看免费下载相关推荐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后端上一篇Raspberry Pi 4 UEFI Firmware性能测试启动速度与系统稳定性实测下一篇OpenChamber 1.8.0 深度解析SSH 远程实例与安全隧道实战指南创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考