如何使用 ecto 进行动态 table 连接?

How to do dynamic table joins using ecto?

我正在尝试编写涉及动态 table 连接(照片到相册)的动态查询。我的第一次尝试仅适用于一对多(放置照片):

defmodule Test1 do
  def filter_by_place_id(dynamic, id) do
    dynamic([p], ^dynamic and p.place_id == ^id)
  end
end
dynamic =
  true
  |> Test1.filter_by_place_id(248)

但这不适用于多对多领域。我认为这需要 table 加入。所以我的下一次尝试:

defmodule Test2 do
  def filter_by_place_id({query, dynamic}, id) do
    dynamic = dynamic([p], ^dynamic and p.place_id == ^id)
    {query, dynamic}
  end
  def filter_by_album_id({query, dynamic}, id) do
    query = join(query, :inner, [p], album in assoc(p, :albums), as: :x)
    dynamic = dynamic([{:x, x}], ^dynamic and x.id == ^id)
    {query, dynamic}
  end
end
query = from(p in Photo)
{query, dynamic} =
  {query, true}
  |> Test2.filter_by_place_id(248)
  |> Test2.filter_by_album_id(10)
  |> Test2.filter_by_album_id(11)

但这失败了,因为绑定 :x 是硬编码的,显然我不能重用它。但是我需要一个绑定来确保 where 子句引用正确的连接。

但是如果我尝试使用 as: ^binding 而不是 as :x,我会收到错误消息:

** (Ecto.Query.CompileError) `as` must be a compile time atom, got: `^binding`
    (ecto 3.6.2) expanding macro: Ecto.Query.join/5
    meow.exs:30: Test2.filter_by_album_id/2

所以我不确定从这里到哪里去。是否可以为连接动态分配绑定?

绑定可以这样写:first, ..., last

https://hexdocs.pm/ecto/Ecto.Query.html:

Similarly, if you are interested only in the last binding (or the last bindings) in a query, you can use ... to specify "all bindings before" and match on the last one.

from [p, ..., c] in posts_with_comments, select: {p.title, c.body}

所以尝试重写filter_by_album_id:

def filter_by_album_id({query, dynamic}, id) do
    query = join(query, :inner, [p], album in assoc(p, :albums))
    dynamic = dynamic([p, ..., x], ^dynamic and x.id == ^id)
    {query, dynamic}
  end

另一个答案给了我一个想法,放弃动态,只使用多个 where 子句。我不确定这会得到支持。看起来像多个 where 子句放在一起。

def filter_by_value({query, dynamic}, field, id) do                            
  query = join(query, :inner, [p], album in assoc(p, ^field.id))               
  |> where([p, ..., x], x.id == ^id)                                           
  {query, dynamic}                                                             
end

不幸的是,这消除了动态的灵活性,我真的很喜欢,但现在这是一个足够的解决方案。

实际上,您可以使用 hack 来动态绑定别名。 Ecto 查询是一个结构体,它有一个 属性 aliases 来存储名称绑定映射。

例如你有一个查询:

from(p in Post, join: c in assoc(p, :comments), as: :comments)

那么别名将如下所示:

%Ecto.Query{
   aliases: %{
     comments: 1
   }
}

comments是别名,1是位置。所以你可以手动更新别名映射。

query = from(p in Post, join: c in assoc(p, :comments))
column = :comments

aliases = query.aliases

if Map.has_key?(aliases, column) do
  raise ArgumentError, "Do not allow 2 ref binding with the same name"
end

aliases =
  case Enum.map(aliases, &elem(&1, 1)) do
    [] ->
      %{column => 1}

    positions ->
      max = Enum.max(positions)
      Map.put(aliases, column, max + 1)
  end

# Update the alias map
query = struct(query, aliases: aliases)

但这只是 HACK,请注意。

我在构建动态连接查询时发现了这个,你可以在这里找到源代码 https://github.com/bluzky/filtery/blob/6338f53c81608fd44f1eeac0a2ceb108043e56c9/lib/base.ex#L285