如何使用 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
我正在尝试编写涉及动态 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