具有条件范围的Rails模型取决于数据库

时间:2013-03-08 23:19:04

标签: ruby-on-rails ruby oracle scopes

我正在为基于this railscast的模型实现范围。范围中的条件使用二进制AND运算符&,如下所示:

scope :with_role, lambda { |role| 
  {:conditions => "roles_mask & #{2**ROLES.index(role.to_s)} > 0"} 
}

由于我目前正在使用的数据库是使用BITAND函数而不是&运算符的Oracle,我已经重写了这样的条件:

{:conditions => "BITAND(roles_mask, #{2**ROLES.index(role.to_s)}) > 0"}

我的问题是,我希望尽可能保持我的代码与数据库无关,因为将来我们计划定位其他数据库。我目前的解决方案是检查我是否使用Oracle并相应地定义范围,如下所示(using_oracle是我在其他地方计算的布尔值):

if using_oracle 
  scope :with_role, lambda { |role| 
    {:conditions => "BITAND(roles_mask, #{2**ROLES.index(role.to_s)}) > 0"}
  }
else
  scope :with_role, lambda { |role| 
    {:conditions => "roles_mask & #{2**ROLES.index(role.to_s)} > 0"}
  }
end

这样可行,但不会让我觉得特别优雅或像红宝石/铁轨一样。有人可以提出更好的选择吗?

1 个答案:

答案 0 :(得分:2)

我认为应该有更好的方式来扩展Arel:我努力达到这个结果。

反正;此解决方案使用Model#extending

module BitOperations
  def bitwise_and_sql
    @bitwise_and_sql ||=
      case connection.adapter_name
      when 'Oracle' # probably wrong!
        "BITAND(%s, %s)"
      else
        "%s & %s"
      end
  end
  def bitwise_and(i, j)
    where(bitwise_and_sql % [i, j])
  end
  def bitmask(i, j)
    where('%s > 0' % scoped.bitwise_and(i, j).wheres.to_a.last.to_sql)
  end
end

p User.scoped.extending(BitOperations).bitwise_and(1, 2).bitmask(3, 4).to_sql
#=> "SELECT \"users\".* FROM \"users\"  WHERE (1 & 2) AND ((3 & 4) > 0)"

.wheres包含Arel关系;它包括Enumerable,因此我们可以检索将其转换为数组并获取最后一个元素的最后一个关系。我使用它来获取bitwise_and(i, j)的sql以便在bitmask(i, j)中使用它。我想知道是否有更好的方法从某个地方获取sql ...

.wheres引发了关于wheres弃用的警告,目前可以忽略它(它也适用于Rails 4 beta)。

您可以为User

定义类方法
class User
  def self.scope_with_bit_operations
    @scope_with_bit_operations ||= scoped.extending(BitOperations)    
  end
  def self.bitwise_and(i, j)
    scope_with_bit_operations.bitwise_and(i, j)
  end
  def self.bitmask(i, j)
    scope_with_bit_operations.bitmask(i, j)
  end
end

p User.bitwise_and(1, 2).bitmask(3, 4).to_sql
#=> "SELECT \"users\".* FROM \"users\"  WHERE (1 & 2) AND ((3 & 4) > 0)"

或所有型号:

class ActiveRecord::Base
  def self.scope_with_bit_operations
    @scope_with_bit_operations ||= scoped.extending(BitOperations)    
  end
  def self.bitwise_and(i, j)
    scope_with_bit_operations.bitwise_and(i, j)
  end
  def self.bitmask(i, j)
    scope_with_bit_operations.bitmask(i, j)
  end
end

p Post.bitwise_and(1, 2).bitmask(3, 4).to_sql
#=> "SELECT \"posts\".* FROM \"posts\"  WHERE (1 & 2) AND ((3 & 4) > 0)"

最后,您可以实现更优雅的with_role范围:

class User < ActiveRecord::Base
  ROLES = %w[admin moderator author]

  scope :with_role, ->(role) do
    # I'm a fan of quoting everything :-P
    bitmask connection.quote_column_name(:roles_mask),
            connection.quote(2**ROLES.index(role.to_s))
  end
end

p User.with_role('admin').to_sql
#=> "SELECT \"users\".* FROM \"users\"  WHERE ((\"roles_mask\" & 1) > 0)"

我必须说IMO这更像是一个概念验证:如果你不打算在其他模型中重用bitwise_andbitmask,你不需要抽象它们,所以可能你会选择类似于scope的东西像这样的东西:

class User < ActiveRecord::Base
  ROLES = %w[admin moderator author]

  BITMASK_SQL =
    case connection.adapter_name
    when 'Oracle' # probably wrong!
      "BITAND(%s, %s) > 0"
    else
      "%s & %s > 0"
    end

  scope :with_role, ->(role) do
    where BITMASK_SQL % [ connection.quote_column_name(:roles_mask), 
                          connection.quote(2**ROLES.index(role.to_s)) ]
  end
end

p User.with_role('admin').to_sql
#=> "SELECT \"users\".* FROM \"users\"  WHERE (\"roles_mask\" & 1 > 0)"

我认为规则是在你需要时添加抽象,当你不需要时不要(我不知道这句话的英文是否正确:-))

我想说另一件事:因为你是Ruby / Rails的新手,我建议你阅读很多Rails&amp; C。码; IMO这是学习Rails如何工作的最佳方式(这就是我花时间回答你的问题的原因:因为我对Arel关系的Rails管理感到好奇:-))。