हर बार पेज लोड होने पर डेटाबेस में संबंधित रिकॉर्ड्स को गिनने के बजाय, एक्टिवरिकॉर्ड की काउंटर कैशिंग सुविधा काउंटर को स्टोर करने और हर बार संबंधित ऑब्जेक्ट बनाए या हटाए जाने पर इसे अपडेट करने की अनुमति देती है। ऐपसिग्नल अकादमी के इस एपिसोड में, हम एक्टिवरिकॉर्ड में काउंटर्स कैशिंग के बारे में सब कुछ सीखेंगे।
आइए आर्टिकल्स और रिस्पॉन्सेस के साथ एक क्लासिक ब्लॉग का उदाहरण लें। प्रत्येक आर्टिकल के रिस्पॉन्सेस हो सकते हैं, और हम ब्लॉग के इंडेक्स पेज पर प्रत्येक आर्टिकल के शीर्षक के आगे रिस्पॉन्सेस की संख्या प्रदर्शित करना चाहेंगे ताकि इसकी लोकप्रियता दिखाई जा सके।
# app/controllers/articles_controller.rb
class ArticlesController < ApplicationController
def index
@articles = Article.all
end
# ...
end
हमें रिस्पॉन्सेस को प्रीलोड करने की आवश्यकता नहीं है, क्योंकि हम इंडेक्स पेज पर उनका डेटा नहीं दिखाते। हम एक काउंटर दिखा रहे हैं, इसलिए हम प्रत्येक आर्टिकल के लिए रिस्पॉन्सेस की संख्या में ही रुचि रखते हैं। कंट्रोलर सभी आर्टिकल्स ढूंढता है और उन्हें व्यू के उपयोग के लिए @articles वेरिएबल में रखता है।
<!-- app/views/articles/index.html.erb -->
<h1>Articles</h1>
<% @articles.each do |article| %>
<article>
<h1><%= article.title %></h1>
<p><%= article.description %></p>
<%= article.responses.size %> responses
</article>
<% end %>
व्यू प्रत्येक आर्टिकल पर लूप करता है और उसका शीर्षक, विवरण और प्राप्त रिस्पॉन्सेस की संख्या रेंडर करता है। क्योंकि हम व्यू में article.responses.size कॉल करते हैं, एक्टिवरिकॉर्ड जानता है कि उसे प्रत्येक रिस्पॉन्स के लिए पूरा रिकॉर्ड लोड करने के बजाय एसोसिएशन को गिनने की आवश्यकता है।
टिप: हालांकि #count रिस्पॉन्सेस की संख्या गिनने के लिए अधिक सहज विकल्प लगता है, इस उदाहरण में #size का उपयोग करता है, क्योंकि #count हमेशा COUNT क्वेरी करेगा, जबकि #size क्वेरी को छोड़ देगा अगर रिस्पॉन्सेस पहले से लोड हैं।
Started GET "/articles" for 127.0.0.1 at 2018-06-14 16:25:36 +0200
Processing by ArticlesController#index as HTML
Rendering articles/index.html.erb within layouts/application
Article Load (0.2ms) SELECT "articles".* FROM "articles"
↳ app/views/articles/index.html.erb:3
(0.2ms) SELECT COUNT(*) FROM "responses" WHERE "responses"."article_id" = ? [["article_id", 2]]
↳ app/views/articles/index.html.erb:7
(0.3ms) SELECT COUNT(*) FROM "responses" WHERE "responses"."article_id" = ? [["article_id", 3]]
↳ app/views/articles/index.html.erb:7
(0.1ms) SELECT COUNT(*) FROM "responses" WHERE "responses"."article_id" = ? [["article_id", 4]]
↳ app/views/articles/index.html.erb:7
(0.1ms) SELECT COUNT(*) FROM "responses" WHERE "responses"."article_id" = ? [["article_id", 5]]
↳ app/views/articles/index.html.erb:7
(0.1ms) SELECT COUNT(*) FROM "responses" WHERE "responses"."article_id" = ? [["article_id", 6]]
↳ app/views/articles/index.html.erb:7
(0.1ms) SELECT COUNT(*) FROM "responses" WHERE "responses"."article_id" = ? [["article_id", 7]]
↳ app/views/articles/index.html.erb:7
(0.1ms) SELECT COUNT(*) FROM "responses" WHERE "responses"."article_id" = ? [["article_id", 8]]
↳ app/views/articles/index.html.erb:7
(0.1ms) SELECT COUNT(*) FROM "responses" WHERE "responses"."article_id" = ? [["article_id", 9]]
↳ app/views/articles/index.html.erb:7
(0.1ms) SELECT COUNT(*) FROM "responses" WHERE "responses"."article_id" = ? [["article_id", 10]]
↳ app/views/articles/index.html.erb:7
(0.1ms) SELECT COUNT(*) FROM "responses" WHERE "responses"."article_id" = ? [["article_id", 11]]
↳ app/views/articles/index.html.erb:7
Rendered articles/index.html.erb within layouts/application (23.1ms)
Completed 200 OK in 52ms (Views: 45.7ms | ActiveRecord: 1.6ms)
ब्लॉग के इंडेक्स का अनुरोध करने पर N+1 क्वेरीज़ परिणाम होती हैं, क्योंकि एक्टिवरिकॉर्ड प्रत्येक आर्टिकल के लिए रिस्पॉन्स काउंट को अलग क्वेरी में लेज़ी-लोड करता है।
क्वेरी से COUNT() का उपयोग करना
प्रति आर्टिकल एक अतिरिक्त क्वेरी चलाने से बचने के लिए, हम आर्टिकल्स और रिस्पॉन्सेस टेबल्स को एक साथ जोड़ सकते हैं ताकि एक सिंगल क्वेरी में संबंधित रिस्पॉन्सेस को गिना जा सके।
# app/controllers/articles_controller.rb
class ArticlesController < ApplicationController
def index
@articles = Article
.joins(:responses)
.select("articles.*", 'COUNT("responses.id") AS responses_count')
.group('articles.id')
end
# ...
end
इस उदाहरण में, हम आर्टिकल्स क्वेरी में रिस्पॉन्सेस को जोड़ते हैं और रिस्पॉन्सेस की संख्या गिनने के लिए COUNT("responses.id") का चयन करते हैं। हम प्रति आर्टिकल रिस्पॉन्सेस गिनने के लिए प्रोडक्ट आईडीज़ द्वारा ग्रुप करेंगे। व्यू में, हमें रिस्पॉन्सेस एसोसिएशन पर size कॉल करने के बजाय responses_count का उपयोग करना होगा।
यह समाधान पहली क्वेरी को धीमा और अधिक जटिल बनाकर अतिरिक्त क्वेरीज़ को रोकता है। हालांकि यह इस पेज के प्रदर्शन को ऑप्टिमाइज़ करने की दिशा में एक अच्छा पहला कदम है, हम एक कदम आगे जा सकते हैं और काउंटर को कैश कर सकते हैं ताकि हर पेज व्यू पर प्रत्येक रिस्पॉन्स को गिनने की आवश्यकता न हो।
काउंटर कैश
चूंकि ब्लॉग पर आर्टिकल्स (उम्मीद है) अपडेट होने की तुलना में अधिक बार पढ़े जाते हैं, काउंटर कैश इस पेज को क्वेरी करना तेज और सरल बनाने के लिए एक अच्छा ऑप्टिमाइज़ेशन है।
हर बार आर्टिकल्स प्रदर्शित होने पर रिस्पॉन्सेस की संख्या गिनने के बजाय, एक काउंटर कैश एक अलग रिस्पॉन्स काउंटर रखता है जो प्रत्येक आर्टिकल के डेटाबेस रो में स्टोर होता है। काउंटर अपडेट होता है जब भी कोई रिस्पॉन्स जोड़ा या हटाया जाता है।
यह आर्टिकल इंडेक्स को रेंडर करने की अनुमति देता है एक डेटाबेस क्वेरी के साथ, बिना क्वेरी में रिस्पॉन्सेस को जोड़े। इसे सेट करने के लिए, belongs_to रिलेशन में स्विच को counter_cache विकल्प सेट करके फ्लिप करें।
# app/models/response.rb
class Response
belongs_to :article, counter_cache: true
end
इसके लिए Article मॉडल में responses_count नाम का एक फील्ड चाहिए। counter_cache विकल्प सुनिश्चित करता है कि उस फील्ड में नंबर स्वचालित रूप से अपडेट होता है जब भी कोई रिस्पॉन्स जोड़ा या हटाया जाता है।
टिप: फील्ड नाम को true के बजाय सिम्बल का उपयोग करके counter_cache विकल्प के मान के रूप में ओवरराइड किया जा सकता है।
हम काउंट स्टोर करने के लिए अपने डेटाबेस में एक नया कॉलम बनाते हैं।
$ rails generate migration AddResponsesCountToArticles responses_count:integer
invoke active_record
create db/migrate/20180618093257_add_responses_count_to_articles.rb
$ rake db:migrate
== 20180618093257 AddResponsesCountToArticles: migrating ======================
-- add_column(:articles, :responses_count, :integer) -> 0.0016s
== 20180618093257 AddResponsesCountToArticles: migrated (0.0017s)
=============
क्योंकि रिस्पॉन्सेस की संख्या अब आर्टिकल्स टेबल में कैश्ड है, हमें आर्टिकल्स क्वेरी में रिस्पॉन्सेस को जोड़ने की आवश्यकता नहीं है। हम कंट्रोलर में सभी आर्टिकल्स लाने के लिए Article.all का उपयोग करेंगे।
# app/controllers/articles_controller.rb
class ArticlesController < ApplicationController
def index
@articles = Article.all
end
# ...
end
हमें व्यू बदलने की आवश्यकता नहीं है, क्योंकि रेल्स #size मेथड के लिए काउंटर कैश का उपयोग करना समझता है।
<!-- app/views/articles/index.html.erb -->
<h1>Articles</h1>
<% @articles.each do |article| %>
<article>
<h1><%= article.title %></h1>
<p><%= article.description %></p>
<%= article.responses.size %> responses
</article>
<% end %>
फिर से हमारे इंडेक्स का अनुरोध करने पर, हम एक क्वेरी निष्पादित होते हुए देख सकते हैं। क्योंकि प्रत्येक आर्टिकल को उसके रिस्पॉन्सेस की संख्या पता है, उसे रिस्पॉन्सेस टेबल को बिल्कुल भी क्वेरी करने की आवश्यकता नहीं है।
Started GET "/articles" for 127.0.0.1 at 2018-06-14 17:15:23 +0200
Processing by ArticlesController#index as HTML
Rendering articles/index.html.erb within layouts/application
Article Load (0.2ms) SELECT "articles".* FROM "articles"
↳ app/views/articles/index.html.erb:3
Rendered articles/index.html.erb within layouts/application (3.5ms)
Completed 200 OK in 42ms (Views: 36.5ms | ActiveRecord: 0.2ms)
स्कोप्ड एसोसिएशन्स के लिए काउंटर कैश
एक्टिवरिकॉर्ड के काउंटर कैश कॉलबैक्स केवल रिकॉर्ड्स बनाते या नष्ट करते समय फायर होते हैं, इसलिए स्कोप्ड एसोसिएशन पर काउंटर कैश जोड़ने से काम नहीं करेगा। उन्नत मामलों के लिए, जैसे केवल *प्रकाशित* रिस्पॉन्सेस की संख्या गिनना, counter_culture जेम देखें।
काउंटर कैश को भरना
काउंटर कैश से पहले के आर्टिकल्स के लिए, काउंटर आउट ऑफ सिंक होगा, क्योंकि यह डिफ़ॉल्ट रूप से 0 है। हम किसी ऑब्जेक्ट के लिए काउंटर को .reset_counters मेथड का उपयोग करके "रीसेट" कर सकते हैं और ऑब्जेक्ट की आईडी और वह रिलेशन पास कर सकते हैं जिसके लिए काउंटर अपडेट किया जाना चाहिए।
Article.reset_counters(article.id, :responses)
यह सुनिश्चित करने के लिए कि यह डेप्लॉय करते समय प्रोडक्शन पर चलता है, हम इसे पिछली माइग्रेशन में कॉलम जोड़ने के ठीक बाद चलने वाली माइग्रेशन में डालेंगे।
$ rails generate migration PopulateArticleResponsesCount --force
invoke active_record
create db/migrate/20180618093443_populate_article_responses_count.rb
माइग्रेशन में, हम प्रत्येक आर्टिकल के लिए Article.reset_counters कॉल करेंगे, आर्टिकल्स की आईडीज़ और एसोसिएशन के नाम के रूप में :responses पास करेंगे।
# db/migrate/20180618093443_populate_article_responses_count.rb
class PopulateArticleResponsesCount < ActiveRecord::Migration[5.2]
def up
Article.find_each do |article|
Article.reset_counters(article.id, :responses)
end
end
end
यह माइग्रेशन डेटाबेस में सभी आर्टिकल्स के काउंट्स को अपडेट करती है जिसमें काउंटर कैश से पहले मौजूद आर्टिकल्स शामिल हैं।
कॉलबैक्स
क्योंकि काउंटर कैश काउंटर्स को अपडेट करने के लिए कॉलबैक्स का उपयोग करते हैं, सीधे SQL कमांड निष्पादित करने वाले मेथड्स (जैसे #destroy के बजाय #delete का उपयोग करते समय) काउंटर्स को अपडेट नहीं करेंगे।
ऐसी स्थितियों में जहां किसी कारण से ऐसा होता है, समय-समय पर काउंट्स को सिंक में रखने के लिए एक Rake टास्क या बैकग्राउंड जॉब जोड़ना समझदारी हो सकती है।
namespace :counters do
task update: :environment do
Article.find_each do |article|
Article.reset_counters(article.id, :responses)
end
end
end
कैश्ड काउंटर्स
क्वेरी में संबंधित ऑब्जेक्ट्स को गिनकर N+1 क्वेरीज़ को रोकना मदद कर सकता है, लेकिन अधिकांश एप्लिकेशन्स के लिए काउंटर्स कैश करना काउंटर्स दिखाने का एक और भी तेज़ तरीका है। एक्टिवरिकॉर्ड के बिल्ट-इन कैश्ड काउंटर्स बहुत मददगार हो सकते हैं, और काउंटर_कल्चर जैसे विकल्पों का उपयोग अधिक विस्तृत आवश्यकताओं के लिए किया जा सकता है।
एक्टिवरिकॉर्ड के काउंटर कैश के बारे में कोई प्रश्न? कृपया @AppSignal पर हमें बताने में संकोच न करें। बेशक, हमें यह जानकर खुशी होगी कि आपको यह लेख कैसा लगा, या यदि आपके पास कोई अन्य विषय है जिसके बारे में आप अधिक जानना चाहेंगे।