如何在Ecto Repo中使用原始SQL

50

我有一个upsert需求,需要调用Postgres存储过程或使用公共表达式。我还使用pgcrypto扩展来处理密码,并且希望使用Postgres函数(如“crypt”)来编码/解码密码。

但是我找不到在Ecto中使用部分或全部原始SQL的方法,这意味着Ecto只支持Elixir DSL并且不允许在DSL不足时切换到原始SQL吗?

我发现我可以通过适配器查询(Rocket是应用程序的名称)。

q = Ecto.Adapters.Postgres.query(Rocket.Repo,"select * from users limit 1",[])

但不确定如何将此传递给模型。我新手使用 Elixir,似乎应该能够使用 Ecto.Model.Schema.schema/3,但这会失败。

Rocket.User.__schema__(:load,q.rows |> List.first,0)
** (FunctionClauseError) no function clause matching in Rocket.User.__schema__/3    

升级到 ecto 4.0,上述工作正常运行!不确定为什么 Repo 不本质支持 SQL 查询,但我很高兴能够继续前进。 - Krut
1
仓库不支持查询,因为它旨在成为SQL无关的!我对__schema__(:load, ..., ...)是否应该直接这样使用有矛盾的感觉...但你的解决方案非常适合当前的代码! - José Valim
1
我想在正在进行的一个Ecto项目示例上做类似于这个的事情,感谢问题的提问者并给Jose留下一个例子。我有一个使用parent_id (邻接表)的树形结构,并且我希望能够a) 使用postgres递归查询和b)返回模型树,因此即使它永远不会直接出现在Ecto本身中,我仍然希望有一种官方的方式从我自己得到的一些数据创建一堆模型。 - chrismcg
@Krut请回答您自己的问题并接受此答案,以便该问题不再显示为未回答。这将使未来的访问者更容易找到答案。谢谢! - Patrick Oscity
1
@PatrickOscity仍在努力找出完整的答案,查询部分已经:ok,但仍需要弄清如何最好地将结果放入Ecto.Model中。 - Krut
显示剩余2条评论
8个回答

45
在使用Postgres的Ecto 2.0(beta)中,您可以使用Ecto.Adapters.SQL.query()当前文档2.0-beta2文档)执行任意SQL查询; 除了返回行本身的列表外(“rows”),它还会返回列名的列表(“columns”)。

在下面的示例中,我:

  1. 运行不带参数的自定义查询,
  2. 将结果的列名从字符串转换为原子,
  3. 将其与结果的每一行组合起来,并将其映射到一个结构体中,使用Kernel.struct()

(您可能想要运行没有感叹号! 的query()版本并检查{ok, res}。)

qry = "SELECT * FROM users"
res = Ecto.Adapters.SQL.query!(Repo, qry, []) # 1

cols = Enum.map res.columns, &(String.to_atom(&1)) # 2

roles = Enum.map res.rows, fn(row) ->
  struct(MyApp.User, Enum.zip(cols, row)) # 3
end

Kernel.struct将绕过模式的类型转换,因此如果将changeset作为最后一个函数调用,则认为这将起作用。但是,我只想获取changeset.data(快速阅读2.0文档),那现在它是结构体的引用? - Krut

9

针对 Ecto 2.0 的修改方案:

在 repo.ex 文件中:

  def execute_and_load(sql, params, model) do
    Ecto.Adapters.SQL.query!(__MODULE__, sql, params)
    |> load_into(model)
  end

  defp load_into(response, model) do
    Enum.map(response.rows, fn row ->
      fields = Enum.reduce(Enum.zip(response.columns, row), %{}, fn({key, value}, map) ->
        Map.put(map, key, value)
      end)
      Ecto.Schema.__load__(model, nil, nil, nil, fields,
                           &Ecto.Type.adapter_load(__adapter__, &1, &2))
    end)
  end

使用方法:

Repo.execute_and_load("SELECT * FROM users WHERE id = $1", [1], User)

更新:对于Ecto 3,你可以使用__MODULE__.load(model, fields)代替Ecto.Schema.__load__

很好,如果我需要使用带有左连接的原始SQL怎么办?我尝试过这种方法,但对于连接操作它不起作用。它只返回与函数参数中指定的模型相关联的数据。 - W.M.
1
太棒了!我把它稍微缩小了一点,以防有帮助。我无法在这个评论中格式化它,所以我放在这里:https://dev59.com/2V4c5IYBdhLWcg3wl7Mg#46247304 - Michael Bishop

8

现在Ecto 1.0已经发布,这应该能持续一段时间:

将以下函数添加到您的 Repo 模块中:

def execute_and_load(sql, params, model) do
  Ecto.Adapters.SQL.query!(__MODULE__, sql, params)
  |> load_into(model)
end

defp load_into(response, model) do
  Enum.map response.rows, fn(row) ->
    fields = Enum.reduce(Enum.zip(response.columns, row), %{}, fn({key, value}, map) ->
      Map.put(map, key, value)
    end)

    Ecto.Schema.__load__(model, nil, nil, [], fields, &__MODULE__.__adapter__.load/2)
  end
end

并且可以这样使用:

Repo.execute_and_load("SELECT * FROM users WHERE id = $1", [1], User)

7

除了Ecto.Adapters.SQL.query/4之外,还有Ecto.Query.API.fragment/1可以用于向数据库发送查询表达式。例如,要使用Postgres的数组函数array_upper,可以使用以下方法:

Ecto.Query.where([x], fragment("array_upper(some_array_field, 1)]" == 1)

6

Ecto 2.2.8提供了Ecto.Query.load/2,因此您可以像这样操作:

use Ecto.Repo

def execute_and_load(sql, params, model) do
  result = query!(sql, params)
  Enum.map(result.rows, &load(model, {result.columns, &1}))
end

请查看https://hexdocs.pm/ecto/Ecto.Repo.html#c:load/2,这里提供有关it技术的信息。

1
如果查询包含联接或计算列,则似乎会出现错误。我观察到使用 phoenix_ecto ~> 4.0 时某些属性未被加载。 - tfwright

4

如果是Ecto版本大于0.7,建议使用以下方法:

Ecto.Adapters.SQL.query/4

def query(repo, sql, params, opts \\ [])

在给定的存储库上运行自定义SQL查询。

如果成功,它必须返回一个包含至少两个键的map的 :ok 元组:

• :num_rows - 受影响的行数 • :rows - 结果集的列表形式。如果命令没有产生任何行作为结果(但仍然产生了受影响的行数, 如不返回删除命令), 可以返回 nil。

选项

• :timeout - 调用完成的等待时间(以毫秒为单位),:infinity 将无限期地等待 (默认值:5000) • :log - 当为 false 时,不记录查询日志

例子

iex> Ecto.Adapters.SQL.query(MyRepo, "SELECT $1 + $2", [40, 2])

%{rows: [{42}], num_rows: 1}


2
这只返回行数据,而不是ecto结构体。 - Krut

3

这是 https://stackoverflow.com/users/1758892/thousandsofthem 的示例,但缩小了一点点(由他/她提供)

defmodule MyApp.Repo do
  [...]
  def execute_and_load(sql, params, schema) do
    response = query!(sql, params)
    Enum.map(response.rows, fn row ->
      fields = Enum.zip(response.columns, row) |> Enum.into(%{})
      Ecto.Schema.__load__(schema, nil, nil, nil, fields,
        &Ecto.Type.adapter_load(__adapter__(), &1, &2))
    end)
  end
end

-5

至少使用 Ecto 4.0,您可以使用适配器进行查询,然后将结果提供给 Ecto.Model.schema/3:

q = Ecto.Adapters.Postgres.query(Rocket.Repo,"select * from users limit 1",[])
Rocket.User.__schema__(:load,q.rows |> List.first,0)

3
目前还没有Ecto 4.0版本。 - denis.peplin

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接