Ruby on Rails | Screencasts | Download | Documentation | Weblog | Community | Source

Changeset 7315

Show
Ignore:
Timestamp:
08/14/07 08:53:02 (9 months ago)
Author:
nzkoz
Message:

Change the implementation of ActiveRecord's attribute reader and writer methods:

  • Generate Reader and Writer methods which cache attribute values in hashes. This is to avoid repeatedly parsing the same date or integer columns.
  • Move the attribute related methods out to attribute_methods.rb to de-clutter base.rb
  • Change exception raised when users use find with :select then try to access a skipped column. Plugins could override missing_attribute() to lazily load the columns.
  • Move method definition to the class, instead of the instance
  • Always generate the readers, writers and predicate methods.
Files:

Legend:

Unmodified
Added
Removed
Modified
Copied
Moved
  • trunk/activerecord/lib/active_record/attribute_methods.rb

    r4632 r7315  
    4444      end 
    4545 
     46 
     47      # Contains the names of the generated attribute methods. 
     48      def generated_methods #:nodoc: 
     49        @generated_methods ||= Set.new 
     50      end 
     51       
     52      def generated_methods? 
     53        !generated_methods.empty? 
     54      end 
     55       
     56      # generates all the attribute related methods for columns in the database 
     57      # accessors, mutators and query methods 
     58      def define_attribute_methods 
     59        return if generated_methods? 
     60        columns_hash.each do |name, column| 
     61          unless instance_methods.include?(name) 
     62            if self.serialized_attributes[name] 
     63              define_read_method_for_serialized_attribute(name) 
     64            else 
     65              define_read_method(name.to_sym, name, column) 
     66            end 
     67          end 
     68 
     69          unless instance_methods.include?("#{name}=") 
     70            define_write_method(name.to_sym) 
     71          end 
     72 
     73          unless instance_methods.include?("#{name}?") 
     74            define_question_method(name) 
     75          end 
     76        end 
     77      end 
     78      alias :define_read_methods :define_attribute_methods 
     79 
     80 
     81 
    4682      private 
    4783        # Suffixes a, ?, c become regexp /(a|\?|c)$/ 
     
    5591          @@attribute_method_suffixes ||= [] 
    5692        end 
    57     end 
     93         
     94        # Define an attribute reader method.  Cope with nil column. 
     95        def define_read_method(symbol, attr_name, column) 
     96          cast_code = column.type_cast_code('v') if column 
     97          access_code = cast_code ? "(v=@attributes['#{attr_name}']) && #{cast_code}" : "@attributes['#{attr_name}']" 
     98 
     99          unless attr_name.to_s == self.primary_key.to_s 
     100            access_code = access_code.insert(0, "missing_attribute('#{attr_name}', caller) unless @attributes.has_key?('#{attr_name}'); ") 
     101          end 
     102           
     103          evaluate_attribute_method attr_name, "def #{symbol}; @attributes_cache['#{attr_name}'] ||= begin; #{access_code}; end; end" 
     104        end 
     105 
     106        # Define read method for serialized attribute. 
     107        def define_read_method_for_serialized_attribute(attr_name) 
     108          evaluate_attribute_method attr_name, "def #{attr_name}; unserialize_attribute('#{attr_name}'); end" 
     109        end 
     110 
     111        # Define an attribute ? method. 
     112        def define_question_method(attr_name) 
     113          evaluate_attribute_method attr_name, "def #{attr_name}?; query_attribute('#{attr_name}'); end", "#{attr_name}?" 
     114        end 
     115 
     116        def define_write_method(attr_name) 
     117          evaluate_attribute_method attr_name, "def #{attr_name}=(new_value);write_attribute('#{attr_name}', new_value);end", "#{attr_name}=" 
     118        end 
     119 
     120        # Evaluate the definition for an attribute related method 
     121        def evaluate_attribute_method(attr_name, method_definition, method_name=attr_name) 
     122 
     123          unless method_name.to_s == primary_key.to_s 
     124            generated_methods << method_name 
     125          end 
     126 
     127          begin 
     128            class_eval(method_definition) 
     129          rescue SyntaxError => err 
     130            generated_methods.delete(attr_name) 
     131            if logger 
     132              logger.warn "Exception occurred during reader method compilation." 
     133              logger.warn "Maybe #{attr_name} is not a valid Ruby identifier?" 
     134              logger.warn "#{err.message}" 
     135            end 
     136          end 
     137        end 
     138    end #  ClassMethods 
     139 
     140 
     141    # Allows access to the object attributes, which are held in the @attributes hash, as were 
     142    # they first-class methods. So a Person class with a name attribute can use Person#name and 
     143    # Person#name= and never directly use the attributes hash -- except for multiple assigns with 
     144    # ActiveRecord#attributes=. A Milestone class can also ask Milestone#completed? to test that 
     145    # the completed attribute is not nil or 0. 
     146    # 
     147    # It's also possible to instantiate related objects, so a Client class belonging to the clients 
     148    # table with a master_id foreign key can instantiate master through Client#master. 
     149    def method_missing(method_id, *args, &block) 
     150      method_name = method_id.to_s 
     151 
     152      # If we haven't generated any methods yet, generate them, then 
     153      # see if we've created the method we're looking for. 
     154      if !self.class.generated_methods? 
     155        self.class.define_attribute_methods 
     156        if self.class.generated_methods.include?(method_name) 
     157          return self.send(method_id, *args, &block) 
     158        end 
     159      end 
     160       
     161      if self.class.primary_key.to_s == method_name 
     162        id 
     163      elsif md = self.class.match_attribute_method?(method_name) 
     164        attribute_name, method_type = md.pre_match, md.to_s 
     165        if @attributes.include?(attribute_name) 
     166          __send__("attribute#{method_type}", attribute_name, *args, &block) 
     167        else 
     168          super 
     169        end 
     170      elsif @attributes.include?(method_name) 
     171        read_attribute(method_name) 
     172      else 
     173        super 
     174      end 
     175    end 
     176 
     177    # Returns the value of the attribute identified by <tt>attr_name</tt> after it has been typecast (for example, 
     178    # "2004-12-12" in a data column is cast to a date object, like Date.new(2004, 12, 12)). 
     179    def read_attribute(attr_name) 
     180      attr_name = attr_name.to_s 
     181      if !(value = @attributes[attr_name]).nil? 
     182        if column = column_for_attribute(attr_name) 
     183          if unserializable_attribute?(attr_name, column) 
     184            unserialize_attribute(attr_name) 
     185          else 
     186            column.type_cast(value) 
     187          end 
     188        else 
     189          value 
     190        end 
     191      else 
     192        nil 
     193      end 
     194    end 
     195 
     196    def read_attribute_before_type_cast(attr_name) 
     197      @attributes[attr_name] 
     198    end 
     199 
     200    # Returns true if the attribute is of a text column and marked for serialization. 
     201    def unserializable_attribute?(attr_name, column) 
     202      column.text? && self.class.serialized_attributes[attr_name] 
     203    end 
     204 
     205    # Returns the unserialized object of the attribute. 
     206    def unserialize_attribute(attr_name) 
     207      unserialized_object = object_from_yaml(@attributes[attr_name]) 
     208 
     209      if unserialized_object.is_a?(self.class.serialized_attributes[attr_name]) || unserialized_object.nil? 
     210        @attributes[attr_name] = unserialized_object 
     211      else 
     212        raise SerializationTypeMismatch, 
     213          "#{attr_name} was supposed to be a #{self.class.serialized_attributes[attr_name]}, but was a #{unserialized_object.class.to_s}" 
     214      end 
     215    end 
     216   
     217 
     218    # Updates the attribute identified by <tt>attr_name</tt> with the specified +value+. Empty strings for fixnum and float 
     219    # columns are turned into nil. 
     220    def write_attribute(attr_name, value) 
     221      attr_name = attr_name.to_s 
     222      @attributes_cache.delete(attr_name) 
     223      if (column = column_for_attribute(attr_name)) && column.number? 
     224        @attributes[attr_name] = convert_number_column_value(value) 
     225      else 
     226        @attributes[attr_name] = value 
     227      end 
     228    end 
     229 
     230 
     231    def query_attribute(attr_name) 
     232      unless value = read_attribute(attr_name) 
     233        false 
     234      else 
     235        column = self.class.columns_hash[attr_name] 
     236        if column.nil? 
     237          if Numeric === value || value !~ /[^0-9]/ 
     238            !value.to_i.zero? 
     239          else 
     240            !value.blank? 
     241          end 
     242        elsif column.number? 
     243          !value.zero? 
     244        else 
     245          !value.blank? 
     246        end 
     247      end 
     248    end 
     249     
     250    # A Person object with a name attribute can ask person.respond_to?("name"), person.respond_to?("name="), and 
     251    # person.respond_to?("name?") which will all return true. 
     252    alias :respond_to_without_attributes? :respond_to? 
     253    def respond_to?(method, include_priv = false) 
     254      method_name = method.to_s 
     255      if super 
     256        return true 
     257      elsif !self.class.generated_methods? 
     258        self.class.define_attribute_methods 
     259        if self.class.generated_methods.include?(method_name) 
     260          return true 
     261        end 
     262      end 
     263         
     264      if @attributes.nil? 
     265        return super 
     266      elsif @attributes.include?(method_name) 
     267        return true 
     268      elsif md = self.class.match_attribute_method?(method_name) 
     269        return true if @attributes.include?(md.pre_match) 
     270      end 
     271      super 
     272    end 
     273     
    58274 
    59275    private 
     276     
     277      def missing_attribute(attr_name, stack) 
     278        raise ActiveRecord::MissingAttributeError, "missing attribute: #{attr_name}", stack 
     279      end 
     280       
    60281      # Handle *? for method_missing. 
    61282      def attribute?(attribute_name) 
  • trunk/activerecord/lib/active_record/base.rb

    r7278 r7315  
    3636  class Rollback < StandardError #:nodoc: 
    3737  end 
     38   
     39  # Raised when you've tried to access a column, which wasn't 
     40  # loaded by your finder.  Typically this is because :select 
     41  # has been specified 
     42  class MissingAttributeError < NoMethodError 
     43  end 
    3844 
    3945  class AttributeAssignmentError < ActiveRecordError #:nodoc: 
     
    343349    @@allow_concurrency = false 
    344350 
    345     # Determines whether to speed up access by generating optimized reader 
    346     # methods to avoid expensive calls to method_missing when accessing 
    347     # attributes by name. You might want to set this to false in development 
    348     # mode, because the methods would be regenerated on each request. 
    349     cattr_accessor :generate_read_methods, :instance_writer => false 
    350     @@generate_read_methods = true 
    351      
    352351    # Specifies the format to use when dumping the database schema with Rails' 
    353352    # Rakefile.  If :sql, the schema is dumped as (potentially database- 
     
    876875      end 
    877876 
    878       # Contains the names of the generated reader methods. 
    879       def read_methods #:nodoc: 
    880         @read_methods ||= Set.new 
    881       end 
    882  
    883877      # Resets all the cached information about columns, which will cause them to be reloaded on the next request. 
    884878      def reset_column_information 
    885         read_methods.each { |name| undef_method(name) } 
    886         @column_names = @columns = @columns_hash = @content_columns = @dynamic_methods_hash = @read_methods = @inheritance_column = nil 
     879        generated_methods.each { |name| undef_method(name) } 
     880        @column_names = @columns = @columns_hash = @content_columns = @dynamic_methods_hash = @generated_methods = @inheritance_column = nil 
    887881      end 
    888882 
     
    11011095 
    11021096          object.instance_variable_set("@attributes", record) 
     1097          object.instance_variable_set("@attributes_cache", Hash.new) 
    11031098          object 
    11041099        end 
     
    12851280        def all_attributes_exists?(attribute_names) 
    12861281          attribute_names.all? { |name| column_methods_hash.include?(name.to_sym) } 
    1287         end 
     1282        end         
    12881283 
    12891284        def attribute_condition(argument) 
     
    16401635      def initialize(attributes = nil) 
    16411636        @attributes = attributes_from_column_definition 
     1637        @attributes_cache = {} 
    16421638        @new_record = true 
    16431639        ensure_proper_type 
     
    16531649        column = column_for_attribute(attr_name) 
    16541650         
    1655         if self.class.generate_read_methods 
    1656           define_read_method(:id, attr_name, column) 
    1657           # now that the method exists, call it 
    1658           self.send attr_name.to_sym 
    1659         else 
    1660           read_attribute(attr_name) 
    1661         end 
     1651        self.class.send(:define_read_method, :id, attr_name, column) 
     1652        # now that the method exists, call it 
     1653        self.send attr_name.to_sym 
     1654 
    16621655      end 
    16631656 
     
    17881781        clear_association_cache 
    17891782        @attributes.update(self.class.find(self.id, options).instance_variable_get('@attributes')) 
     1783        @attributes_cache = {} 
    17901784        self 
    17911785      end 
     
    19031897      end 
    19041898 
    1905       # For checking respond_to? without searching the attributes (which is faster). 
    1906       alias_method :respond_to_without_attributes?, :respond_to? 
    1907  
    1908       # A Person object with a name attribute can ask person.respond_to?("name"), person.respond_to?("name="), and 
    1909       # person.respond_to?("name?") which will all return true. 
    1910       def respond_to?(method, include_priv = false) 
    1911         if @attributes.nil? 
    1912           return super 
    1913         elsif attr_name = self.class.column_methods_hash[method.to_sym] 
    1914           return true if @attributes.include?(attr_name) || attr_name == self.class.primary_key 
    1915           return false if self.class.read_methods.include?(attr_name) 
    1916         elsif @attributes.include?(method_name = method.to_s) 
    1917           return true 
    1918         elsif md = self.class.match_attribute_method?(method.to_s) 
    1919           return true if @attributes.include?(md.pre_match) 
    1920         end 
    1921         # super must be called at the end of the method, because the inherited respond_to? 
    1922         # would return true for generated readers, even if the attribute wasn't present 
    1923         super 
    1924       end 
    1925  
    19261899      # Just freeze the attributes hash, such that associations are still accessible even on destroyed records. 
    19271900      def freeze 
     
    19991972      end 
    20001973 
    2001  
    2002       # Allows access to the object attributes, which are held in the @attributes hash, as were 
    2003       # they first-class methods. So a Person class with a name attribute can use Person#name and 
    2004       # Person#name= and never directly use the attributes hash -- except for multiple assigns with 
    2005       # ActiveRecord#attributes=. A Milestone class can also ask Milestone#completed? to test that 
    2006       # the completed attribute is not nil or 0. 
    2007       # 
    2008       # It's also possible to instantiate related objects, so a Client class belonging to the clients 
    2009       # table with a master_id foreign key can instantiate master through Client#master. 
    2010       def method_missing(method_id, *args, &block) 
    2011         method_name = method_id.to_s 
    2012         if @attributes.include?(method_name) or 
    2013             (md = /\?$/.match(method_name) and 
    2014             @attributes.include?(query_method_name = md.pre_match) and 
    2015             method_name = query_method_name) 
    2016           if self.class.read_methods.empty? && self.class.generate_read_methods 
    2017             define_read_methods 
    2018             # now that the method exists, call it 
    2019             self.send method_id.to_sym 
    2020           else 
    2021             md ? query_attribute(method_name) : read_attribute(method_name) 
    2022           end 
    2023         elsif self.class.primary_key.to_s == method_name 
    2024           id 
    2025         elsif md = self.class.match_attribute_method?(method_name) 
    2026           attribute_name, method_type = md.pre_match, md.to_s 
    2027           if @attributes.include?(attribute_name) 
    2028             __send__("attribute#{method_type}", attribute_name, *args, &block) 
    2029           else 
    2030             super 
    2031           end 
    2032         else 
    2033           super 
    2034         end 
    2035       end 
    2036  
    2037       # Returns the value of the attribute identified by <tt>attr_name</tt> after it has been typecast (for example, 
    2038       # "2004-12-12" in a data column is cast to a date object, like Date.new(2004, 12, 12)). 
    2039       def read_attribute(attr_name) 
    2040         attr_name = attr_name.to_s 
    2041         if !(value = @attributes[attr_name]).nil? 
    2042           if column = column_for_attribute(attr_name) 
    2043             if unserializable_attribute?(attr_name, column) 
    2044               unserialize_attribute(attr_name) 
    2045             else 
    2046               column.type_cast(value) 
    2047             end 
    2048           else 
    2049             value 
    2050           end 
    2051         else 
    2052           nil 
    2053         end 
    2054       end 
    2055  
    2056       def read_attribute_before_type_cast(attr_name) 
    2057         @attributes[attr_name] 
    2058       end 
    2059  
    2060       # Called on first read access to any given column and generates reader 
    2061       # methods for all columns in the columns_hash if 
    2062       # ActiveRecord::Base.generate_read_methods is set to true. 
    2063       def define_read_methods 
    2064         self.class.columns_hash.each do |name, column| 
    2065           unless respond_to_without_attributes?(name) 
    2066             if self.class.serialized_attributes[name] 
    2067               define_read_method_for_serialized_attribute(name) 
    2068             else 
    2069               define_read_method(name.to_sym, name, column) 
    2070             end 
    2071           end 
    2072  
    2073           unless respond_to_without_attributes?("#{name}?") 
    2074             define_question_method(name) 
    2075           end 
    2076         end 
    2077       end 
    2078  
    2079       # Define an attribute reader method.  Cope with nil column. 
    2080       def define_read_method(symbol, attr_name, column) 
    2081         cast_code = column.type_cast_code('v') if column 
    2082         access_code = cast_code ? "(v=@attributes['#{attr_name}']) && #{cast_code}" : "@attributes['#{attr_name}']" 
    2083          
    2084         unless attr_name.to_s == self.class.primary_key.to_s 
    2085           access_code = access_code.insert(0, "raise NoMethodError, 'missing attribute: #{attr_name}', caller unless @attributes.has_key?('#{attr_name}'); ") 
    2086           self.class.read_methods << attr_name 
    2087         end 
    2088          
    2089         evaluate_read_method attr_name, "def #{symbol}; #{access_code}; end" 
    2090       end 
    2091        
    2092       # Define read method for serialized attribute. 
    2093       def define_read_method_for_serialized_attribute(attr_name) 
    2094         unless attr_name.to_s == self.class.primary_key.to_s 
    2095           self.class.read_methods << attr_name 
    2096         end 
    2097          
    2098         evaluate_read_method attr_name, "def #{attr_name}; unserialize_attribute('#{attr_name}'); end" 
    2099       end 
    2100             
    2101       # Define an attribute ? method. 
    2102       def define_question_method(attr_name) 
    2103         unless attr_name.to_s == self.class.primary_key.to_s 
    2104           self.class.read_methods << "#{attr_name}?" 
    2105         end 
    2106          
    2107         evaluate_read_method attr_name, "def #{attr_name}?; query_attribute('#{attr_name}'); end" 
    2108       end 
    2109        
    2110       # Evaluate the definition for an attribute reader or ? method 
    2111       def evaluate_read_method(attr_name, method_definition) 
    2112         begin 
    2113           self.class.class_eval(method_definition) 
    2114         rescue SyntaxError => err 
    2115           self.class.read_methods.delete(attr_name) 
    2116           if logger 
    2117             logger.warn "Exception occurred during reader method compilation." 
    2118             logger.warn "Maybe #{attr_name} is not a valid Ruby identifier?" 
    2119             logger.warn "#{err.message}" 
    2120           end 
    2121         end 
    2122       end 
    2123  
    2124       # Returns true if the attribute is of a text column and marked for serialization. 
    2125       def unserializable_attribute?(attr_name, column) 
    2126         column.text? && self.class.serialized_attributes[attr_name] 
    2127       end 
    2128  
    2129       # Returns the unserialized object of the attribute. 
    2130       def unserialize_attribute(attr_name) 
    2131         unserialized_object = object_from_yaml(@attributes[attr_name]) 
    2132  
    2133         if unserialized_object.is_a?(self.class.serialized_attributes[attr_name]) || unserialized_object.nil? 
    2134           @attributes[attr_name] = unserialized_object 
    2135         else 
    2136           raise SerializationTypeMismatch, 
    2137             "#{attr_name} was supposed to be a #{self.class.serialized_attributes[attr_name]}, but was a #{unserialized_object.class.to_s}" 
    2138         end 
    2139       end 
    2140  
    2141       # Updates the attribute identified by <tt>attr_name</tt> with the specified +value+. Empty strings for fixnum and float 
    2142       # columns are turned into nil. 
    2143       def write_attribute(attr_name, value) 
    2144         attr_name = attr_name.to_s 
    2145         if (column = column_for_attribute(attr_name)) && column.number? 
    2146           @attributes[attr_name] = convert_number_column_value(value) 
    2147         else 
    2148           @attributes[attr_name] = value 
    2149         end 
    2150       end 
    2151  
    21521974      def convert_number_column_value(value) 
    21531975        case value 
     
    21561978          when '':         nil 
    21571979          else value 
    2158         end 
    2159       end 
    2160  
    2161       def query_attribute(attr_name) 
    2162         unless value = read_attribute(attr_name) 
    2163           false 
    2164         else 
    2165           column = self.class.columns_hash[attr_name] 
    2166           if column.nil? 
    2167             if Numeric === value || value !~ /[^0-9]/ 
    2168               !value.to_i.zero? 
    2169             else 
    2170               !value.blank? 
    2171             end 
    2172           elsif column.number? 
    2173             !value.zero? 
    2174           else 
    2175             !value.blank? 
    2176           end 
    21771980        end 
    21781981      end 
  • trunk/activerecord/lib/active_record/transactions.rb

    r6439 r7315  
    122122      else 
    123123        @attributes.delete(self.class.primary_key) 
    124       end 
     124        @attributes_cache.delete(self.class.primary_key) 
     125      end   
    125126      raise 
    126127    end 
  • trunk/activerecord/test/associations_test.rb

    r7279 r7315  
    14711471    assert project.respond_to?("name?") 
    14721472    assert project.respond_to?("joined_on") 
    1473     assert project.respond_to?("joined_on=") 
     1473    # given that the 'join attribute' won't be persisted, I don't 
     1474    # think we should define the mutators 
     1475    #assert project.respond_to?("joined_on=") 
    14741476    assert project.respond_to?("joined_on?") 
    14751477    assert project.respond_to?("access_level") 
    1476     assert project.respond_to?("access_level=") 
     1478    #assert project.respond_to?("access_level=") 
    14771479    assert project.respond_to?("access_level?") 
    14781480  end 
  • trunk/activerecord/test/base_test.rb

    r7278 r7315  
    345345  end 
    346346 
    347   def test_reader_generation 
    348     Topic.find(:first).title 
    349     Firm.find(:first).name 
    350     Client.find(:first).name 
    351     if ActiveRecord::Base.generate_read_methods 
    352       assert_readers(Topic,  %w(type replies_count)) 
    353       assert_readers(Firm,   %w(type)) 
    354       assert_readers(Client, %w(type ruby_type rating?)) 
    355     else 
    356       [Topic, Firm, Client].each {|klass| assert_equal klass.read_methods, {}} 
    357     end 
    358   end 
    359347 
    360348  def test_reader_for_invalid_column_names 
    361     # column names which aren't legal ruby ids 
    362     topic = Topic.find(:first) 
    363     topic.send(:define_read_method, "mumub-jumbo".to_sym, "mumub-jumbo", nil) 
    364     assert !Topic.read_methods.include?("mumub-jumbo") 
     349    Topic.send(:define_read_method, "mumub-jumbo".to_sym, "mumub-jumbo", nil) 
     350    assert !Topic.generated_methods.include?("mumub-jumbo") 
    365351  end 
    366352 
     
    792778  def test_mass_assignment_protection_against_class_attribute_writers 
    793779    [:logger, :configurations, :primary_key_prefix_type, :table_name_prefix, :table_name_suffix, :pluralize_table_names, :colorize_logging, 
    794       :default_timezone, :allow_concurrency, :generate_read_methods, :schema_format, :verification_timeout, :lock_optimistically, :record_timestamps].each do |method| 
     780      :default_timezone, :allow_concurrency, :schema_format, :verification_timeout, :lock_optimistically, :record_timestamps].each do |method| 
    795781      assert  Task.respond_to?(method) 
    796782      assert  Task.respond_to?("#{method}=") 
     
    17091695    assert_equal '"This is some really long content, longer than 50 ch..."', t.attribute_for_inspect(:content) 
    17101696  end 
    1711  
    1712   private 
    1713     def assert_readers(model, exceptions) 
    1714       expected_readers = Set.new(model.column_names - ['id']) 
    1715       expected_readers += expected_readers.map { |col| "#{col}?" } 
    1716       expected_readers -= exceptions 
    1717       assert_equal expected_readers, model.read_methods 
    1718     end 
    17191697end 
  • trunk/activerecord/test/finder_test.rb

    r7167 r7315  
    11require 'abstract_unit' 
     2require 'fixtures/author' 
    23require 'fixtures/comment' 
    34require 'fixtures/company' 
     
    130131  def test_find_only_some_columns 
    131132    topic = Topic.find(1, :select => "author_name") 
    132     assert_raises(NoMethodError) { topic.title
     133    assert_raises(ActiveRecord::MissingAttributeError) {topic.title
    133134    assert_equal "David", topic.author_name 
    134135    assert !topic.attribute_present?("title") 
    135     assert !topic.respond_to?("title") 
     136    #assert !topic.respond_to?("title") 
    136137    assert topic.attribute_present?("author_name") 
    137138    assert topic.respond_to?("author_name")