Модель Rails с условной областью в зависимости от базы данных

Я реализую область для модели, основанной на этом 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

Это работает, но не кажется мне особенно элегантным или похожим на рубин / рельсы. Может ли кто-нибудь любезно предложить лучшие альтернативы?

1 ответ

Решение

Я думаю, что должен быть лучший способ расширить Арель: я изо всех сил пытался достичь этого результата.

Тем не мение; это решение использует Model # extension:

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 содержит отношения арела; это включает EnumerableТаким образом, мы можем получить последнее отношение, преобразовав его в массив и получив последний элемент. Я использовал его, чтобы получить sql bitwise_and(i, j) для того, чтобы использовать его в 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_and а также bitmask в других моделях вам не нужно абстрагировать их, так что, вероятно, вы хорошо бы применили что-то похожее на 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 & c. код; IMO, это лучший способ узнать, как работает Rails (вот почему я потратил свое время, чтобы ответить на ваш вопрос: потому что мне было интересно узнать, как Rails управляет отношениями с Arel:-)).

Другие вопросы по тегам