Ruby和SQL中的业务逻辑重复

6

我有一个 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中对每个对象进行分类,但这会损失数据库的性能。
如果要同时满足两种情况,似乎无法避免重复业务逻辑。但是我只是想确保一下这一点。 :)
谢谢!
3个回答

0

ActiveRecord本身暗示了持久性和业务逻辑之间的耦合程度。然而,只要模式允许,并且如果您没有真正的性能限制,第一选择应该是尽可能使您的持久性代码保持愚笨,并将此“分类”(显然是业务规则)尽可能远离数据库。

理由是与数据库相关的代码更难更慢地进行测试,而纯业务逻辑则更容易测试,而且更容易更改(特别是在系统已经投入生产后)。


0

我更愿意保持数据库简单,尽可能将逻辑放在 Ruby 代码中。由于分类不存储在数据库中,我不会期望查询返回它。

我的解决方案是定义一个 concern,它将被包含到 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

-1

数据库中是否有引入触发器的机会?如果有,我会在数据库中使用“计算”字段state_id,该字段在INSERTUPDATE时更改其值(这将带来更多的生产力优势),并使用以下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ão Daniel
@JoãoDaniel 是的和不是。无论是持久化对象还是非持久化对象,业务逻辑是否完全相同,采用这种方法,您可能会将所有逻辑放入DB层,并执行“start_transaction ⇒ read_state ⇒ rollback” hack。 - Aleksei Matiushkin

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接