Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Rails3 Recipe Book Gaiden

Sponsored · Ship Features Fearlessly Turn features on and off without deploys. Used by thousands of Ruby developers.
Avatar for Akira Matsuda Akira Matsuda
September 16, 2012

Rails3 Recipe Book Gaiden

札幌Ruby会議2012の発表スライド with @moro, @takahashim, and @tenderlove

Avatar for Akira Matsuda

Akira Matsuda

September 16, 2012
Tweet

More Decks by Akira Matsuda

Other Decks in Programming

Transcript

  1. % rails g model blog name % rails g model

    entry blog:references title body:text Blog.has_many :entries ॻ੶தͷαϯϓϧΞϓϦ
  2. % rails c > Blog.scoped.entries => [#<Blog id: 1, name:

    "Riding Rails">, #<Blog id: 2, name: "Matzʹ͖ͬ">, ...] > Blog.scoped.entries.entries.entries.entries => [#<Blog id: 1, name: "Riding Rails">, #<Blog id: 2, name: "Matzʹ͖ͬ">, ...] Ṗͷݱ৅ શͯͷRelationʹ࠷ॳ͔Β#entries͕ੜ͑ͯΔʂʁ ԿճͰ΋܁Γฦ͠ݺ΂Δʂʁ
  3. > [1, 2, 3].entries #=> [1, 2, 3] Enumerable#entries entries

    -> [object] શͯͷཁૉΛؚΉ഑ྻΛฦ͠·͢ɻ Relation#entries ??
  4. Ұ౓ಡΜͩࢠڙΛΩϟογϡ > user.emails.loaded? #=> false > user.emails SELECT "emails".* FROM

    "emails" WHERE "emails"."user_id" = 1 > user.emails.loaded? #=> true > user.emails ࠓ౓͸SQL͕࣮ߦ͞Εͳ͍ʂ
  5. Ͳ͜ʹΩϟογϡͯ͠Δͷʁ > user.emails.class #=> Array > user.emails.instance_variables #=> [] >

    user.emails.proxy_association #=> #<ActiveRecord::Associations::HasManyAssociation: 0x007f8dcf1c6df8 @loaded=true, @owner= #<User id: 1, name: "a_matsuda">, ... @target= [#<Email id: 1, user_id: 1, email: "[email protected]">...], ... (AR 3.2ͷ৔߹)
  6. ϝιουΛੜ΍ͨ͠ΓͰ͖Δ class User < ActiveRecord::Base has_many :emails do def latest_verified

    if loaded? target.select(&:verified?). sort_by(:updated_at).last else where('verified_at IS NOT NULL'). order('updated_at DESC').first end end end end by @moro
  7. About me •MOROHASHI Kyosuke •@moro on Twitter and GitHub •works

    at @esminc •One of the authors of Rails3 Recipe Book
  8. AssociationProxyΛ࢖͏ͱɺ͜ΕΛO(2+1)ʹͰ͖·͢ɻ SELECT "snippets".* FROM "snippets" WHERE "snippets"."id" = 25 LIMIT

    1 SELECT "snippets".* FROM "snippets" WHERE "snippets"."id" = 24 LIMIT 1 SELECT "snippets".* FROM "snippets" WHERE "snippets"."id" = 23 LIMIT 1 SELECT "pictures".* FROM "pictures" WHERE "pictures"."id" = 37 LIMIT 1 SELECT "snippets".* FROM "snippets" WHERE "snippets"."id" = 22 LIMIT 1 SELECT "snippets".* FROM "snippets" WHERE "snippets"."id" = 21 LIMIT 1 SELECT "snippets".* FROM "snippets" WHERE "snippets"."id" = 19 LIMIT 1 SELECT "snippets".* FROM "snippets" WHERE "snippets"."id" = 18 LIMIT 1 SELECT "pictures".* FROM "pictures" WHERE "pictures"."id" = 38 LIMIT 1
  9. ϙϦϞϧϑΟοΫΞιγΤʔγϣϯ͸ɺnݸͷ௨ৗ ͷؔ࿈ʹ෼ղͰ͖Δɻ class Paste < ActiveRecord::Base belongs_to :snippet, readonly: true,

    foreign_key: :item_id, conditions: ['EXISTS(SELECT 1 FROM snippets s WHERE s.id = ? AND pastes.item_id = s.id)', 'Snippet'] belongs_to :pictures, readonly: true, foreign_key: :item_id, conditions: ['EXISTS(SELECT 1 FROM pictures s WHERE s.id = ? AND pastes.item_id = s.id)', 'Picture'] def item (...) end end
  10. eager loadͨ͠௨ৗͷؔ࿈ͷΦϒδΣΫτΛ࢖͏ class Paste < ActiveRecord::Base belongs_to :snippet, (...) belongs_to

    :pictures, (...) def item actual_item = association(item_type.underscore.to_sym) polymorphic_item = association(:item) if actual_item.loaded? && !polymorphic_item.loaded? polymorphic_item.target = actual_item.target end super end end
  11. ͍͍ͩͨͰ͖ͨ > Paste.limit(10).includes(:snippet, :pictures).map(&:item) # => SELECT "pastes".* FROM "pastes"

    LIMIT 10 # => SELECT "snippets".* FROM "snippets" WHERE "snippets"."id" IN (1, 2, 3, 4, 5, 6) AND (EXISTS(SELECT 1 FROM "pastes" WHERE "pastes".item_type = 'Snippet' AND "pastes".item_id = "snippets".id)) # => SELECT "pictures".* FROM "pictures" WHERE "pictures"."id" IN (1, 2, 3, 4, 5, 6) AND (EXISTS(SELECT 1 FROM "pastes" WHERE "pastes".item_type = 'Picture' AND "pastes".item_id = "pictures".id))
  12. ΋ͬͱίϯύΫτʹॻ͖͍ͨ! class Paste < ActiveRecord::Base extend EagerLoadablePolymorph belongs_to :item, polymorphic:

    true eager_loadable_polymorphic_association \ :item, [:snippet, :picture] ... end
  13. ΋ͬͱίϯύΫτʹॻ͖͍ͨ! module EagerLoadablePolymorph class AssociationWriter def initialize(association, types) raise ArgumentError,

    "#{association} is not polymorphic association" unless association.options[:polymorphic] @association = association @types = types end def belong_to_them(ref_from) @types.each do |t| ref_from.belongs_to t, readonly: true, foreign_key: fk, conditions: condition_sql(t.to_s.classify.constantize) end end def define_scope(ref_from) ref_from.scope "with_#{@association.name}", ref_from.includes(*@types) end def override_accessor(ref_from) ref_from.class_eval <<-RUBY.strip_heredoc def #{@association.name} acutual_item = association(self[#{ft.dump}].underscore.to_sym) polymorphic_item = association(:#{@association.name}) if acutual_item.loaded? && !polymorphic_item.loaded? polymorphic_item.target = acutual_item.target end super end RUBY end private # aliasses def ft @association.foreign_type http://bit.ly/eager-loadable-polymorph
  14. def define_scope(ref_from) ref_from.scope "with_#{@association.name}", re end def override_accessor(ref_from) ref_from.class_eval <<-RUBY.strip_heredoc

    def #{@association.name} acutual_item = association(self[#{ft.dump} polymorphic_item = association(:#{@associa if acutual_item.loaded? && !polymorphic_it polymorphic_item.target = acutual_item.t end super end RUBY end private ΋ͬͱίϯύΫτʹॻ͖͍ͨ! http://bit.ly/eager-loadable-polymorph
  15. class Blog < AR scope :rails, where(name: ‘Riding Rails’) end

    class Post < AR scope :recent, -> { where 'posts.created_at >= ?', 1.week.ago } end class User < AR has_many :blogs, through: ... end models
  16. > Post.joins(:blog).recent.merge(Blog.rails) SELECT posts.* FROM posts INNER JOIN blogs ON

    blogs.id = posts.blog_id WHERE blogs.name = 'Riding Rails' AND (posts.created_at >= '2012-09-08 03:16:03.888840') scopeͱmerge
  17. > Post.joins(:blog).recent .merge(User. rst.blogs.scoped) SELECT posts.* FROM posts INNER JOIN

    blogs ON blogs.id = posts.blog_id INNER JOIN user_blogs ON blogs.id = user_blogs.blog_id WHERE user_blogs.user_id = 1 AND (posts.created_at >= '2012-09-08 03:14:52.839676') Associationͱmerge AR 3.2Ͱ͸ඞཁ 4.0Ͱ͸ෆཁ
  18. whereͰαϒΫΤϦ > Post.where(blog_id: Blog.rails) SELECT "posts".* FROM "posts" WHERE "posts"."blog_id"

    IN (SELECT "blogs"."id" FROM "blogs" WHERE "blogs"."name" = 'Riding Rails')
  19. ARͷwhereͷ੯͍͠ͱ͜Ζ select * from users where name = ‘tenderlove’ <=

    User.where(name: ‘tenderlove’) select * from users where name <> ‘tenderlove’ <= User.where(“name <>‘tenderlove’”) select * from users where name like ‘tender%’ <= User.where(“name like ‘tender%’”) RubyͷHash SQLͦͷ΋ͷ
  20. • Step 1: Read the API • Step 2: Write

    your code • Step 3: Annoy your friends (or profit)
  21. struct sqlite3_vfs { int iVersion; /* Structure version number */

    int szOsFile; /* Size of subclassed sqlite3_file */ int mxPathname; /* Maximum file pathname length */ sqlite3_vfs *pNext; /* Next registered VFS */ const char *zName; /* Name of this virtual file system */ void *pAppData; /* Pointer to application-specific data */ int (*xOpen)(sqlite3_vfs*, const char *zName, sqlite3_file*, int flags, int *pOutFlags); int (*xDelete)(sqlite3_vfs*, const char *zName, int syncDir); int (*xAccess)(sqlite3_vfs*, const char *zName, int flags, int *pResOut); int (*xFullPathname)(sqlite3_vfs*, const char *zName, int nOut, char *zOut); void *(*xDlOpen)(sqlite3_vfs*, const char *zFilename); void (*xDlError)(sqlite3_vfs*, int nByte, char *zErrMsg); void (*(*xDlSym)(sqlite3_vfs*,void*, const char *zSymbol))(void); void (*xDlClose)(sqlite3_vfs*, void*); int (*xRandomness)(sqlite3_vfs*, int nByte, char *zOut); int (*xSleep)(sqlite3_vfs*, int microseconds); int (*xCurrentTime)(sqlite3_vfs*, double*); int (*xGetLastError)(sqlite3_vfs*, int, char *); /* New fields may be appended in figure versions. The iVersion ** value will increment whenever this happens. */ }; Step 1: Read the API
  22. static int rbFile_close(sqlite3_file * ctx) { rubyFilePtr rfile = (rubyFilePtr)ctx;

    VALUE file = rfile->file; rb_funcall(file, rb_intern("close"), 0); return SQLITE_OK; } Example of the hooks we have to write
  23. class VFS < SQLite3::VFS def open(name, flags) OurFile.new(name, flags) end

    end class OurFile def read(...); ... end def write(...); ... end end
  24. class EvilVFS < SQLite3::VFS def open name, flags DATABase.new name,

    flags end end class DATABase < SQLite3::VFS::File def initialize name, flags super @store = File.open(name, File::RDWR | File::CREAT) @offset = 0 if File.expand_path(__FILE__) == name @store.seek DATA.pos @offset = DATA.pos end end def close @store.close end def read amt, offset @store.seek(offset + @offset) @store.read amt end def write data, offset @store.seek(offset + @offset) @store.write data end def sync opts @store.fsync end def file_size File.size(@store.path) - @offset end end Stores data to after then __END__ part
  25. SQLite3.vfs_register(EvilVFS.new) db = SQLite3::Database.new(__FILE__, nil, 'EvilVFS') db.execute(<<-eosql) create table if

    not exists users(id integer primary key, name string) eosql 100.times { db.execute(<<-eosql, 'tenderlove') insert into users(name) values (?) eosql } p db.execute('select count(*) from users') __END__
  26. % rake middleware use ActionDispatch::Static use Rack::Lock use #<ActiveSupport::Cache::Strategy::LocalCache::Middleware:0x007fa191fde078> use

    Rack::Runtime use Rack::MethodOverride use ActionDispatch::RequestId use Rails::Rack::Logger use ActionDispatch::ShowExceptions use ActionDispatch::DebugExceptions use ActionDispatch::RemoteIp use ActionDispatch::Reloader use ActionDispatch::Callbacks use ActiveRecord::ConnectionAdapters::ConnectionManagement use ActiveRecord::QueryCache use ActionDispatch::Cookies use ActionDispatch::Session::CookieStore use ActionDispatch::Flash use ActionDispatch::ParamsParser use ActionDispatch::Head use Rack::ConditionalGet use Rack::ETag use ActionDispatch::BestStandardsSupport run Test328::Application.routes ੵ·ΕͯΔmiddlewareΛ֬ೝ
  27. localhost, memcached, “hello”ͬͯݴ͏͚ͩͷ ΞϓϦ before Requests per second: 355.45 [#/sec]

    (mean) after Requests per second: 381.99 [#/sec] (mean) ๭ࣾͰ͸͜Ε͚ͩͰ1.5ഒ͘Β͍଎͘ͳͬͨͱ͍ ͏ࣄྫ΋ ϕϯν
  28. localhost, memcached, “hello”ͬͯݴ͏͚ͩͷ ΞϓϦ before Requests per second: 381.99 [#/sec]

    (mean) after Requests per second: 410.48 [#/sec] (mean) ϕϯν
  29. commit 29acc17 Author: David Heinemeier Hansson <[email protected]> Date: 2010-06-09 16:19:03

    -0400 Cut down on tasks shown in rake -T DHHʮrake -Tݟʹ͘͘Ͷʁʯ
  30. % rake -T | wc -l 32 % rake t

    | wc -l 103 (102 + 1)
  31. % rake t | grep db db:_dump db:abort_if_pending_migrations db:charset db:collation

    db:create db:create:all db:drop db:drop:all db: xtures:identify db: xtures:load db:forward db:load_con g db:migrate db:migrate:down db:migrate:redo db:migrate:reset db:migrate:status db:migrate:up db:reset db:rollback db:schema:dump db:schema:load db:schema:load_if_ruby db:seed db:sessions:clear db:sessions:create db:setup db:structure:dump db:structure:load db:structure:load_if_sql db:test:clone db:test:clone_structure db:test:load db:test:load_schema db:test:load_structure db:test:prepare db:test:purge
  32. active_decorator Gem le gem ‘active_decorator’ % bundle % rails g

    decorator user create app/decorators/user_decorator.rb
  33. user_decorator.rb app/decorators/user_decorator.rb module UserDecorator def icon image_tag icon_url end end

    ↑ͷmodule͕view_assigns࣌ʹ@userͱ͔@usersత ͳ΍ͭʹࣗಈతʹextend͞ΕΔ view͔Β࢖͏ͱ͖͸ <%= @user.icon %>
  34. ؔ࿈ઌΛdecorate͍ͨ࣌͠͸ app/decorators/post_decorator.rb module PostDecorator def summary truncate body, length: 20

    end end app/views/entries/show.html.erb render @entry.posts app/views/posts/_post.html.erb <%= post.summary %>
  35. AR::Relation#arel > Post.scoped.arel => #<Arel::SelectManager:0x007fac81ce7478 @ast= #<Arel::Nodes::SelectStatement:0x007fac81ce7428 @cores= [#<Arel::Nodes::SelectCore:0x007fac81ce73d8 @groups=[],

    @having=nil, @projections= [#<struct Arel::Attributes::Attribute relation= #<Arel::Table:0x007fac81aadc20 @aliases=[], @columns=nil, @engine= Post(id: integer, blog_id: integer, title: string, created_at: datetime, updated_at: datetime), @name="posts", ...
  36. ARel AST => SQL > Post.scoped.arel.class => Arel::SelectManager > puts

    Post.where(name: 'foo').arel.to_sql SELECT "posts".* FROM "posts" WHERE "posts"."name" = 'foo'
  37. ARGV.each do |f| File.open(f, 'r+:UTF8-MAC') do |f| str = f.read

    f.rewind f.truncate(0) f.set_encoding('UTF-8') f.write(str) end end
  38. جຊతͳEngineͷͭ͘Γ ᵓᴷᴷ app ᴹ ᵓᴷᴷ controllers ᴹ ᴹ ᵋᴷ foo

    ᴹ ᴹ ᵋᴷ bar_controller.rb ᴹ ᵓᴷᴷ helpers ᴹ ᴹ ᵋᴷ foo ᴹ ᴹ ᵋᴷ bar_helper.rb ᴹ ᵋᴷᴷ views ᴹ ᵋᴷ foo ᴹ ᵋᴷ bar ᴹ ᵋᴷ index.html.erb ᵓᴷᴷ con g ᴹ ᵋᴷ routes.rb ᵋᴷᴷ lib ᵋᴷ foo ᵋᴷ engine.rb
  39. EngineΛςετ͢Δ ςετίʔυ಺Ͱಈతʹ਌RailsΞϓϦΛ࡞ͬͯmount͢Δͷָ͕ app = Class.new(Rails::Application) app.con g.secret_token = '3b7cd727ee24e8444053437c36cc66c4' app.con

    g.session_store :cookie_store, key: '_myapp_session' app.initialize! app.routes.draw { resources(:users) } class User < ActiveRecord::Base; end class ApplicationController < ActionController::Base; end class UsersController < ApplicationController def index @users = User.all render inline: ’<%= @users.map(&:name).join("\n") %>’ end end Object.const_set(:ApplicationHelper, Module.new)
  40. Engine gem͔ΒผͷαϒEngineΛmount͍ͨ͠ ؅ཧ͕໘౗͔ͩΒGem͸Ұͭʹ͍ͨ͠ gemspec le Gem::Speci cation.new do |s| s.require_paths

    = ['lib', 'engines/foo/lib'] end ࢀߟ: https://github.com/amatsuda/hocus_pocus Engine gemʹEngineΛmount ͢Δ
  41. end