Rails Under The Knife
Jacob Harris
The New York Times
http://open.nytimes.com/
http://www.nimblecode.com/
harrisj@nytimes....
Things You Might Know
• basic Ruby syntax
• object-oriented programming
• has_many
:talks
• <%=
for
t
in
@talks
%>
 • and ...
The Stuff of Magic
• Three things you
  might kinda know:
 • Blocks
 • Reflection
 • Metaprogramming
• Commonly called
  ma...
Code can and should be
 manipulated like data
Blocks

<%
@talks.each
do
|t|
%>

 <%=
render_partial
'talk',
t
%>
<%
end
%>
Blocks

@talks.any?
{
|t|
t.title
=~
/rails/i
}

@talks.select
{|t|
sounds_cool?
t
}

@talks.inject
{|mins,
t|
mins
+=
t.m...
Reflection
irb>
(3.public_methods
‐
Object.public_methods).sort
#
=>
[quot;%quot;,
quot;&quot;,
quot;*quot;,
quot;**quot;,
...
Metaprogramming
console>>
(3.public_methods
‐
Object.public_methods).sort
#
=>
[quot;%quot;,
quot;&quot;,
quot;*quot;,
quo...
Metaprogramming
• Or add new code as your program runs
 • define_method - specify new methods for
    your classes as need...
photo from Flickr user procsilas
Active Record

class
Conference
<
ActiveRecord::Base

 has_many
:talks
end
AR Associations
class
Conference
<
ActiveRecord::Base

 has_many
:talks
end

adds to the class these methods (among others...
ActiveRecord
@talks
=
@conference.talks




SELECT
*
FROM
talks
WHERE

(talks.conference_id
=
1)
has_many

def
has_many(association_id,
options
=
{},
&extension)

 reflection
=
create_has_many_reflection(association_id,...
define_method
def
collection_reader_method(reflection,
association_proxy_class)

 define_method(reflection.name)
do
|*param...
All Together Now
def
collection_reader_method(reflection,
association_proxy_class)

 define_method(reflection.name)
do
|*p...
defines methods
class
Conference

 def
talks(*params)

 
 association
=
instance_variable_get(quot;@#{reflection.name}quot;...
About That SQL
class
HasManyAssociation
<
AssociationCollection

 def
initialize

 
 construct_sql

 end


 def
construct_...
method_missing
Conference.find_all
Conference.find_by_id
Talk.find_all_by_name
Talk.find_all_by_track
Talk.find_by_day_and...
Where Are Those From?
• class
Talk
<
ActiveRecord::Base
  

belongs_to
:conference
  end
• No find methods added by script/...
method_missing
irb>
3.foo
NoMethodError:
undefined
method

`foo'
for
Fixnum:Class








from
(irb):2

class
Object

 def...
method_missing
class
ActiveRecord::Base
def
method_missing(method_id,
*arguments)

 if
match
=
/^find_(all_by|by)_([_a‐zA‐...
method_missing
def
method_missing(method_id,
*arguments)

 if
match
=
/^find_(all_by|by)_([_a‐zA‐Z]w*)$/.match

 
 
 
 
 
...
Reflection
Observers

class
LocationObserver
<
ActiveRecord::Observer

 def
before_save(location)


   res
=
MultiGeocoder.geocode(lo...
Calling My Observers
#
when
your
app
calls
Location.save
def
create_or_update_with_callbacks

 return
false
if
callback(:b...
Doing The Callback
                         :before_save
def
callback(method)

 callbacks_for(method).each
do
|callback|

...
Doing The Callback

def
callback(:before_save)

 callbacks_for(:before_save).each
do
|callback|

 
 ...

 
 if
callback.re...
Type Is Irrelevant

Notice it's

if
callback.respond_to?(:before_save)

NOT

if
callback.kind_of?(ActiveRecord::Observer)
photo from Flickr user selva
GarbageTruck acts_as_plow




                            photo from Flickr user phrenologist
Where Classic OOP Fails
class
GarbageTruck
<
SnowPlow
                                  No Way!
end

class
GarbageTruck

 ...
The Ruby Way
class
GarbageTruck

 acts_as_plow_maybe
end

def
acts_as_plow_maybe

 if
snowing?

 
 define_method('plow!')
...
Blocks
λ
aka
Managing Resources

transaction
do

talk.add_attendee('Jake')

conference.recalc_ranking!
end
def
transaction(start_db_transaction
=
true)

 transaction_open
=
false

 begin

 
 if
block_given?

 
 
 if
start_db_tran...
RESTful Responding

respond_to
do
|format|

format.html
#
index.rhtml

format.xml
{
render
:xml


 
 
 
 
 
 =>
@users.to_...
RESTful Responding
• Rails 1.2 allows you specify different actions
  for different formats requested by the caller
  (eg,...
RESTful Responding
/talks
	 => return the rendered index.rhtml
/talks.xml
	 => return XML format
/talks.jpg

 => return HT...
Content Negotation
 Accept:
 text/xml,application/xml,application/
 xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/
 png...
Why Not A Case?

case
format

 when
:html

 
 render
:html

 when
:xml

 
 render
:xml
=>
@users.to_xml
end
respond_to
do
|format|

format.html
#
index.rhtml

format.xml
{
render
:xml


 
 
 
 
 
 =>
@users.to_xml
}
end
          ...
Registering a Handler
format.xml
{
render
:xml
=>
@users.to_xml
}
                :xml            { render :xml ... }
clas...
Responding
                               priority list of acceptable
                               response MIME types
d...
Thank You
www.nimblecode.com
Os Harris
Os Harris
Upcoming SlideShare
Loading in …5
×

Os Harris

761 views
720 views

Published on

Published in: Technology
0 Comments
0 Likes
Statistics
Notes
  • Be the first to comment

  • Be the first to like this

No Downloads
Views
Total views
761
On SlideShare
0
From Embeds
0
Number of Embeds
0
Actions
Shares
0
Downloads
0
Comments
0
Likes
0
Embeds 0
No embeds

No notes for slide

Os Harris

  1. 1. Rails Under The Knife Jacob Harris The New York Times http://open.nytimes.com/ http://www.nimblecode.com/ harrisj@nytimes.com harrisj@schizopolis.net harrisj on Flickr / Twitter / Del.icio.us / 43whatever / NYC.rb / Last.fm / etc.
  2. 2. Things You Might Know • basic Ruby syntax • object-oriented programming • has_many
:talks • <%=
for
t
in
@talks
%> • and that it’s really @talks.each
do
|t| • validates_presence_of
:name • def
before_save(talk)
  3. 3. The Stuff of Magic • Three things you might kinda know: • Blocks • Reflection • Metaprogramming • Commonly called magic, but...
  4. 4. Code can and should be manipulated like data
  5. 5. Blocks <%
@talks.each
do
|t|
%> 
 <%=
render_partial
'talk',
t
%> <%
end
%>
  6. 6. Blocks @talks.any?
{
|t|
t.title
=~
/rails/i
} @talks.select
{|t|
sounds_cool?
t
} @talks.inject
{|mins,
t|
mins
+=
t.minutes
}
  7. 7. Reflection irb>
(3.public_methods
‐
Object.public_methods).sort #
=>
[quot;%quot;,
quot;&quot;,
quot;*quot;,
quot;**quot;,
quot;+quot;,
quot;+@quot;,
quot;‐quot;,
quot;‐@quot;,
quot;/quot;,
quot;<<quot;,
quot;>>quot;,
quot;[]quot;,
 quot;^quot;,
quot;absquot;,
quot;between?quot;,
quot;ceilquot;,
quot;chrquot;,
quot;coercequot;,
quot;denominatorquot;,
quot;divquot;,
 quot;divmodquot;,
quot;downtoquot;,
quot;floorquot;,
quot;gcdquot;,
quot;gcdlcmquot;,
quot;id2namequot;,
quot;integer?quot;,
quot;lcmquot;,
 quot;moduloquot;,
quot;nextquot;,
quot;nonzero?quot;,
quot;numeratorquot;,
quot;power!quot;,
quot;precquot;,
quot;prec_fquot;,
 quot;prec_iquot;,
quot;quoquot;,
quot;rdivquot;,
quot;remainderquot;,
quot;roundquot;,
quot;rpowerquot;,
 quot;singleton_method_addedquot;,
quot;sizequot;,
quot;stepquot;,
quot;succquot;,
quot;timesquot;,
quot;to_bnquot;,
quot;to_fquot;,
 quot;to_iquot;,
quot;to_intquot;,
quot;to_rquot;,
quot;to_symquot;,
quot;truncatequot;,
quot;uptoquot;,
quot;zero?quot;,
quot;|quot;,
quot;~quot;]
  8. 8. Metaprogramming console>>
(3.public_methods
‐
Object.public_methods).sort #
=>
[quot;%quot;,
quot;&quot;,
quot;*quot;,
quot;**quot;,
quot;+quot;,
quot;+@quot;,
quot;‐quot;,
quot;‐@quot;,
quot;/quot;,
quot;<<quot;,
quot;>>quot;,
quot;[]quot;,
 quot;^quot;,
quot;absquot;,
quot;agoquot;,
quot;between?quot;,
quot;bytequot;,
quot;bytesquot;,
quot;ceilquot;,
quot;chrquot;,
quot;coercequot;,
 quot;dayquot;,
quot;daysquot;,
quot;denominatorquot;,
quot;divquot;,
quot;divmodquot;,
quot;downtoquot;,
quot;even?quot;,
 quot;exabytequot;,
quot;exabytesquot;,
quot;floorquot;,
quot;fortnightquot;,
quot;fortnightsquot;,
quot;from_nowquot;,
 quot;gcdquot;,
quot;gcdlcmquot;,
quot;gigabytequot;,
quot;gigabytesquot;,
quot;hourquot;,
quot;hoursquot;,
quot;id2namequot;,
 quot;integer?quot;,
quot;kilobytequot;,
quot;kilobytesquot;,
quot;lcmquot;,
quot;megabytequot;,
quot;megabytesquot;,
 quot;minutequot;,
quot;minutesquot;,
quot;moduloquot;,
quot;monthquot;,
quot;monthsquot;,
quot;multiple_of?quot;,
quot;nextquot;,
 quot;nonzero?quot;,
quot;numeratorquot;,
quot;odd?quot;,
quot;ordinalizequot;,
quot;petabytequot;,
quot;petabytesquot;,
 quot;power!quot;,
quot;precquot;,
quot;prec_fquot;,
quot;prec_iquot;,
quot;quoquot;,
quot;rdivquot;,
quot;remainderquot;,
quot;roundquot;,
 quot;rpowerquot;,
quot;secondquot;,
quot;secondsquot;,
quot;sincequot;,
quot;singleton_method_addedquot;,
quot;sizequot;,
 quot;stepquot;,
quot;succquot;,
quot;terabytequot;,
quot;terabytesquot;,
quot;timesquot;,
quot;to_bnquot;,
quot;to_fquot;,
quot;to_iquot;,
 quot;to_intquot;,
quot;to_rquot;,
quot;to_symquot;,
quot;truncatequot;,
quot;untilquot;,
quot;uptoquot;,
quot;weekquot;,
quot;weeksquot;,
 quot;xchrquot;,
quot;yearquot;,
quot;yearsquot;,
quot;zero?quot;,
quot;|quot;,
quot;~quot;] Even base classes are modifiable
  9. 9. 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.
  10. 10. photo from Flickr user procsilas
  11. 11. Active Record class
Conference
<
ActiveRecord::Base 
 has_many
:talks end
  12. 12. AR Associations class
Conference
<
ActiveRecord::Base 
 has_many
:talks end adds to the class these methods (among others): c.talks 
 c.talks
<< 
 c.talks.find 
 c.talks.empty? 
 c.talks.create 
 ...
  13. 13. ActiveRecord @talks
=
@conference.talks SELECT
*
FROM
talks
WHERE
 (talks.conference_id
=
1)
  14. 14. has_many def
has_many(association_id,
options
=
{},
&extension) 
 reflection
=
create_has_many_reflection(association_id,
 
 
 
 
 
 
 
 
 
 
 
 
 

options,
&extension) 
 ... 
 collection_accessor_methods(reflection,
 
 
 
 
 
 
 
 
 
 
 
 
 

HasManyAssociation) end
  15. 15. define_method def
collection_reader_method(reflection,
association_proxy_class) 
 define_method(reflection.name)
do
|*params| 
 
 association
=
instance_variable_get(quot;@#{reflection.name}quot;) 
 
 unless
association.respond_to?(:loaded?) 
 
 
 association
=
association_proxy_class.new(self,
reflection) 
 
 
 instance_variable_set(quot;@#{reflection.name}quot;,
association) 
 
 end 


 association 
 end end
  16. 16. All Together Now def
collection_reader_method(reflection,
association_proxy_class) 
 define_method(reflection.name)
do
|*params| 
 
 association
=
instance_variable_get(quot;@#{reflection.name}quot;) 
 
 unless
association.respond_to?(:loaded?) 
 
 
 association
=
association_proxy_class.new(self,
reflection) 
 
 
 instance_variable_set(quot;@#{reflection.name}quot;,
association) 
 
 end Metaprogramming 


 association Blocks 
 end Reflection end
  17. 17. defines methods class
Conference 
 def
talks(*params) 
 
 association
=
instance_variable_get(quot;@#{reflection.name}quot;) 
 
 unless
association.respond_to?(:loaded?) 
 
 
 association
=
HasManyAssociation.new(self,
reflection) 
 
 
 instance_variable_set(quot;@#{reflection.name}quot;,
 association) 
 
 end closure 
 
 association 
 end end
  18. 18. About That SQL class
HasManyAssociation
<
AssociationCollection 
 def
initialize 
 
 construct_sql 
 end 
 def
construct_sql 
 
 ... 
 
 @finder_sql
=
quot;#{@reflection.klass.table_name}.# {@reflection.primary_key_name}
=
#{@owner.quoted_id}quot; 
 
 @finder_sql
<<
quot;
AND
(#{conditions})quot;
if
 conditions 
 end end
  19. 19. method_missing Conference.find_all Conference.find_by_id Talk.find_all_by_name Talk.find_all_by_track Talk.find_by_day_and_track Speaker.find_by_name_and_hobby Speaker.find_all_by_zipcode
  20. 20. Where Are Those From? • class
Talk
<
ActiveRecord::Base 

belongs_to
:conference end • 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)
  21. 21. method_missing irb>
3.foo NoMethodError:
undefined
method
 `foo'
for
Fixnum:Class 







from
(irb):2 class
Object 
 def
method_missing(method_id,
*arguments) 
 
 throw
NoMethodError
... 
 end end
  22. 22. method_missing class
ActiveRecord::Base def
method_missing(method_id,
*arguments) 
 if
match
=
/^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) 
 
 super
unless
all_attributes_exists?(attribute_names) 
 
 attributes
=
construct_attributes_from_arguments
 (attribute_names,
arguments) 
 
 send(finder,
finder_options) 
 else 
 
 super 
 end end
  23. 23. method_missing def
method_missing(method_id,
*arguments) 
 if
match
=
/^find_(all_by|by)_([_a‐zA‐Z]w*)$/.match 
 
 
 
 
 
 
 
 
is 
 
 
 
 
 
 (method_id.to_s) if method name find_* 
 
 finder
=
determine_finder(match) find all see if we should find one or 
 
 attribute_names
=
extract_attribute_names_from_match(match) 
 
 super
unless
all_attributes_exists?(attribute_names) extract columns to find by from name or extra arguments 
 
 attributes
=
construct_attributes_from_arguments
 (attribute_names,
arguments) call the finder with options, 
 
 send(finder,
finder_options) return results 
 else 
 
 super 
 end end else super ⇌ call Object's MM ⇌ NoMethodError
  24. 24. Reflection
  25. 25. Observers class
LocationObserver
<
ActiveRecord::Observer 
 def
before_save(location) 

 res
=
MultiGeocoder.geocode(location.address) 

 lat
=
res.lat 

 lng
=
res.lng 

 true 
 end end
  26. 26. Calling My Observers #
when
your
app
calls
Location.save def
create_or_update_with_callbacks 
 return
false
if
callback(:before_save)
==
false 
 result
=
create_or_update_without_callbacks 
 callback(:after_save) 
 result end saves to the DB
  27. 27. Doing The Callback :before_save def
callback(method) 
 callbacks_for(method).each
do
|callback| 
 
 ... 
 
 if
callback.respond_to?(method) 
 
 
 callback.send(method,
self) 
 
 end 

... end Callback is an object of some type
  28. 28. Doing The Callback def
callback(:before_save) 
 callbacks_for(:before_save).each
do
|callback| 
 
 ... 
 
 if
callback.respond_to?(:before_save) 
 
 
 callback.send(:before_save,
self) 
 
 end 

... end Callback is your Observer
  29. 29. Type Is Irrelevant Notice it's if
callback.respond_to?(:before_save) NOT if
callback.kind_of?(ActiveRecord::Observer)
  30. 30. photo from Flickr user selva
  31. 31. GarbageTruck acts_as_plow photo from Flickr user phrenologist
  32. 32. Where Classic OOP Fails class
GarbageTruck
<
SnowPlow No Way! end class
GarbageTruck 
 include
Plowing No Better! end class
Plow
<
AbstractFrontAttachment class
Truck WTF? 
 def
add_attachment(attach_object) end class
GarbageTruck
<
Truck
  33. 33. The Ruby Way class
GarbageTruck 
 acts_as_plow_maybe end def
acts_as_plow_maybe 
 if
snowing? 
 
 define_method('plow!')
do
|*params| 
 
 
... 
 
 end 
 end end
  34. 34. Blocks
  35. 35. λ aka
  36. 36. Managing Resources transaction
do 
talk.add_attendee('Jake') 
conference.recalc_ranking! end
  37. 37. def
transaction(start_db_transaction
=
true) 
 transaction_open
=
false 
 begin 
 
 if
block_given? 
 
 
 if
start_db_transaction 
 
 
 
 begin_db_transaction
 
 
 
 
 transaction_open
=
true 
 
 
 end executes your block 
 
 
 yield 
 
 end 
 rescue
Exception
=>
database_transaction_rollback 
 
 if
transaction_open 
 
 
 transaction_open
=
false 
 
 
 rollback_db_transaction 
 
 end 
 
 raise 
 end 
 ensure 
 
 commit_db_transaction
if
transaction_open end
  38. 38. RESTful Responding respond_to
do
|format| 
format.html
#
index.rhtml 
format.xml
{
render
:xml 

 
 
 
 
 
 =>
@users.to_xml
} end
  39. 39. 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
  40. 40. RESTful Responding /talks => return the rendered index.rhtml /talks.xml => return XML format /talks.jpg => return HTTP Error 406 - “Not Acceptable”
  41. 41. Content Negotation Accept:
 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.
  42. 42. Why Not A Case? case
format 
 when
:html 
 
 render
:html 
 when
:xml 
 
 render
:xml
=>
@users.to_xml end
  43. 43. respond_to
do
|format| 
format.html
#
index.rhtml 
format.xml
{
render
:xml 

 
 
 
 
 
 =>
@users.to_xml
} end Outer Block - yields registry Inner Block - mime/type handler
  44. 44. Registering a Handler format.xml
{
render
:xml
=>
@users.to_xml
} :xml { render :xml ... } class
ActionController::MimeResponds::Responder 
 def
method_missing(symbol,
&block) 
 
 mime_constant
=
symbol.to_s.upcase 







 
 
 if
Mime::SET.include?(Mime.const_get (mime_constant)) 
 
 
 custom(Mime.const_get(mime_constant),
&block) 
 
 else 


 
 super stores your block to 
 
 end execute for MIME match 
 end end
  45. 45. Responding priority list of acceptable response MIME types def
respond 
 for
priority
in
@mime_type_priority 
 
 if
priority
===
@order find in your list of 
 
 
 @responses[priority].call blocks to respond_to 
 
 
 return
 
 
 
 #
mime
type
match
found,
be
happy
and
return 
 
 end 
 end 
 eval
'render(:nothing
=>
true,
:status
=>
quot;406
Not
 Acceptablequot;)',
@block_binding end error if no handlers
  46. 46. Thank You
  47. 47. www.nimblecode.com

×