rails under the knifes3.amazonaws.com/harrisj-share/oscon2007.pdf · things you might know •...
TRANSCRIPT
Rails Under The KnifeJacob HarrisThe New York Timeshttp://open.nytimes.com/http://www.nimblecode.com/[email protected]@schizopolis.netharrisj on Flickr / Twitter / Del.icio.us /43whatever / NYC.rb / Last.fm / etc.
Things You Might Know• basic Ruby syntax
• object-oriented programming
• has_many:talks
• <%=fortin@talks%>
• and that it’s really @talks.eachdo|t|
• validates_presence_of:name
• defbefore_save(talk)
• Three things you might kinda know:
• Blocks
• Reflection
• Metaprogramming
• Commonly called magic, but...
The Stuff of Magic
Code can and should be manipulated like data
<%@talks.eachdo|t|%> <%=render_partial'talk',t%><%end%>
Blocks
Blocks
@talks.any?{|t|t.title=~/rails/i}
@talks.select{|t|sounds_cool?t}
@talks.inject{|mins,t|mins+=t.minutes}
Reflection
irb>(3.public_methods‐Object.public_methods).sort#=>["%","&","*","**","+","+@","‐","‐@","/","<<",">>","[]","^","abs","between?","ceil","chr","coerce","denominator","div","divmod","downto","floor","gcd","gcdlcm","id2name","integer?","lcm","modulo","next","nonzero?","numerator","power!","prec","prec_f","prec_i","quo","rdiv","remainder","round","rpower","singleton_method_added","size","step","succ","times","to_bn","to_f","to_i","to_int","to_r","to_sym","truncate","upto","zero?","|","~"]
Metaprogrammingconsole>>(3.public_methods‐Object.public_methods).sort#=>["%","&","*","**","+","+@","‐","‐@","/","<<",">>","[]","^","abs","ago","between?","byte","bytes","ceil","chr","coerce","day","days","denominator","div","divmod","downto","even?","exabyte","exabytes","floor","fortnight","fortnights","from_now","gcd","gcdlcm","gigabyte","gigabytes","hour","hours","id2name","integer?","kilobyte","kilobytes","lcm","megabyte","megabytes","minute","minutes","modulo","month","months","multiple_of?","next","nonzero?","numerator","odd?","ordinalize","petabyte","petabytes","power!","prec","prec_f","prec_i","quo","rdiv","remainder","round","rpower","second","seconds","since","singleton_method_added","size","step","succ","terabyte","terabytes","times","to_bn","to_f","to_i","to_int","to_r","to_sym","truncate","until","upto","week","weeks","xchr","year","years","zero?","|","~"]
Even base classes are modifiable
Metaprogramming
• Or add new code as your program runs
• define_method - specify new methods for your classes as needed
• method_missing - catch-all method that can support infinite methods
• eval - evaluate any Ruby code (be careful)
• send - dynamically invoke methods by name.
photo from Flickr user procsilas
classConference<ActiveRecord::Base has_many:talksend
Active Record
AR AssociationsclassConference<ActiveRecord::Base has_many:talksend
adds to the class these methods (among others): c.talks c.talks<< c.talks.find c.talks.empty? c.talks.create ...
defhas_many(association_id,options={},&extension) reflection=create_has_many_reflection(association_id, options,&extension) ... collection_accessor_methods(reflection, HasManyAssociation)end
has_many
defcollection_reader_method(reflection,association_proxy_class) define_method(reflection.name)do|*params| association=instance_variable_get("@#{reflection.name}")
unlessassociation.respond_to?(:loaded?) association=association_proxy_class.new(self,reflection) instance_variable_set("@#{reflection.name}",association) end
association endend
define_method
defcollection_reader_method(reflection,association_proxy_class) define_method(reflection.name)do|*params| association=instance_variable_get("@#{reflection.name}")
unlessassociation.respond_to?(:loaded?) association=association_proxy_class.new(self,reflection) instance_variable_set("@#{reflection.name}",association) end
association endend
All Together Now
MetaprogrammingBlocksReflection
defines methodsclassConference deftalks(*params) association=instance_variable_get("@#{reflection.name}")
unlessassociation.respond_to?(:loaded?) association=HasManyAssociation.new(self,reflection) instance_variable_set("@#{reflection.name}",association) end
association endend
closure
About That SQLclassHasManyAssociation<AssociationCollectiondefinitializeconstruct_sqlend
defconstruct_sql...@finder_sql="#{@reflection.klass.table_name}.#{@reflection.primary_key_name}=#{@owner.quoted_id}"@finder_sql<<"AND(#{conditions})"ifconditionsendend
Conference.find_allConference.find_by_idTalk.find_all_by_nameTalk.find_all_by_trackTalk.find_by_day_and_trackSpeaker.find_by_name_and_hobbySpeaker.find_all_by_zipcode
method_missing
• classTalk<ActiveRecord::Basebelongs_to:conferenceend
• No find methods added by script/generate
• Nothing being added by define_to.
• It even finds new columns right when I add them to the DB (cue spooky theremin music here)
Where Are Those From?
method_missingirb>3.fooNoMethodError:undefinedmethod`foo'forFixnum:Classfrom(irb):2
classObjectdefmethod_missing(method_id,*arguments)throwNoMethodError...endend
classActiveRecord::Basedefmethod_missing(method_id,*arguments) ifmatch=/^find_(all_by|by)_([_a‐zA‐Z]\w*)$/.match (method_id.to_s)
finder=determine_finder(match) attribute_names=extract_attribute_names_from_match(match) superunlessall_attributes_exists?(attribute_names)
attributes=construct_attributes_from_arguments(attribute_names,arguments)
send(finder,finder_options) else super endend
method_missing
defmethod_missing(method_id,*arguments) ifmatch=/^find_(all_by|by)_([_a‐zA‐Z]\w*)$/.match (method_id.to_s)
finder=determine_finder(match) attribute_names=extract_attribute_names_from_match(match) superunlessall_attributes_exists?(attribute_names)
attributes=construct_attributes_from_arguments(attribute_names,arguments)
send(finder,finder_options) else super endend
method_missing
if method name is find_*
see if we should find one or find all
extract columns to find by from name or extra arguments
call the finder with options, return results
else super ⇌ call Object's MM ⇌ NoMethodError
Reflection
classLocationObserver<ActiveRecord::Observer defbefore_save(location) res=MultiGeocoder.geocode(location.address) lat=res.lat lng=res.lng true endend
Observers
Calling My Observers
#whenyourappcallsLocation.savedefcreate_or_update_with_callbacks returnfalseifcallback(:before_save)==false result=create_or_update_without_callbacks callback(:after_save) resultend saves to the DB
defcallback(method) callbacks_for(method).eachdo|callback| ... ifcallback.respond_to?(method) callback.send(method,self) end...end
Doing The Callback
:before_save
Callback is anobject of some type
defcallback(:before_save) callbacks_for(:before_save).eachdo|callback| ... ifcallback.respond_to?(:before_save) callback.send(:before_save,self) end...end
Doing The Callback
Callback is your Observer
Type Is Irrelevant
Notice it's
ifcallback.respond_to?(:before_save)
NOT
ifcallback.kind_of?(ActiveRecord::Observer)
photo from Flickr user selva
photo from Flickr user phrenologist
GarbageTruck acts_as_plow
Where Classic OOP FailsclassGarbageTruck<SnowPlowend
classGarbageTruck includePlowingend
classPlow<AbstractFrontAttachmentclassTruck defadd_attachment(attach_object)endclassGarbageTruck<Truck
No Way!
No Better!
WTF?
The Ruby WayclassGarbageTruckacts_as_plow_maybeend
defacts_as_plow_maybeifsnowing? define_method('plow!')do|*params| ... endendend
Blocks
λaka
transactiondotalk.add_attendee('Jake')conference.recalc_ranking!end
Managing Resources
deftransaction(start_db_transaction=true) transaction_open=false begin ifblock_given? ifstart_db_transaction begin_db_transaction transaction_open=true end yield end rescueException=>database_transaction_rollback iftransaction_open transaction_open=false rollback_db_transaction end raise end ensure commit_db_transactioniftransaction_openend
executes your block
respond_todo|format|format.html#index.rhtmlformat.xml{render:xml =>@users.to_xml}end
RESTful Responding
RESTful Responding• Rails 1.2 allows you specify different actions
for different formats requested by the caller (eg, page for HTML, feed for XML, etc.)
• Response behavior based on complex logic:
• Caller may explicitly specify in URL
• Your app may have implicit priorities specified (eg, Atom before XML)
• Rails may also have to decide on one based on client HTTP request headers
RESTful Responding
/talks
=> return the rendered index.rhtml
/talks.xml
=> return XML format
/talks.jpg
=> return HTTP Error 406 - “Not Acceptable”
Content NegotationAccept: text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5
HTTP/1.1 includes the following request-header fields for enabling server-driven negotiation through description of user agent capabilities and user preferences: Accept (section 14.1), Accept-Charset (section 14.2), Accept-Encoding (section 14.3), Accept- Language (section 14.4), and User-Agent (section 14.43). However, an origin server is not limited to these dimensions and MAY vary the response based on any aspect of the request, including information outside the request-header fields or within extension header fields not defined by this specification.
Why Not A Case?
caseformat when:html render:html when:xml render:xml=>@users.to_xmlend
respond_todo|format|format.html#index.rhtmlformat.xml{render:xml =>@users.to_xml}end
Outer Block - yields registry
Inner Block - mime/type handler
format.xml{render:xml=>@users.to_xml}
classActionController::MimeResponds::Responder defmethod_missing(symbol,&block) mime_constant=symbol.to_s.upcase ifMime::SET.include?(Mime.const_get(mime_constant)) custom(Mime.const_get(mime_constant),&block) else super end endend
Registering a Handler:xml { render :xml ... }
stores your block to execute for MIME match
defrespond forpriorityin@mime_type_priority ifpriority===@order @responses[priority].call return #mimetypematchfound,behappyandreturn end end
eval'render(:nothing=>true,:status=>"406NotAcceptable")',@block_bindingend
Respondingpriority list of acceptableresponse MIME types
find in your list ofblocks to respond_to
error if no handlers
Thank You
www.nimblecode.com