हर बार पृष्ठ लोड होने पर डेटाबेस में संबंधित रिकॉर्ड्स की गणना करने के बजाय, ActiveRecord की काउंटर कैशिंग सुविधा काउंटर को संग्रहीत करने और हर बार किसी संबद्ध ऑब्जेक्ट को बनाने या हटाने पर इसे अपडेट करने की अनुमति देती है। AppSignal Academy की इस कड़ी में, हम ActiveRecord में कैशिंग काउंटरों के बारे में जानेंगे।
आइए लेखों और प्रतिक्रियाओं वाले ब्लॉग का उत्कृष्ट उदाहरण लें। प्रत्येक लेख में प्रतिक्रियाएँ हो सकती हैं, और हम ब्लॉग के अनुक्रमणिका पृष्ठ पर प्रत्येक लेख के शीर्षक के आगे प्रतिक्रियाओं की संख्या प्रदर्शित करना चाहेंगे ताकि इसकी लोकप्रियता प्रदर्शित हो सके।
# 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
. कहते हैं दृश्य में, ActiveRecord जानता है कि उसे प्रत्येक प्रतिक्रिया के लिए संपूर्ण रिकॉर्ड लोड करने के बजाय एसोसिएशन की गणना करने की आवश्यकता है।
टिप :हालांकि #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 प्रश्न उत्पन्न होते हैं, क्योंकि ActiveRecord प्रत्येक लेख के लिए प्रतिक्रिया संख्या को एक अलग क्वेरी में आलसी-लोड करता है।
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")
का चयन करते हैं प्रतिक्रियाओं की संख्या की गणना करने के लिए। हम प्रति लेख प्रतिक्रियाओं की गणना करने के लिए उत्पाद आईडी के आधार पर समूहबद्ध करेंगे। दृश्य में, हमें responses_count
. का उपयोग करना होगा कॉल करने के बजाय size
प्रतिक्रिया संघ पर।
यह समाधान पहली क्वेरी को धीमा और अधिक जटिल बनाकर अतिरिक्त प्रश्नों को रोकता है। हालांकि इस पृष्ठ के प्रदर्शन को अनुकूलित करने में यह एक अच्छा पहला कदम है, हम एक कदम आगे जाकर काउंटर को कैश कर सकते हैं, इसलिए हमें प्रत्येक पृष्ठ दृश्य पर प्रत्येक प्रतिक्रिया की गणना करने की आवश्यकता नहीं है।
काउंटर कैश
चूंकि ब्लॉग पर लेख (उम्मीद है) अपडेट होने की तुलना में अधिक बार पढ़े जाते हैं, एक काउंटर कैश इस पृष्ठ की क्वेरी को तेज़ और सरल बनाने के लिए एक अच्छा अनुकूलन है।
हर बार लेख प्रदर्शित होने पर प्रतिक्रियाओं की संख्या की गणना करने के बजाय, एक काउंटर कैश एक अलग प्रतिक्रिया काउंटर रखता है जो प्रत्येक लेख की डेटाबेस पंक्ति में संग्रहीत होता है। जब भी कोई जवाब जोड़ा या हटाया जाता है तो काउंटर अपडेट हो जाता है।
यह आलेख अनुक्रमणिका को क्वेरी में प्रतिक्रियाओं में शामिल होने की आवश्यकता के बिना, एक डेटाबेस क्वेरी के साथ प्रस्तुत करने की अनुमति देता है। इसे सेट करने के लिए, स्विच को 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)
स्कोप्ड एसोसिएशन के लिए काउंटर कैश
ActiveRecord का काउंटर कैश कॉलबैक केवल रिकॉर्ड बनाते या नष्ट करते समय सक्रिय होता है, इसलिए किसी स्कोप्ड एसोसिएशन पर काउंटर कैश जोड़ने से काम नहीं चलेगा। उन्नत मामलों के लिए, जैसे केवल *प्रकाशित* प्रतिक्रियाओं की संख्या गिनना, काउंटर_कल्चर रत्न देखें।
काउंटर कैश को पॉप्युलेट करना
काउंटर कैश से पहले के लेखों के लिए, काउंटर सिंक से बाहर हो जाएगा, क्योंकि यह डिफ़ॉल्ट रूप से 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 कमांड को निष्पादित करते हैं (जैसे #delete
का उपयोग करते समय) इसके बजाय #destroy
) काउंटरों को अपडेट नहीं करेगा।
ऐसी स्थितियों में जहां किसी कारण से ऐसा होता है, रेक टास्क या बैकग्राउंड जॉब जोड़ने का कोई मतलब हो सकता है जो समय-समय पर गणनाओं को सिंक में रखता है।
namespace :counters do
task update: :environment do
Article.find_each do |article|
Article.reset_counters(article.id, :responses)
end
end
end
कैश्ड काउंटर
क्वेरी में संबद्ध वस्तुओं की गणना करके N+1 प्रश्नों को रोकने से मदद मिल सकती है, लेकिन कैशिंग काउंटर अधिकांश अनुप्रयोगों के लिए काउंटर दिखाने का एक तेज़ तरीका है। ActiveRecord के अंतर्निर्मित कैश्ड काउंटर बहुत मदद कर सकते हैं, और अधिक विस्तृत आवश्यकताओं के लिए काउंटर_कल्चर जैसे विकल्पों का उपयोग किया जा सकता है।
ActiveRecord के काउंटर कैश के बारे में कोई प्रश्न हैं? कृपया हमें @AppSignal पर बताने में संकोच न करें। बेशक, हमें यह जानना अच्छा लगेगा कि आपको यह लेख कैसा लगा, या यदि आपके पास कोई अन्य विषय है जिसके बारे में आप और जानना चाहते हैं।