在 SQL 连接查询期间计算 Ecto 中的虚拟字段
Getting a virtual field in Ecto calculated during a SQL Join Query
这可能更多的是 SQL 问题,而不是 Elixir/Etco 问题。
我有用户-交易-商家的多对多关系,其中用户通过交易拥有许多商家,而商家通过交易拥有许多客户。这是很典型的。通过执行以下操作,我可以通过 Ecto 获得商家的所有客户:
def find_merchant_customers(%{"merchant_id" => id}) do
merchant = Repo.get(Merchant, id)
Repo.all assoc(merchant, :customers)
end
如果我想为特定商户的用户查找余额,我有这样的 SQL 查询,它汇总交易并为该商户生成余额。
def customer_balance(user_id: user_id, merchant_id: merchant_id) do
q = from t in Transaction,
select: fragment("SUM(CASE WHEN ? = 'credit' THEN (?) ELSE - (?) END)", t.type, t.amount, t.amount),
where: t.user_id == ^user_id and t.merchant_id == ^merchant_id
balance = Repo.one(q) || 0
do_balance(balance, "asset")
|> Money.new(:USD)
end
问题
如何将这两个操作组合到一个查询中,以便 Join 检索用户列表并填充用户中 Balance 的虚拟属性。我知道我可以 运行 第一个查询并获取用户列表,然后通过检索每个用户的每个余额来转换该数据,尽管这看起来效率很低。另一种方法可能是了解如何将 select fragement(
作为子查询分配给查询中的属性。任何指导都会有所帮助。
我的用户模型
defmodule MyApp.User do
@moduledoc """
User struct for user related data
"""
import MyApp.Validation
use MyApp.Model
use Coherence.Schema
schema "my_app_users" do
field :email, :string
field :first_name, :string
field :last_name, :string
field :role, :integer
field :birthdate, Ecto.Date
field :address1, :string
field :address2, :string
field :city, :string
field :state, :string
field :zip, :string
field :status, :boolean, default: true
field :verified_email, :boolean, default: false
field :verified_phone, :boolean, default: false
field :mobile, :string
field :card, :string
field :sms_code, :string
field :balance, Money.Ecto, virtual: true
field :points, :integer, virtual: true
coherence_schema
has_many :transactions, MyApp.Transaction
has_many :merchants, through: [:transactions, :merchant]
timestamps()
end
@doc """
Builds a changeset based on the `struct` and `params`.
"""
def changeset(struct, params \ %{}) do
struct
|> cast(params, [:email, :first_name, :last_name, :password_hash, :role, :birthdate, :address1, :address2, :city, :state, :zip, :status, :mobile, :card, :sms_code, :status, :merchant_id, :verified_email, :verified_phone])
|> validate_required_inclusion([:email, :mobile])
|> validate_format(:email, ~r/(\w+)@([\w.]+)/)
end
defp put_password_hash(changeset) do
case changeset do
%Ecto.Changeset{valid?: true, changes: %{password: password}} ->
put_change(changeset, :password_hash, Comeonin.Bcrypt.hashpwsalt(password))
_ ->
changeset
end
end
我的商家模型
defmodule MyApp.Merchant do
@moduledoc """
Merchant Struct
Merchant has an owner of a User - Which must exist
"""
use MyApp.Model
use Arc.Ecto.Schema
schema "my_app_merchants" do
field :name, :string
field :email, :string
field :address1, :string
field :address2, :string
field :city, :string
field :zip, :string
field :state, :string
field :status, :boolean, default: true
field :description, :string
field :image, MyRewards.Avatar.Type
field :phone, :string
field :website, :string
has_many :transactions, MyApp.Transaction
has_many :customers, through: [:transactions, :user]
timestamps()
end
@doc """
Builds a changeset based on the `struct` and `params`.
"""
def changeset(struct, params \ %{}) do
struct
|> cast(params, [:name, :email, :address1, :address2, :city, :zip, :state, :status, :description, :phone, :website, :status, :category_id, :user_id])
|> cast_attachments(params, [:image])
|> validate_required([:name])
|> validate_format(:email, ~r/(\w+)@([\w.]+)/)
end
end
查询函数
def find_merchant_customers(%{"merchant_id" => id}) do
merchant = Repo.get(Merchant, id)
Repo.all assoc(merchant, :customers)
end
def customer_balance(user_id: user_id, merchant_id: merchant_id) do
q = from t in Transaction,
select: fragment("SUM(CASE WHEN ? = 'credit' THEN (?) ELSE - (?) END)", t.type, t.amount, t.amount),
where: t.user_id == ^user_id and t.merchant_id == ^merchant_id
balance = Repo.one(q) || 0
do_balance(balance, "asset")
|> Money.new(:USD)
end
将片段移动到宏中以保持代码清晰:
defmacro balance_amount(transaction) do
quote do
fragment("CASE WHEN ? = 'credit' THEN (?) ELSE - (?) END",
unquote(transaction).type, unquote(transaction).amount, unquote(transaction).amount)
end
end
使用 %{user_id, merchant_id, balance}
创建一个子查询
def user_merchant_balance do
from t in Transaction,
select: %{user_id: t.user_id, merchant_id: t.merchant_id, balance: sum(balance_amount(t))},
group_by: [t.user_id, t.merchant_id]
end
从主查询加入子查询,使用映射更新语法%{|}填充虚拟字段:
def merchant_customers(merchant_id) do
from u in User,
join: b in subquery(user_merchant_balance()), on: u.id == b.user_id,
where: b.merchant_id == ^merchant_id,
select: %{u | balance: b.balance}
end
编辑:在 Ecto 2.2 中,balance
字段可以转换为 Money.Ecto.Type
def merchant_customers(merchant_id) do
from u in User,
join: b in subquery(user_merchant_balance()), on: u.id == b.user_id,
where: b.merchant_id == ^merchant_id,
select: %{u | balance: type(b.balance, Money.Ecto.Type)}
end
这可能更多的是 SQL 问题,而不是 Elixir/Etco 问题。
我有用户-交易-商家的多对多关系,其中用户通过交易拥有许多商家,而商家通过交易拥有许多客户。这是很典型的。通过执行以下操作,我可以通过 Ecto 获得商家的所有客户:
def find_merchant_customers(%{"merchant_id" => id}) do
merchant = Repo.get(Merchant, id)
Repo.all assoc(merchant, :customers)
end
如果我想为特定商户的用户查找余额,我有这样的 SQL 查询,它汇总交易并为该商户生成余额。
def customer_balance(user_id: user_id, merchant_id: merchant_id) do
q = from t in Transaction,
select: fragment("SUM(CASE WHEN ? = 'credit' THEN (?) ELSE - (?) END)", t.type, t.amount, t.amount),
where: t.user_id == ^user_id and t.merchant_id == ^merchant_id
balance = Repo.one(q) || 0
do_balance(balance, "asset")
|> Money.new(:USD)
end
问题
如何将这两个操作组合到一个查询中,以便 Join 检索用户列表并填充用户中 Balance 的虚拟属性。我知道我可以 运行 第一个查询并获取用户列表,然后通过检索每个用户的每个余额来转换该数据,尽管这看起来效率很低。另一种方法可能是了解如何将 select fragement(
作为子查询分配给查询中的属性。任何指导都会有所帮助。
我的用户模型
defmodule MyApp.User do
@moduledoc """
User struct for user related data
"""
import MyApp.Validation
use MyApp.Model
use Coherence.Schema
schema "my_app_users" do
field :email, :string
field :first_name, :string
field :last_name, :string
field :role, :integer
field :birthdate, Ecto.Date
field :address1, :string
field :address2, :string
field :city, :string
field :state, :string
field :zip, :string
field :status, :boolean, default: true
field :verified_email, :boolean, default: false
field :verified_phone, :boolean, default: false
field :mobile, :string
field :card, :string
field :sms_code, :string
field :balance, Money.Ecto, virtual: true
field :points, :integer, virtual: true
coherence_schema
has_many :transactions, MyApp.Transaction
has_many :merchants, through: [:transactions, :merchant]
timestamps()
end
@doc """
Builds a changeset based on the `struct` and `params`.
"""
def changeset(struct, params \ %{}) do
struct
|> cast(params, [:email, :first_name, :last_name, :password_hash, :role, :birthdate, :address1, :address2, :city, :state, :zip, :status, :mobile, :card, :sms_code, :status, :merchant_id, :verified_email, :verified_phone])
|> validate_required_inclusion([:email, :mobile])
|> validate_format(:email, ~r/(\w+)@([\w.]+)/)
end
defp put_password_hash(changeset) do
case changeset do
%Ecto.Changeset{valid?: true, changes: %{password: password}} ->
put_change(changeset, :password_hash, Comeonin.Bcrypt.hashpwsalt(password))
_ ->
changeset
end
end
我的商家模型
defmodule MyApp.Merchant do
@moduledoc """
Merchant Struct
Merchant has an owner of a User - Which must exist
"""
use MyApp.Model
use Arc.Ecto.Schema
schema "my_app_merchants" do
field :name, :string
field :email, :string
field :address1, :string
field :address2, :string
field :city, :string
field :zip, :string
field :state, :string
field :status, :boolean, default: true
field :description, :string
field :image, MyRewards.Avatar.Type
field :phone, :string
field :website, :string
has_many :transactions, MyApp.Transaction
has_many :customers, through: [:transactions, :user]
timestamps()
end
@doc """
Builds a changeset based on the `struct` and `params`.
"""
def changeset(struct, params \ %{}) do
struct
|> cast(params, [:name, :email, :address1, :address2, :city, :zip, :state, :status, :description, :phone, :website, :status, :category_id, :user_id])
|> cast_attachments(params, [:image])
|> validate_required([:name])
|> validate_format(:email, ~r/(\w+)@([\w.]+)/)
end
end
查询函数
def find_merchant_customers(%{"merchant_id" => id}) do
merchant = Repo.get(Merchant, id)
Repo.all assoc(merchant, :customers)
end
def customer_balance(user_id: user_id, merchant_id: merchant_id) do
q = from t in Transaction,
select: fragment("SUM(CASE WHEN ? = 'credit' THEN (?) ELSE - (?) END)", t.type, t.amount, t.amount),
where: t.user_id == ^user_id and t.merchant_id == ^merchant_id
balance = Repo.one(q) || 0
do_balance(balance, "asset")
|> Money.new(:USD)
end
将片段移动到宏中以保持代码清晰:
defmacro balance_amount(transaction) do
quote do
fragment("CASE WHEN ? = 'credit' THEN (?) ELSE - (?) END",
unquote(transaction).type, unquote(transaction).amount, unquote(transaction).amount)
end
end
使用 %{user_id, merchant_id, balance}
创建一个子查询 def user_merchant_balance do
from t in Transaction,
select: %{user_id: t.user_id, merchant_id: t.merchant_id, balance: sum(balance_amount(t))},
group_by: [t.user_id, t.merchant_id]
end
从主查询加入子查询,使用映射更新语法%{|}填充虚拟字段:
def merchant_customers(merchant_id) do
from u in User,
join: b in subquery(user_merchant_balance()), on: u.id == b.user_id,
where: b.merchant_id == ^merchant_id,
select: %{u | balance: b.balance}
end
编辑:在 Ecto 2.2 中,balance
字段可以转换为 Money.Ecto.Type
def merchant_customers(merchant_id) do
from u in User,
join: b in subquery(user_merchant_balance()), on: u.id == b.user_id,
where: b.merchant_id == ^merchant_id,
select: %{u | balance: type(b.balance, Money.Ecto.Type)}
end