Ruby on Rails Eager Loading 加速:一次拿取所以資料
     
  
  
  
  
    這個在 rails 裡面,資料有關聯的時候,會產生的一些效能上的問題,假設我們的例子如下:
1 2 3 4 5 6 7 ┌──────────────────┐                ┌───────────────────┐ │      Author      │                │       Book        │ ├──────────────────┤                ├───────────────────┤ │   id:integer     │←───────┐       │ id:integer        │ │   name:string    │        └───────│ author_id:integer │ │                  │                │ title:string      │ └──────────────────┘                └───────────────────┘ 
 
當我們在 books controllers 拿了一群東西,像是有 all  或是 where 
1 2 @books  = Book .all@books  = Book .where(author:  @author )
 
常常接著又在 view 裡面使用 each  抓取了關聯的東西 author ,這時 @books  不知道 author  的內容所以又必須呼叫一次 SQL 指令去拿資料,所以當資料量一大的時候,會產生效能上的問題。
1 2 3 @books .each do  |book |  book.author end 
 
 
環境設置 所以在效能測試之前,先來建立環境:新建一個專案,然後用 scaffold 產生 author  和 book  還有他們之間的關係。
1 2 3 4 rails new speed_test cd  speed_testrails generate scaffold author name:string rails generate scaffold book title:string author:references 
 
在 db/seeds.rb 產生一些接下來要測試用的資料:一個作者有十本書。
1 2 3 4 5 author = Author .create! name:  "akii"  10 .times do  |i |  Book .create! title:  "book#{i} " , author:  author end 
 
migration 後開啟 rails server
1 2 rake db:migrate db:seed rails s 
 
效能測試 這是在 books controller 裡面,用 scaffold 產生的 index 如下:
1 2 3 4 def  index   @books  = Book .all end 
 
然後用瀏覽器開啟 localhost:3000/books 頁面,可以看到 rails server 會產生以下的東西。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 # rails console Started GET  "/books" for  ::1  at  2016 -11 -21  14 :10 :10  + 0800    ActiveRecord::SchemaMigration Load (0.1 ms)  SELECT  "schema_migrations".*  FROM  "schema_migrations" Processing by  BooksController#index as  HTML   Rendering books/ index.html.erb within  layouts/ application   Book Load (0.1 ms)  SELECT  "books".*  FROM  "books"   Author Load (0.1 ms)  SELECT   "authors".*  FROM  "authors" WHERE  "authors"."id" =  ? LIMIT ?  [["id", 1 ], ["LIMIT", 1 ]]   Author Load (0.1 ms)  SELECT   "authors".*  FROM  "authors" WHERE  "authors"."id" =  ? LIMIT ?  [["id", 1 ], ["LIMIT", 1 ]]   Author Load (0.1 ms)  SELECT   "authors".*  FROM  "authors" WHERE  "authors"."id" =  ? LIMIT ?  [["id", 1 ], ["LIMIT", 1 ]]   Author Load (0.1 ms)  SELECT   "authors".*  FROM  "authors" WHERE  "authors"."id" =  ? LIMIT ?  [["id", 1 ], ["LIMIT", 1 ]]   Author Load (0.1 ms)  SELECT   "authors".*  FROM  "authors" WHERE  "authors"."id" =  ? LIMIT ?  [["id", 1 ], ["LIMIT", 1 ]]   Author Load (0.1 ms)  SELECT   "authors".*  FROM  "authors" WHERE  "authors"."id" =  ? LIMIT ?  [["id", 1 ], ["LIMIT", 1 ]]   Author Load (0.1 ms)  SELECT   "authors".*  FROM  "authors" WHERE  "authors"."id" =  ? LIMIT ?  [["id", 1 ], ["LIMIT", 1 ]]   Author Load (0.1 ms)  SELECT   "authors".*  FROM  "authors" WHERE  "authors"."id" =  ? LIMIT ?  [["id", 1 ], ["LIMIT", 1 ]]   Author Load (0.1 ms)  SELECT   "authors".*  FROM  "authors" WHERE  "authors"."id" =  ? LIMIT ?  [["id", 1 ], ["LIMIT", 1 ]]   Author Load (0.1 ms)  SELECT   "authors".*  FROM  "authors" WHERE  "authors"."id" =  ? LIMIT ?  [["id", 1 ], ["LIMIT", 1 ]]   Rendered books/ index.html.erb within  layouts/ application (32.9 ms) Completed 200  OK in  216 ms (Views: 202.9 ms |  ActiveRecord: 1.8 ms) 
 
在 views/book/index.html.erb 裡面的 @books.each ,每次有需要 author  的時候,都會重新使用 SQL 指令抓取 author  的內容,所以每個 author  的 id 也會不同。
1 2 3 4 5 6 7 8 9 10 11 12 # localhost:3000/books Title	         Author book0	#<Author:0x007f92b10aa618> book1	#<Author:0x007f92b013d400> book2	#<Author:0x007f92b0a76198> book3	#<Author:0x007f92af9e03d8> book4	#<Author:0x007f92af9502d8> book5	#<Author:0x007f92b09f4d28> book6	#<Author:0x007f92b0907230> book7	#<Author:0x007f92b00fdb98> book8	#<Author:0x007f92b002ea00> book9	#<Author:0x007f92af8d5858> 
 
現在把在 all  後面加上 includes(:author) 表示順便幫我們把 author  的東西也一起抓下來。
1 2 3 4 def  index   @books  = Book .all.includes(:author ) end 
 
所以重新開啟 rails server,然後載入 localhost:3000/books 頁面,就會發現 SQL 指令會少了好幾行。
1 2 3 4 5 6 7 8 9 # rails console Started GET  "/books" for  ::1  at  2016 -11 -21  14 :09 :55  + 0800    ActiveRecord::SchemaMigration Load (0.1 ms)  SELECT  "schema_migrations".*  FROM  "schema_migrations" Processing by  BooksController#index as  HTML   Rendering books/ index.html.erb within  layouts/ application   Book Load (0.2 ms)  SELECT  "books".*  FROM  "books"   Author Load (0.1 ms)  SELECT  "authors".*  FROM  "authors" WHERE  "authors"."id" =  1    Rendered books/ index.html.erb within  layouts/ application (23.2 ms) Completed 200  OK in  205 ms (Views: 192.0 ms |  ActiveRecord: 1.3 ms) 
 
而且每個 author  的 id 都會是一樣的
1 2 3 4 5 6 7 8 9 10 11 12 # localhost:3000/books Title	         Author book0	#<Author:0x007fcee42c2238> book1	#<Author:0x007fcee42c2238> book2	#<Author:0x007fcee42c2238> book3	#<Author:0x007fcee42c2238> book4	#<Author:0x007fcee42c2238> book5	#<Author:0x007fcee42c2238> book6	#<Author:0x007fcee42c2238> book7	#<Author:0x007fcee42c2238> book8	#<Author:0x007fcee42c2238> book9	#<Author:0x007fcee42c2238> 
 
只有 10 比資料效能提升可能不太明顯,但是如果 books  有 1000 筆資料,這樣就會有明顯的差異了:
1 2 Rendered books/ index.html.erb within  layouts/ application (812.6 ms) Completed 200  OK in  968 ms (Views: 887.0 ms |  ActiveRecord: 69.6 ms) 
 
1 2 Rendered books/ index.html.erb within  layouts/ application (202.5 ms) Completed 200  OK in  359 ms (Views: 344.5 ms |  ActiveRecord: 3.3 ms) 
 
注意 如果只想要拿一筆資料 find、find_by 之類的,寫法如下:
1 2 3 4 Book .includes(:author ).find_by_name("akiicat" )