কম্পিউটার টিউটোরিয়াল

ActiveRecord অপ্টিমাইজেশন: ক্যাশিং ব্যবহার করে রিডান্ডান্ট ডাটাবেস কুয়েরি দূর করা

ক্যাশিং কে সাধারণভাবে বোঝাতে গেলে কোনো কোডের ফলাফল সংরক্ষণ করা যাতে পরে সেটি দ্রুত রিট্রিভ করা যায়। কিছু কেসে, এর মানে হলো একটি কম্পিউটেড ভ্যালু সংরক্ষণ করা যাতে পরে সেটি পুনরায় কম্পিউট করতে না হয়। তবে, আমরা ডেটা ক্যাশও করতে পারি সেটি মেমরিতে রাখে, কোনো কম্পিউটেশন ছাড়া, যাতে হার্ড ড্রাইভ থেকে পড়তে হয় না বা নেটওয়ার্ক রিকোয়েস্ট করতে হয় না।

এই পর্যায়টি ActiveRecord-এর জন্য বিশেষত প্রাসঙ্গিক, যেখানে ডাটাবেস প্রায়শই আলাদা সার্ভারে চলে। তাই, সকল রিকোয়েস্ট নেটওয়ার্ক-ট্রাফিক ওভারহেড ধারণ করে, না হয় ডাটাবেস সার্ভারে যা লোড পড়ে কুয়েরি повторbaar পারফর্ম করার সময়।

সৌভাগ্যের জন্য, রেইলস ডেভেলপারদের জন্য, ActiveRecord নিজে এতে অনেক কিছু হ্যান্ডেল করে, হয়তো আমাদের জানে না সে হয়। এটি প্রোডাক্টিভিটির জন্য ভালো, কিন্তু কখনো কখনো জানা গুরুত্বপূর্ণ যে পেছনে কী ক্যাশ হচ্ছে। উদাহরণস্বরূপ, যখন আপনি জানেন (অথবা আশা করেন) একটি ভ্যালু অন্য প্রসেস দ্বারা পরিবর্তন হচ্ছে, অথবা আপনাকে অবশ্যই সবচেয়ে আপ-টু-ডেট ভ্যালু লাগে। এই কেসগুলোতে, ActiveRecord কিছু 'এস্কেপ হ্যাচ' (escape hatches) দেয় ডেটার আনক্যাশড রিড ফোর্স করার জন্য।

ActiveRecord-এর লেজি ইভ্যালুয়েশন

ActiveRecord-এর লেজি ইভ্যালুয়েশন ক্যাশিং না হলেও, আমরা কোড উদাহরণগুলোতে এনকাউন্টার করব, তাই সংক্ষিপ্ত ওভারভিউ দিচ্ছি। যখন আপনি একটি ActiveRecord কুয়েরি কন্সট্রাক্ট করেন, অনেক কেসে, কোড ডাটাবেসে তত্ক्षণা কল ইস্যু করে না। এটি আমাদের একাধিক .where ক্লজ চেইন করতে দেয় ডাটাবেস হিট ছাড়া:


@posts = Post.where(published: true) # কোনো DB হিট নাই
@posts = @posts.where(publied_at: Date.today) // এখনো কিছু নাই
@posts.count # SELECT COUNT(*) FROM "posts" WHERE...

এছাড়া কিছু এক্সসেপশন আছে। উদাহরণস্বরূপ, .find, .find_by, .pluck, .to_a, বা .first ব্যবহার করলে অডিশনাল ক্লজ চেইন করা সম্ভব নয়। নিচের অধিকাংশ উদাহরণে, আমি .to_a ব্যবহার করব DB কল ফোর্স করার একটি সিম্পল উপায় হিসেবে।

লক্ষ্য করুন আপনি যদি রেইলস কনসোলে এটি এক্সপেরিমেন্ট করেন, তবে আপনাকে 'echo' মোড অফ করতে হবে। অন্যথায়, কনসোল (irb বা pry) একবার 'enter' চাপলে অবজেক্টে .inspect কল করে, যা DB কুয়েরি ফোর্স করে। Echo মোড ডিজেবল করতে:


conf.echo = false # irb-এর জন্য
pry_instance.config.print = proc {} # pry-এর জন্য

ActiveRecord রিলেশনস

ActiveRecord-এর বিল্ট-ইন ক্যাশিং-এর প্রথম অংশ যেটি আমরা দেখব সেটি রিলেশনস। উদাহরণস্বরূপ, আমাদের একটি টিপিক্যাল User-Posts রিলেশনশিপ আছে:


# app/models/user.rb
class User < ApplicationRecord
  has_many :posts
end

# app/models/post.rb
class Post < ApplicationRecord
  belongs_to :user
end

এটি আমাদের হ্যান্ডি user.posts এবং post.user মেথড দেয় রিলেটেড রেকর্ড(গুলো) খুঁজে বের করার জন্য ডাটাবেস কুয়েরি করে। ধরুন, আমরা এগুলো একটি কন্ট্রোলার এবং ভিউতে ব্যবহার করছি:


# app/controllers/posts_controller.rb
class PostsController < ApplicationController
  def index
    @user = User.find(params[:user_id])
    @posts = @user.posts
  end
  ...
# app/views/posts/index.html.erb
...
<%= render 'shared/sidebar' %>
<% @posts.each do |post| %>
  <%= render post %>
<% end %>

# app/views/shared/_sidebar.html.erb
...
<% @posts.each do |post| %>
  
  • <%= post.title %>
  • <% end %>

    আমাদের একটি বেসিক index অ্যাকশন আছে যা @user.posts গ্র্যাব করে। পূর্বের সেকশনের মত, এই পয়েন্টে ডাটাবেস কুয়েরি রান হয়নি। রেইলস তারপর আমাদের index ভিউ রেন্ডার করে, যা সাইডবার রেন্ডার করে। সাইডবার @posts.each ... কল করে, এবং এই পয়েন্টে ActiveRecord ডাটাবেস কুয়েরি ফায়ার করে ডেটা পেতে।

    তারপর আমরা আমাদের index টেমপ্লেটের বাকি অংশে ফিরি, যেখানে আরেকটি @posts.each আছে; তবে এইবার, কোনো ডাটাবেস কল নেই। কী হচ্ছে তা হলো ActiveRecord এই সকল পোস্ট ক্যাশ করছে এবং আবার ডাটাবেস থেকে পড়ার চেষ্টা করছে না।

    এস্কেপ হ্যাচ

    কখনো কখনো আমরা ActiveRecord-কে ফোর্স করতে চাই অ্যাসোসিয়েটেড রেকর্ডগুলো আবার ফেচ করতে; হয়তো, আমরা জানি এটি অন্য প্রসেস দ্বারা পরিবর্তন হচ্ছে (একটি ব্যাকগ্রাউন্ড জব, উদাহরণস্বরূপ)। আরেকটি সাধারণ পরিস্থিতি হলো অটোমেটেড টেস্টগুলোতে যেখানে আমরা ডাটাবেস থেকে লেটেস্ট ভ্যালু পেতে চাই যাতে ভ্যালিডেট করা যায় কোডটি সেটি সঠিকভাবে আপডেট করেছে।

    এটি করার দুটি সাধারণ উপায় আছে, পরিস্থিতির ওপর নির্ভর করে। আমার মতে সবচেয়ে সাধারণ উপায় হলো আস্তে .reload কল করা অ্যাসোসিয়েশনে, যা ActiveRecord-কে বলে যে আমরা যা ক্যাশ করা আছে সেটি ইগনোর করে লেটেস্ট ভার্সন ডাটাবেস থেকে চাই:

    
    @user = User.find(1)
    @user.posts # DB কল
    @user.posts # ক্যাশড, কোনো DB কল নেই
    @user.posts.reload # DB কল
    @user.posts # ক্যাশড নতুন ভার্সন, কোনো DB কল নেই
    

    আরেকটি অপশন হলো সিম্পলি ActiveRecord মডেলের একটি নতুন ইনস্ট্যান্স নেওয়া (যেমন, find আবার কল করে):

    
    @user = User.find(1)
    @user.posts # DB কল
    @user.posts # ক্যাশড, কোনো DB কল নেই
    @user = User.find(1) # @user এখন User-এর একটি নতুন ইনস্ট্যান্স
    @user.posts # DB কল, এই ইনস্ট্যান্সে কোনো ক্যাশ নেই
    

    রিলেশনশিপ ক্যাশিং ভালো, কিন্তু আমরা প্রায়শই জটিল .where(...) কুয়েরি দিয়ে পড়ি সিম্পল রিলেশনশিপ লুকআপের বাইরে। এখানেই ActiveRecord-এর SQL ক্যাশ কাজে আসে।

    ActiveRecord-এর SQL ক্যাশ

    ActiveRecord এক্সিকিউট করা কুয়েরিগুলোর একটি ইন্টারন্যাল ক্যাশ রাখে পারফরম্যান্স স্পিড আপ করার জন্য। লক্ষ্য করুন, যাইহোক, এই ক্যাশ নির্দিষ্ট অ্যাকশনের সাথে বাঁধা; এটি অ্যাকশনের শুরুতে তৈরি হয় এবং অ্যাকশনের শেষে ডিস্ট্রয় হয়। এর মানে আপনি এটি তখনই দেখবেন যখন আপনি একই কন্ট্রোলার অ্যাকশনে একই কুয়েরি দুবার পারফর্ম করেন। এটি একথাও মানে রেইলস কনসোলে ক্যাশ ব্যবহার হয় না। ক্যাশ হিট রেইলস লগে CACHE দিয়ে দেখানো হয়। উদাহরণস্বরূপ,

    
    class PostsController < ApplicationController
      def index
        ...
        Post.all.to_a # to_a DB কুয়েরি ফোর্স করতে
        ...
        Post.all.to_a # to_a DB কুয়েরি ফোর্স করতে
    

    নিচের লগ আউটপুট প্রোডিউস করে:

    
    Post Load (2.1ms) SELECT "posts".* FROM "posts"
    ↳ app/controllers/posts_controller.rb:11:in `index'
    CACHE Post Load (0.0ms) SELECT "posts".* FROM "posts"
    ↳ app/controllers/posts_controller.rb:13:in `index'
    

    আপনি আসলে একশনের জন্য ক্যাশে কী আছে সেটি `ActiveRecord::Base.connection.query_cache প্রিন্ট করে দেখতে পারেন (অথবা শুধু SQL কুয়েরি देखने ActiveRecord::Base.connection.query_cache.keys)।

    এস্কেপ হ্যাচ

    SQL ক্যাশ বাইপাস করার অনেক কারণ নেই, কিন্তু যাইহোক, আপনি ActiveRecord-কে ফোর্স করতে পারেন তার SQL ক্যাশ বাইপাস করার জন্য uncached মেথড ব্যবহার করে ActiveRecord::Base-এ:

    
    class PostsController < ApplicationController
      def index
        ...
        Post.all.to_a # to_a DB কুয়েরি ফোর্স করতে
        ...
        ActiveRecord::Base.uncached do
          Post.all.to_a # to_a DB কুয়েরি ফোর্স করতে
        end
    

    যেহেতু এটি ActiveRecord::Base-এ একটি মেথড, আপনি আপনার মডেল ক্লাসের মাধ্যমে ও কল করতে পারেন যদি রিডেবিলিটি বাড়ে; উদাহরণস্বরূপ,

    
    Post.uncached do
      Post.all.to_a
    end
    

    কাউন্টার ক্যাশ

    ওয়েব অ্যাপ্লিকেশনে রিলেশনশিপের রেকর্ড কাউন্ট জানতে খুব কমন (যেমন, একটি ইউজারের X পোস্ট আছে অথবা একটি টিম একাউন্টের Y ইউজার)। এতটাই কমন হলে, ActiveRecord একটি উপায় দিয়ে থাকে কাউন্টার অটোমেটিক্যালি আপ-টু-ডেট রাখতে যাতে আপনার একগুচ্ছ .count কল ডাটাবেস রিসোর্স ব্যবহার করে না। এটি শুধু কয়েক ধাপে ইন্যাবল করা যায়। প্রথম, রিলেশনশিপে counter_cache যোগ করুন যাতে ActiveRecord বুঝতে পারে কাউন্ট ক্যাশ করতে:

    
    class Post < ApplicationRecord
      belongs_to :user, counter_cache: true
    end
    

    আমাদের User-এ একটি নতুন কলামও যোগ করতে হবে, যেখানে কাউন্ট সংরক্ষণ হবে। আমাদের উদাহরণে, এটি হবে User.posts_count। আপনি counter_cache-এ একটি সিম্বল পাস করতে পারেন কলাম নাম স্পেসিফাই করতে যদি দরকার হয়।

    
    rails generate migration AddPostsCountToUsers posts_count:integer
    rails db:migrate
    

    কাউন্টারগুলো এখন ০-তে সেট হবে (ডিফল্ট)। আপনার অ্যাপে যদি ইতিমধ্যে কিছু পোস্ট থাকে, তবে সেগুলো আপডেট করতে হবে। ActiveRecord একটি reset_counters মেথড দিয়ে থাকে নিটি-গ্রিটি ডিটেইল হ্যান্ডেল করার জন্য, তাই আপনাকে শুধু আইডি পাস করতে হবে এবং বলতে হবে কোন কাউন্টার আপডেট করতে হবে:

    
    User.all.each do |user|
      User.reset_counters(user.id, :posts)
    end
    

    শেষে, আমাদের চেক করতে হবে সেসব জায়গা যেখানে এই কাউন্ট ব্যবহার হচ্ছে। এর কারণ হলো .count কল করলে কাউন্টার বাইপাস করবে এবং সবসময় COUNT() SQL কুয়েরি রান করবে। বদলে, আমরা .size ব্যবহার করতে পারি, যেটি জানে কাউন্টার ক্যাশ থাকলে সেটি ব্যবহার করতে। একদিকের কথা, আপনি যেকোনো জায়গায় ডিফল্টভাবে .size ব্যবহার করতে চাইতে পারেন, কারণ এটি অ্যাসোসিয়েশন রিলোড করবে না যদি সেগুলো ইতিমধ্যে প্রেজেন্ট থাকে, সম্ভাব্য ডাটাবেস ট্রিপ বাঁচাতে।

    উপসংহার

    বেশির ভাগ সময়, ActiveRecord-এর ইন্টারন্যাল ক্যাশিং "জাস্ট ওয়ার্কস"। আমি বলতে পারি আমি অনেক কেস দেখিনি যেটি এটিকে বাইপাস করতে হয়, কিন্তু সকল জিনিসের মত, "আন্ডার-দ-হুড" (under-the-hood) কী হচ্ছে সেটি জানা আপনাকে কিছু সময় এবং যন্ত্রণা বাঁচাতে পারে যখন আপনি অন然 একটা অসামান্য সিনেরিওতে পড়ে যান।

    অবশ্যই, ডাটাবেস একমাত্র জায়গা নয় যেখানে রেইলস পেছনে ক্যাশিং করে। HTTP স্পেসিফিকেশন হেডারগুলো অন্তর্ভুক্ত করে যেগুলো ক্লায়েন্ট এবং সার্ভারের মধ্যে পাঠানো যায় ডেটা রি-সেন্ড এড়াতে যেটি পরিবর্তন হয়নি। এই ক্যাশিং সিরিজের পরের আর্টিকেলে, আমরা 304 (Not Modified) HTTP স্ট্যাটাস কোড, রেইলস কীভাবে এটিকে হ্যান্ডেল করে, এবং আপনি কীভাবে এই হ্যান্ডলিং টইক করতে পারেন সেটি দেখব।

    1. MS SQL Server-এ ডেটাবেস রিকভারি (পুনরুদ্ধার)

    2. MS SQL Server-এ ডেটাবেস ডিলিট করা

    3. Windows 10-এ F1 Help কী ডিজেবল করা (অকস্মিক হেল্প এড়াতে)

    4. প্রক্রাস্টিনেশন জিতুন: ফোকাস ও প্রোডাক্টিভিটির জন্য সাবিত অ্যাপ কৌশল