activerecord属性保护

2010-01-30 21:31:48 +0800

最近在看rails安全方面的书,第一部分就是关于生成activerecord对象的参数保护问题。平时一直使用,今天心血来潮想起要看看源代码是如何实现的。

activerecord属性保护就是通过attr_accessible和attr_protected来声明哪些属性可以访问,哪些不可以访问。当然,这些保护只是针对new, create和update_attributes方法,对于直接使用attribute=就无能为力了。

attr_accessible的源码为

def attr_protected(*attributes)
  write_inheritable_attribute(:attr_protected, Set.new(attributes.map(&:to_s)) + (protected_attributes || []))
end

原来activerecord会生成一个attr_protected属性,来记录所有的需要被保护的字段

同样attr_accessible会生成attr_accessible属性

def attr_accessible(*attributes)
  write_inheritable_attribute(:attr_accessible, Set.new(attributes.map(&:to_s)) + (accessible_attributes || []))
end

然后,在传递attributes的时候会调remove_attributes_protected_from_mass_assignment

def remove_attributes_protected_from_mass_assignment(attributes)
  safe_attributes =
    if self.class.accessible_attributes.nil? && self.class.protected_attributes.nil?
      attributes.reject { |key, value| attributes_protected_by_default.include?(key.gsub(/\(.+/, "")) }
    elsif self.class.protected_attributes.nil?
      attributes.reject { |key, value| !self.class.accessible_attributes.include?(key.gsub(/\(.+/, "")) || attributes_protected_by_default.include?(key.gsub(/\(.+/, "")) }
    elsif self.class.accessible_attributes.nil?
      attributes.reject { |key, value| self.class.protected_attributes.include?(key.gsub(/\(.+/,"")) || attributes_protected_by_default.include?(key.gsub(/\(.+/, "")) }
    else
      raise "Declare either attr_protected or attr_accessible for #{self.class}, but not both."
    end

  removed_attributes = attributes.keys - safe_attributes.keys

  if removed_attributes.any?
    log_protected_attribute_removal(removed_attributes)
  end

  safe_attributes
end

如果没有定义attr_accessible和attr_protected,会防止修改默认的属性(primary_key属性,一般是id和inheritance属性,即type)

如果没有定义attr_protected,就只允许修改attr_accessible定义的属性,还会防止修改默认的属性

如果没有定义attr_accessible,就防止修改attr_protected定义的属性,也会防止修改默认的属性

需要注意的是,如果同时定义attr_protected和attr_accessible的话,就会抛异常

ActiveRecord destroy之后的事情

2009-08-30 13:19:02 +0800

一般的Rails应用都在对象destroy之后自动跳转到另一个页面,不再去关心被destroy的对象如何了。其实被destroy的对象虽然从数据库中被删除了,但仍然存在于内存当中。

举个例子吧,比如我们做个博客系统,有文章,有评论,当我们删除一个日志的时候,需要在日志中做下记录

class Post
  after_destroy :log

  def log
    Logger.create(:action => 'destroy', :object_type => self.class, :object_id => self.id, :object_value => self.title)
  end
end

可见,我们是在post被删除之后再做日志记录,此时我们仍然能够得到post对象,并成功记录到日志系统中去。

如果再加些BT的需求呢,要求在日志系统中同时记录子对象(即comments对象)的type, id和value。看看很简单,但是你会不会想到,comments在post之前就被删除了,我们去哪里拿这些数据呢?答案就是内存中

class Post
  has_many :comments, :dependent => :destroy
  after_destroy :log

  def log
    Logger.create(:action => 'destroy', :object_type => self.class, :object_id => self.id, :object_value => self.title, :associations_value => comments.collect(&:name).join(','))
  end
end

在comments和post相继被删除之后,我们仍然可以得到post的name和post所有commnets的name,为什么呢?

首先看看:dependennt => :destroy有什么作用

method_name = "has_many_dependent_destroy_for_#{reflection.name}".to_sym
define_method(method_name) do
  send(reflection.name).each { |o| o.destroy }
end  
before_destroy method_name

也就是说在destroy post之前,会遍历所有的comments,然后逐一删除comment。

由于ActiveRecord::AssociationProxy的作用,当遍历comments之前会调用load_target来读取所有的comments

def load_target
  if !@owner.new_record? || foreign_key_present
    begin
      if !loaded?
        if @target.is_a?(Array) && @target.any?
          @target = find_target + @target.find_all {|t| t.new_record? }
        else 
          @target = find_target
        end  
      end  
    rescue ActiveRecord::RecordNotFound
      reset
    end  
  end  

  loaded if target
  target
end  

也就是说,在删除所有的comments之前,ActiveRecord已经将post所有的comments读取过来,并赋值给@target,所以我们才能在post.destroy之后读取到post.comments的name

default_scope影响attribute的default值

2009-08-25 12:04:24 +0800

之前有个需求,发表的文章需要审核之后才能显示,于是在Post类中加了一个default_scope

default_scope :order => 'updated_at desc', :conditions => {:verify => true}

之后就发觉每次创建的post对象,其verify值总是为true,除非手动设置verify=false。当然我在migration的时候已经设置verify的default为false了。很奇怪,于是看了下rails的源代码,其中是这么定义default_scope的

def default_scope(options = {})
  self.default_scoping << { :find => options, :create => options[:conditions].is_a?(Hash) ? options[:conditions] : {} }
end

这里可以看到如果default_scope的conditions是一个Hash的话,那么这个Hash会被保存起来,并在对象initialize的时候生效

def initialize(attributes = nil)
  @attributes = attributes_from_column_definition
  @attributes_cache = {}
  @new_record = true
  ensure_proper_type
  self.attributes = attributes unless attributes.nil?
  self.class.send(:scope, :create).each { |att,value| self.send("#{att}=", value) } if self.class.send(:scoped?, :create)
  result = yield self if block_given?
  callback(:after_initialize) if respond_to_without_attributes?(:after_initialize)
  result
end

注意第7行,可以看到,在实例化Post对象时,会根据conditions的Hash值设置其verify=true。

原来如此,解决方案自然是把conditions的值从Hash改成Array即可

default_scope :order => 'updated_at desc', :conditions => ['verify = ?', true]

 

ActiveRecord Without Rails

2009-06-02 20:52:21 +0800

前几天写了个小程序,帮我选号买彩票。主要是去网上抓取历次的开奖号码,存到数据库,然后再做统计分析。

因为程序很小,所以实在不想把Java这个大胖子叫出来,就简单地在vi下写了几十行的ruby代码。由于要用到数据库,自然想到了ActiveRecord,平时都是在Rails环境下用的,现在却是要让它脱离出来,闹独立。

首先是在mysql中新建数据库:

mysqladmin -uroot create caipiao

接着当然应该是定义database.yml

adapter: mysql
encoding: utf8
database: caipiao
username: root
password:
socket: /var/run/mysqld/mysqld.sock

定义migration,新建db/migrate目录,新建migration文件,注意前面加上数字前缀,001_xxx, 002_yyy,migration文件内容就和rails中的一模一样。

class CreateRedBlueBalls < ActiveRecord::Migration
  def self.up
    create_table :red_blue_balls do |t|
      t.integer :number
    end
  end

  def self.down
    drop_table :red_blue_balls
  end
end

最关键的是Rakefile文件

require 'active_record'
require 'yaml'

task :default => :migrate

task :migrate => :environment do
  ActiveRecord::Migrator.migrate('db/migrate', ENV["VERSION"] ? ENV["VERSION"].to_i : nil )
end

task :environment do
  ActiveRecord::Base.establish_connection(YAML::load(File.open('database.yml')))
end

在environment任务中读取database.yml配置文件,在migrate任务中,根据VERSION定义数据库。

剩下的就是定义好model文件了:

require 'rubygems'
require 'active_record'
require 'yaml'

dbconfig = YAML::load(File.open('database.yml'))
ActiveRecord::Base.establish_connection(dbconfig)

class RedBlueBall < ActiveRecord::Base
  has_many :red_balls
  has_one :blue_ball

  validates_uniqueness_of :number
end

最后你只需要在命令行输入rake即可运行migration,RedBlueBall就可以像在Rails中一样来使用了。

可以看到,Rails的约定帮我们做了很多事情,定义好了Rakefile,所有的model都会自动require相应的gems,读取database.yml配置文件。

使用ActiveRecord的conditions最基本的方法就是数组:  

                            :conditions => ['first_name = ? and middle_name = ? and last_name = ?', 'George', 'W', 'Bush']

更灵活的方法是使用Hash来组织:

                            :conditions => {:first_name => 'George', :middle_name => 'W', :last_name => 'Bush'}  
                            

这样在动态构建查询条件的情况非常有帮助,比如根据查询参数来构建conditions:

                            conditions = {}  
                            conditions.merge!({:first_name => params[:first_name]}) if params[:first_name]  
                            conditions.merge!({:first_name => params[:middle_name]}) if params[:middle_name]  
                            conditions.merge!({:first_name => params[:last_name]}) if params[:last_name]  
                            :conditions => conditions  
                            

但是Hash conditions也有其限制,不支持LIKE,不支持Not Null等等,能不能结合Array Conditions的强大和Hash Conditions的灵活呢?答案当然是肯定的:

                            conditions = []      
                            conditions << ['first_name LIKE ?', "%#{params[:first_name]}%"] if params[:first_name]      
                            conditions << ['middle_name LIKE ?', "%#{params[:middle_name]}%"] if params[:middle_name]      
                            conditions << ['last_name LIKE ?', "%#{params[:last_name]}%"] if params[:last_name]      
                            :conditions => [conditions.transpose.first.join(' AND '), *conditions.transpose.last]  
                            

其实就是通过数组转置来构建查询语句的。