从 SQL 查询中填充虚拟字段

Fill virtual fields from a SQL query

我必须处理无法更改的数据库设置,而且我必须使用特定的 SQL 查询来计算 table 中不属于字段的值。我怎样才能在 Ecto 中完成这项工作?这是我的方法和我 运行 遇到的问题:

设置

$ mix phx.new testapp
$ cd testapp
$ mix ecto.create
$ mix phx.gen.html Shops Product products name price:float
$ mix ecto.migrate

之后我创建了几个产品。

x

我向 product 添加了一个虚拟 x 字段:

lib/testapp/shops/product.ex

defmodule Testapp.Shops.Product do
  use Ecto.Schema
  import Ecto.Changeset

  schema "products" do
    field :name, :string
    field :price, :float
    field :x, :integer, virtual: true  # <-----

    timestamps()
  end

  @doc false
  def changeset(product, attrs) do
    product
    |> cast(attrs, [:name, :price])
    |> validate_required([:name, :price])
  end
end

然后我将以下功能添加到 Testapp.Shops:

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

def list_products_with_x do
  sql = "SELECT *, 1 AS x FROM products;" # <- simplified
  execute_and_load(sql, [], Testapp.Shops.Product)
end

1 AS x 和整个 SQL 查询只是一个简化的示例!在实际应用程序中,我必须使用 SQL 查询调用存储过程进行计算,将值存储在 x 中。所以会有某种 SQL 我无法用 Ecto 本身创建的东西。如果您对 SQL 感兴趣:

问题

SQL 查询为每个条目提供 x 的值,但 productx 列为 nil。我怎么解决这个问题?如何在 execute_and_load/3 中填写 virtual 字段?

iex(1)> Testapp.Shops.list_products_with_x
[debug] QUERY OK db=1.3ms queue=2.2ms idle=8177.7ms
SELECT *, 1 AS x FROM products; []
[
  %Testapp.Shops.Product{
    __meta__: #Ecto.Schema.Metadata<:loaded, "products">,
    id: 1,
    inserted_at: ~N[2020-02-12 07:29:36],
    name: "Apple",
    price: 0.5,
    updated_at: ~N[2020-02-12 07:29:36],
    x: nil
  },
  %Testapp.Shops.Product{
    __meta__: #Ecto.Schema.Metadata<:loaded, "products">,
    id: 2,
    inserted_at: ~N[2020-02-12 07:29:47],
    name: "Orange",
    price: 0.75,
    updated_at: ~N[2020-02-12 07:29:47],
    x: nil
  }
]

我愿意为给定的问题寻找替代解决方案。我无法在我的 Elixir 程序中计算 x 的值。我必须使用 SQL 来计算它,我想使用 Ecto.

一个更好的方法是 select 将您需要的所有内容都添加到一个结构中,然后将其移动到 Ecto.Struct。我的方法如下:

def get_products() do
    query = from p in Products,
            select: %{name: p.name, price: p.price, x: fragment("1")}
    query
    |> Repo.all()
    |> Enum.map(fn el -> struct(Products, el) end)
  end

这种方法的优点是我不使用原始字符串查询。您的计算应该进入片段部分。

在我看来,你最好让 SQL 与片段一起工作。

Repo.all from p in Product, select: %{p | x: 1}

如果你不能让它工作,Repo.load/2 可以使用地图而不是模式。

data =
  :load
  |> Product.__schema__()
  |> Enum.into(%{x: :integer})
  |> Repo.load({columns, row})

struct(Product, data)

如果您想简化它,您可以覆盖 Product.__schema__(:load) 并使用现有的 &Repo.load(model, {result.columns, &1}):

schema "products" do
  ...
end

# WARNING: This could have unintended effects
# You're probably better off not poking around in Ecto internals
defoverridable __schema__: 1
def __schema__(:load), do: [{:x, :integer} | super(:load)]
def __schema__(arg), do: super(arg)