简体   繁体   English

Ruby 和 SQL 中的重复业务逻辑

[英]Duplicated business logic in Ruby and SQL

I have a PORO (Plain Old Ruby Object) to deal with some business logic.我有一个 PORO(Plain Old Ruby Object)来处理一些业务逻辑。 It receives an ActiveRecord object and classify it.它接收一个ActiveRecord对象并对其进行分类。 For the sake of simplicity, take the following as an example:为简单起见,以以下为例:

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

However, I also want to do queries that groups objects based on these state_id "virtual attribute".但是,我也想根据这些state_id “虚拟属性”对对象进行分组查询。 I'm currently dealing with that by creating this attribute in the SQL queries and using it in GROUP BY statements.我目前正在通过在 SQL 查询中创建这个属性并在GROUP BY语句中使用它来处理这个问题。 See the example:看例子:

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

This way, I can get this business logic into SQL and make this kind of query in a very efficient way.通过这种方式,我可以将这种业务逻辑放入 SQL 中,并以一种非常有效的方式进行这种查询。

The problem is: I have duplicated business logic.问题是:我有重复的业务逻辑。 It exists in "ruby" code, to classify a single object and also in "SQL" to classify a collection of objects in database-level.它存在于“ruby”代码中,用于对单个对象进行分类,也存在于“SQL”中,用于对数据库级别的对象集合进行分类。

Is it a bad practice?这是一种不好的做法吗? Is there a way to avoid this?有没有办法避免这种情况? I actually was able to do this, doing the following:我实际上能够做到这一点,执行以下操作:

item = Item.find(4)
items.select(group_conditions).where(id: item.id).select('state_id')

But by doing this, I loose the ability to classify objects that are not persisted in database.但是通过这样做,我失去了对未保存在数据库中的对象进行分类的能力。 The other way out would be classifying each object in ruby, using an Iterator, but then I would lose database performance.另一种方法是使用迭代器对 ruby​​ 中的每个对象进行分类,但这样我就会失去数据库性能。

It's seem to be unavoidable to keep duplicated business logic if I need the best of the two cases.如果我需要两种情况中最好的,那么保留重复的业务逻辑似乎是不可避免的。 But I just want to be sure about this.但我只想确定这一点。 :) :)

Thanks!谢谢!

I'd rather keep database simple, and put logic in Ruby code as much as possible.我宁愿保持数据库简单,并尽可能将逻辑放在 Ruby 代码中。 Since classification is not stored in database, I won't expect the queries to return it.由于分类不存储在数据库中,我不希望查询返回它。

My solution is to define a concern which will be included into ActiveRecord model classes.我的解决方案是定义一个将包含在 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

And I fetch items from database just as usual.我像往常一样从数据库中获取项目。

items = Item.where(...)

Since each item knows its own classification value, I don't have to ask database for it.由于每个item知道自己的分类值,因此我不必向数据库询问。

items.each do |item|
  puts item.state_name
end

ActiveRecord itself implies a degree of coupling between your persistence and business logic. ActiveRecord 本身意味着持久性和业务逻辑之间存在一定程度的耦合。 However, as much as the pattern allows, and if you don't have real performance constraints, the first option should be to keep your persistence code as dumb as possible, and move this "classification" (which is clearly a business rule) away from the database as much as possible.然而,只要模式允许,如果你没有真正的性能限制,第一个选择应该是让你的持久性代码尽可能地愚蠢,并把这个“分类”(这显然是一个业务规则)移开尽可能从数据库中获取。

The rationale is that database-related code is more expensive to change (especially as your system is already in production) and generally more difficult and slower to test than pure business logic.其基本原理是更改与数据库相关的代码成本更高(尤其是当您的系统已投入生产时),并且通常比纯业务逻辑更难、更慢地测试。

Is there any chance to introduce trigger in the database?有没有机会在数据库中引入触发器? If so, I would go with “calculated” field state_id in the database, that changes it's value on both INSERT and UPDATE (this will bring even more productivity benefit) and this code in ruby:如果是这样,我会在数据库中使用“计算”​​字段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

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM