প্রতিবার পৃষ্ঠা লোড হওয়ার সময় ডাটাবেসে সংশ্লিষ্ট রেকর্ডগুলি গণনা করার পরিবর্তে, ActiveRecord-এর কাউন্টার ক্যাশিং বৈশিষ্ট্য কাউন্টারটিকে সংরক্ষণ করতে এবং প্রতিবার একটি সম্পর্কিত বস্তু তৈরি বা সরানো হলে এটি আপডেট করার অনুমতি দেয়। AppSignal একাডেমির এই পর্বে, আমরা 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-এর কাউন্টার ক্যাশে কলব্যাক শুধুমাত্র রেকর্ড তৈরি বা ধ্বংস করার সময় ফায়ার হয়, তাই স্কোপড অ্যাসোসিয়েশনে কাউন্টার ক্যাশে যোগ করা কাজ করবে না। উন্নত ক্ষেত্রে, শুধুমাত্র *প্রকাশিত* প্রতিক্রিয়ার সংখ্যা গণনা করার মতো, 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 কমান্ড কার্যকর করে (যেমন #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 এ জানাতে দ্বিধা করবেন না। অবশ্যই, আমরা জানতে চাই যে আপনি এই নিবন্ধটি কেমন পছন্দ করেছেন, অথবা আপনার যদি অন্য কোন বিষয় থাকে যে সম্পর্কে আপনি আরও জানতে চান।