Заполните виртуальные поля из запроса SQL - PullRequest
1 голос
/ 12 февраля 2020

Я имею дело с настройкой базы данных, которую я не могу изменить, и я должен использовать конкретный запрос c SQL, который вычисляет значения, которые не являются полями в таблице. Как я могу сделать эту работу в Экто? Вот мой подход и проблема, с которой я столкнулся:

Настройка

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

После этого я создаю пару продуктов.

x

Я добавляю виртуальное x поле в product:

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 для каждой записи, но product перечисляет x как nil. Как я могу решить эту проблему? Как я могу заполнить virtual поля в execute_and_load/3?

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
  }
]

Я открыт для альтернативных решений данной проблемы. Я не могу рассчитать значение x в моей программе Elixir. Я должен использовать SQL, чтобы вычислить это, и я хочу использовать Ecto.

Ответы [ 2 ]

2 голосов
/ 12 февраля 2020

По моему мнению, вам лучше заставить 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)
0 голосов
/ 12 февраля 2020

Ваш лучший подход - выбрать все, что вам нужно для структуры, а затем переместить ее на 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

Преимущество этого подхода заключается в том, что я не использую необработанный запрос строки. Ваши расчеты должны go внутри части фрагмента.

...