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,这会改变它在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ãoDaniel 是和否。持久化和非持久化对象的业务逻辑是否完全相同,通过这种方法,您可以将所有逻辑放入 DB 层并进行start_transaction ⇒ read_state ⇒ rollback hack。

以上是关于Ruby 和 SQL 中的重复业务逻辑的主要内容,如果未能解决你的问题,请参考以下文章

sql 加载约会变更表和业务逻辑

业务逻辑层-Active Record

业务逻辑是指控制器或模型。请详细说明[重复]

域或业务逻辑层 (Vb.Net) 中的错误处理

如果每个业务逻辑都不允许缺失值,为啥要使用 Optional.of 方法[重复]

使用 Linq To SQL 时,我是不是应该在 BLL 类中使用数据访问和业务逻辑