向模型添加按位虚拟列

时间:2012-02-25 20:10:46

标签: ruby-on-rails model bit-manipulation

我正在现有数据库上构建此RoR站点。数据库上的用户模型有一个名为“secret”的列,它是一个按位整数,用于保存用户设置为secret(名字,姓氏等)的列的信息。

变量是2的幂,例如:姓氏= 1<< 1 = 2,名字= 1<< 2 = 4,电子邮件== 1<< 3 = 8等等。所以如果用户设置了名字&电子邮件为秘密,列值变为4 + 8 = 12.

现在,我正在尝试找到一种将这些虚拟列实现为Rails模型的通用方法。所以,我可以做(只是一个虚拟的例子,重点是,我想检索和存储状态):

if user.secret_email?
  user.secret_name_last = true
  user.secret_name_first = false
end

如何将这些虚拟列整齐地实现到模型(不修改现有数据库)?目前我已经跟随了。它有效,但它并不整洁。由于我有20个秘密列,代码看起来非常丑陋

  SECRET_NAME_LAST    = (1 << 1) # 2
  attr_accessible :secret_name_last
  def secret_name_last; secret & SECRET_NAME_LAST > 0 unless secret.nil?; end
  def secret_name_last=(value); secret_set_value(SECRET_NAME_LAST, value); end

  SECRET_NAME_FIRST   = (1 << 2) # 4
  attr_accessible :secret_name_first
  def secret_name_first; secret & SECRET_NAME_FIRST > 0 unless secret.nil?; end
  def secret_name_first=(value); secret_set_value(SECRET_NAME_FIRST, value); end

  SECRET_EMAIL        = (1 << 3) # 8
  attr_accessible :secret_email
  def secret_email; secret & SECRET_EMAIL > 0 unless secret.nil?; end
  def secret_email=(value); secret_set_value(SECRET_EMAIL, value); end

  ***snip (17 more)***

  private
  def secret_set_value(item, value)
    if self.secret.nil?
      self.secret = 0
    end

    if value == "1" || value == true || value == 1
      # Add item to secret column (if it doesn't exist)
      if self.secret & item == 0
        self.secret += item
      end
    else
      # Remove item from secret column (if it exists)
      if self.secret & item > 0
        self.secret -= item
      end
    end
  end

我可以做的事情很好

as_bitwise :secret_name_first, :column=>'secret', :value=>4
as_bitwise :secret_name_last,  :column=>'secret', :value=>2

甚至,

as_bitwise :secret, { :secret_name_last=>4, :secret_name_first=>2 }

修改

根据Brandan的优秀答案,这就是我目前所拥有的:

  module BitwiseColumn
    extend ActiveSupport::Concern

    module ClassMethods
      def bitwise_column(*args)
        mapping = args.extract_options!
        column_name = args.shift
        real_column_name = args.shift

        logger.debug "Initializing bitwisecolumn, column: " + column_name.to_s

        mapping.each_pair do |attribute, offset|
          logger.debug "\tSetting a pair: offset: " + offset.to_s + ", " + attribute.to_s
          mask = 2 ** offset

          class_eval %{
            attr_accessible :#{column_name}_#{attribute}
            def #{column_name}_#{attribute}?
              #{real_column_name} & #{mask} > 0 unless #{real_column_name}.nil?
            end

            def #{column_name}_#{attribute}=(value)
              if self.#{real_column_name}.nil?
                self.#{real_column_name} = 0
              end

              if value == "1" || value == true || value == 1
                if self.#{real_column_name} & #{mask} == 0
                  self.#{real_column_name} += #{mask}
                end
              else
                if self.#{real_column_name} & #{mask} > 0
                  self.#{real_column_name} -= #{mask}
                end
              end
            end
          }
        end
      end
    end
  end

这允许我使用:

  bitwise_column :secret, :realsecretcolumnatdatabase, :name_last=>1, :name_first=>2, :email=>3, :picture=>5, :dob=>6, :place=>12

之后,我可以调用User.first.secret_name_last?等

1 个答案:

答案 0 :(得分:3)

您可以使用class_eval来干预您的代码。我还建议将此行为分解为与User类分开的某种模块,以便您可以彻底地测试它并与其他User特定行为分开。

与您一样,我倾向于使用所需的API启动这些类型的任务并向后工作。我在我的模型中开始使用它:

class User < ActiveRecord::Base
  include BitwiseColumn

  bitwise_column :secret, :first_name => 1, :last_name => 2
end

传递给bitwise_column的哈希将虚拟属性名称映射为其掩码值作为指数。我觉得这比自己记住2的力量更容易管理: - )

然后我创建了mixin:

module BitwiseColumn
  extend ActiveSupport::Concern

  module ClassMethods
    def bitwise_column(*args)
      mapping = args.extract_options!
      column_name = args.shift

      mapping.each_pair do |attribute, offset|
        mask = 2 ** offset

        class_eval %{
          def secret_#{attribute}?
            #{column_name} & #{mask} > 0 unless #{column_name}.nil?
          end

          def secret_#{attribute}=(value)
            if self.#{column_name}.nil?
              self.#{column_name} = 0
            end

            if value == "1" || value == true || value == 1
              if self.#{column_name} & #{mask} == 0
                self.#{column_name} += #{mask}
              end
            else
              if self.#{column_name} & #{mask} > 0
                self.#{column_name} -= #{mask}
              end
            end
          end
        }
      end
    end
  end
end

这个mixin为每个虚拟属性创建两个实例方法,一个用?,一个用=,因为这似乎就是你所追求的。我使用现有的逻辑进行按位运算,这似乎完美无缺。