آنچه کلود فکر می‌کرد درباره Rails Callbacks می‌داند

سعی کردم یک rake task را برای حذف رکوردهای LineItem و فایل‌های S3 مربوط به آن‌ها اجرا کنم. می‌خواستم از اجرای callbackهای سنگین روی مدل‌های والد مثل Order جلوگیری کنم.

از Claude کمک خواستم. او با اطمینان پاسخ داد، اما اشتباه می‌کرد.

در اینجا آنچه درباره Rails، counter cacheها و دلیل لزوم تأیید توصیه‌های هوش مصنوعی آموختم را می‌خوانید.

مشکل LineItem متعلق به OrderItem است. OrderItem متعلق به Order است. هر دو از counter_cache و touch استفاده می‌کنند. حذف یک LineItem باعث ایجاد یک زنجیره (cascade) می‌شود. این زنجیره باعث اجرای jobهای سنگینی مثل تخمین هزینه ارسال و بازمحاسبه مجموع می‌شود. برای صرفه‌جویی در مصرف CPU و هزینه‌های S3، نیاز داشتم این زنجیره را متوقف کنم.

اشتباه هوش مصنوعی Claude پیشنهاد داد از skip_callback استفاده کنم. این ایده بدی است. skip_callback کلاس را به صورت سراسری (globally) تغییر می‌دهد. این کار روی تمام threadهای اپلیکیشن شما تأثیر می‌گذارد. اگر کد شما قبل از فعال‌سازی مجدد آن کرش کند، callbackهای شما برای همیشه از کار می‌افتند.

سپس no_touching را امتحان کردم. برای اطمینان، فراخوانی را هم در OrderItem و هم در Order قرار دادم. تست‌ها با موفقیت انجام شدند، اما کنسول چیز دیگری را نشان داد. timestamp مدل Order همچنان تغییر می‌کرد.

دلیل واقعی مشکل در نحوه عملکرد counter_cache در کنار touch بود.

  • وقتی از counter_cache: true و touch: true به طور همزمان استفاده می‌کنید، Rails آن‌ها را با هم ترکیب (bundle) می‌کند.
  • این کار یک دستور واحد raw SQL UPDATE ALL اجرا می‌کند.
  • دستورات raw SQL چرخه حیات (lifecycle) ActiveRecord را دور می‌زنند.
  • چون چرخه حیات دور زده می‌شود، hookهای after_commit اجرا نمی‌شوند.

این موضوع یک پارادوکس عجیب ایجاد کرد:

  • callbackهای OrderItem به دلیل ترکیب شدن با raw SQL اجرا نشدند.
  • اما callbackهای Order اجرا شدند، چون مرحله بعدی در زنجیره یک touch معمولی بود.

راه حل فقط لازم بود مدل پدربزرگ (grandparent) را در no_touching قرار دهم.

Order.no_touching { line_item.destroy! }

این کار مانع از رسیدن touch در سطح AR به مدل Order می‌شود. این کار جلوی raw SQL روی OrderItem را نمی‌گیرد، اما اهمیتی هم ندارد چون ترکیبِ counter cache از قبل آن callbackها را نادیده می‌گیرد.

نکات کلیدی

  • counter_cache: true + touch: true = raw SQL UPDATE ALL.
  • دستورات raw SQL تمام hookهای after_commit را نادیده می‌گیرند.
  • یک touch معمولی (بدون counter cache) از چرخه حیات استاندارد AR پیروی کرده و callbackها را اجرا می‌کند.
  • هرگز کورکورانه به کدهای هوش مصنوعی اعتماد نکنید. Claude فقط می‌خواهد به شما پاسخ دهد؛ برایش مهم نیست که آن پاسخ یک توهم (hallucination) باشد یا خیر.

همیشه فرضیات خود را در Rails console تست کنید. کد را عمداً خراب کنید تا ببینید آیا واقعاً رفتاری را که می‌خواهید متوقف کنید، مهار می‌کند یا خیر.

Source: https://dev.to/husteadrobert/what-claude-thought-he-knew-about-rails-callbacks-and-how-console-testing-proved-him-wrong-j20

Optional learning community: https://t.me/GyaanSetuAi