Reputation: 8318
I have to deal with a database setup I can not change and I have to use a specific SQL query which calculates values which are not fields in the table. How can I make this work in Ecto? Here is my approach and the problem I ran into:
$ mix phx.new testapp
$ cd testapp
$ mix ecto.create
$ mix phx.gen.html Shops Product products name price:float
$ mix ecto.migrate
After that I create a couple of products.
I add a virtual x
field to the 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
And I add the following functions to 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
and the whole SQL query is just a simplified example! In the real application I have to use a SQL query which calls stored procedures to make a calculation which will store the value in x
. So there will be some sort of SQL which I can not create with Ecto itself. In case you are interested in the SQL:
Overlapping gaps and islands in a school vacation setup
The SQL query delivers the value for x
for each entry but the product
lists the x
as nil
. How can I solve this problem? How can I fill virtual
fields in 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
}
]
I am open for alternative solutions for the given problem. I can not calculate the value of x
within my Elixir program. I have to use SQL to calculate it and I want to use Ecto.
Upvotes: 3
Views: 1222
Reputation: 582
I was able to do this using :
result = Ecto.Adapters.SQL.query!(Repo, sql, params)
x_index = Enum.find_index(result.columns, fn x -> x == "x" end)
Enum.map(result.rows, fn row ->
Repo.load(Product, {result.columns, row})
|> Map.put(:x, Enum.at(row, x_index))
end)
Upvotes: 0
Reputation: 5963
In my opinion you'd be better off getting your SQL to work with fragments.
Repo.all from p in Product, select: %{p | x: 1}
If you can't get that to work, Repo.load/2
can take a map instead of a schema.
data =
:load
|> Product.__schema__()
|> Enum.into(%{x: :integer})
|> Repo.load({columns, row})
struct(Product, data)
If you want to simplify that, you could override Product.__schema__(:load)
and use your existing &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)
Upvotes: 3
Reputation: 2554
A better approach to yours is to select everything you need to a struct and then move it to Ecto.Struct
. My approach is the following:
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
The advantage of this approach is the fact that I don't use raw string query. Your calculations should go inside of the fragment part.
Upvotes: 0