Ruby 和 SQL 中的重复业务逻辑
Posted
技术标签:
【中文标题】Ruby 和 SQL 中的重复业务逻辑【英文标题】:Duplicated business logic in Ruby and SQL 【发布时间】:2016-07-12 02:09:25 【问题描述】:我有一个 PORO (Plain Old Ruby Object) 来处理一些业务逻辑。它接收一个ActiveRecord
对象并对其进行分类。为简单起见,以以下为例:
class Classificator
STATES =
1 => "Positive",
2 => "Neutral",
3 => "Negative"
def initializer(item)
@item = item
end
def name
STATES.fetch(state_id)
end
private
def state_id
return 1 if @item.value > 0
return 2 if @item.value == 0
return 3 if @item.value < 0
end
end
但是,我还想根据这些state_id
“虚拟属性”对对象进行分组查询。我目前正在通过在 SQL 查询中创建此属性并在 GROUP BY
语句中使用它来处理这个问题。看例子:
class Classificator::Query
SQL_CONDITIONS =
1 => "items.value > 0",
2 => "items.value = 0",
3 => "items.value < 0"
def initialize(relation = Item.all)
@relation = relation
end
def count
@relation.select(group_conditions).group('state_id').count
end
private
def group_conditions
'CASE ' + SQL_CONDITIONS.map do |k, v|
'WHEN ' + v.to_s + " THEN " + k.to_s
end.join(' ') + " END AS state_id"
end
end
这样,我可以把这个业务逻辑放到 SQL 中,并以一种非常有效的方式进行这种查询。
问题是:我有重复的业务逻辑。它存在于“ruby”代码中,用于对单个对象进行分类,也存在于“SQL”中,用于对数据库级别的对象集合进行分类。
这是一种不好的做法吗?有没有办法避免这种情况?我实际上能够做到这一点,执行以下操作:
item = Item.find(4)
items.select(group_conditions).where(id: item.id).select('state_id')
但是通过这样做,我失去了对未保存在数据库中的对象进行分类的能力。另一种方法是使用迭代器在 ruby 中对每个对象进行分类,但这样我会失去数据库性能。
如果我需要这两种情况中最好的一种,保留重复的业务逻辑似乎是不可避免的。但我只想确定这一点。 :)
谢谢!
【问题讨论】:
【参考方案1】:我宁愿让数据库保持简单,并尽可能将逻辑放在 Ruby 代码中。由于分类不存储在数据库中,我不希望查询返回它。
我的解决方案是定义一个关注点,该关注点将包含在 ActiveRecord 模型类中。
module Classified
extend ActiveSupport::Concern
STATES =
1 => "Positive",
2 => "Neutral",
3 => "Negative"
included do
def state_name
STATES.fetch(state_id)
end
private
def state_id
(0 <=> value.to_i) + 2
end
end
end
class Item < ActiveRecord::Base
include Classified
end
我像往常一样从数据库中获取项目。
items = Item.where(...)
由于每个item
都知道自己的分类值,所以我不必向数据库询问。
items.each do |item|
puts item.state_name
end
【讨论】:
【参考方案2】:ActiveRecord 本身意味着持久性和业务逻辑之间存在一定程度的耦合。但是,在模式允许的范围内,如果您没有真正的性能约束,第一个选择应该是让您的持久性代码尽可能地笨拙,并移动这个“分类”(显然是业务规则)尽可能远离数据库。
理由是与数据库相关的代码更改成本更高(尤其是当您的系统已经投入生产时)并且通常比纯业务逻辑更难测试且速度更慢。
【讨论】:
【参考方案3】:有没有机会在数据库中引入触发器?如果是这样,我会在数据库中使用“计算”字段state_id
,这会改变它在INSERT
和UPDATE
上的值(这将带来更多的生产力优势)和这个Ruby 中的代码:
def state_if
return @item.state_id if @item.state_id # persistent object
case @item.value
when 0 then 2
when -Float::INFINITY...0 then 3
else 1
end
end
【讨论】:
它可能会通过更新数据库中的此字段来提高性能。但是,我相信我仍然会有重复的业务逻辑,对吧?在数据库触发器和state_if
方法上。
@JoãoDaniel 是和否。持久化和非持久化对象的业务逻辑是否完全相同,通过这种方法,您可以将所有逻辑放入 DB 层并进行start_transaction ⇒ read_state ⇒ rollback
hack。以上是关于Ruby 和 SQL 中的重复业务逻辑的主要内容,如果未能解决你的问题,请参考以下文章