【问题标题】:Rails model with conditional scope depending on database具有取决于数据库的条件范围的 Rails 模型
【发布时间】:2013-03-10 22:53:23
【问题描述】:

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

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

由于我当前针对的数据库是 Oracle,它使用 BITAND 函数而不是 & 运算符,因此我将条件改写如下:

{: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

这很有效,但并没有让我觉得特别优雅或像 ruby​​/rails 一样。谁能提出更好的替代方案?

【问题讨论】:

  • 你可以获取postgres_ext,看看它如何增加对PostgreSQL-specific datatypes的支持;那么您应该能够在一个地方修补特定于数据库的内容并写入您的 API 添加。是的,那是 PostgreSQL 特有的,但它是唯一想到的东西,而且这些技术是你感兴趣的。
  • 您使用的是哪个版本的 Rails?
  • @mu 太短了——当然,扩展 ActiveRecord 看起来像是解决 Oracle 特殊性的更合适的方法。谢谢!
  • @ProGNOMmers - 3.2.12,这是一个相当新的项目。

标签: ruby-on-rails ruby oracle scopes


【解决方案1】:

我认为应该有更好的方法来扩展 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, f.e. 类似的东西一起使用像这样:

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 & c。代码; IMO 这是了解 Rails 工作原理的最佳方式(这就是我花时间回答您的问题的原因:因为我对 Rails 对 Arel 关系的管理感到好奇 :-))。

【讨论】:

  • 但是您能否将extending 业务推入初始化程序并将case connection.adapter_name 翻过来,这样检查只进行一次?
  • 由于我对 Ruby 和 Rails 还很陌生,所以您不知道您的代码对我有多大的教育意义。我仍然需要做一些工作才能理解你在那里所做的一切,以及你为什么这样做,但是只是回顾一下并用撬控制台检查一下,我学到了很多有用的东西事物。谢谢!
  • @muistooshort 我更新了答案;你的意思是这样的吗? laffinkippah,不客气;-) 我用其他想法更新了答案
  • @ProGNOMmers - 我同意你的观点,对于我目前的需求,“简单”解决方案更清晰,可能更合适(因为它将所有东西都放在一个地方,而且更简单)。对我来说最有启发性的是您对connection.quote_column_name 的使用,因为一开始我一直在努力做到这一点并且不知道如何。
  • @ProGNOMmers - 我的母语不是英语,但我想我们可以将其留在仅在需要时添加抽象。 ;) 另外,是的,我正在阅读大量的 Ruby 代码,并发现它令人着迷。一个月前我开始学习 Ruby,感觉我已经取得了比在 Java 中苦苦挣扎的一年更多的进步。我很高兴!
猜你喜欢
  • 2015-01-20
  • 1970-01-01
  • 1970-01-01
  • 2014-10-11
  • 2015-02-14
  • 1970-01-01
  • 2017-02-23
  • 1970-01-01
  • 2023-04-01
相关资源
最近更新 更多