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

Rails3 Recipe Book Gaiden

Rails3 Recipe Book Gaiden

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

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