وقتی SQLAlchemy دیتای قدیمی را به ما برگرداند؟
یه اتفاق خیلی عجیب برای ما توی پروداکشن افتاد که ساعت یک شب برای بخش خرید ارور 500 گرفتیم.
حالا موضوع چی بود؟
فروشگاه ما به این شکل بود که محصولات از طریق یه Provider آپدیت موجودی و قیمت میشدن. تقریباً آپدیت هر محصول 5 ثانیه طول میکشید.
ما توی فروشگاهمون یه سری محصولات خیلی ارزون هم داشتیم که کمتر از چند سنت بودن و هفتهای یک یا دوبار شارژ میشدن.
یه فیچری هم زده بودیم که مشتریهامون میتونستن بگن:
هر موقع این محصول موجود شد برامون خرید رو بزن.
چون خیلی سریع هم تموم میشد.
ما هم میدیدیم اگه موجودی والتش اندازه بود، میخریدیم براش و فعالش میکردیم روی اکانتش.
حالا این وسط یه مشتری ما با موجودی بر فرض 5$ اومده بود یه خرید بزنه و اشتراک بخره.
در لحظهای که میاد اشتراک رو بخره، یه تسک دیگه میاد از حساب والت چند سنت خرید رو کم میکنه و اون سرویس هفتگی رو براش فعال میکنه.
تقریباً:
11 سنتحالا این وسط چه اتفاقی میافته؟
این دوتا Request یهجورایی پشت سر هم اتفاق میافتن.
و Wallet کاربر توی دیتابیس شده:
$4.89ولی خرید اون محصول مورد نظر مشتری بوده:
$4.99از اون جالبتر اینکه Query ما هم داشته میگفته:
$5.00در جایی که اصلاً نمیتونسته اگه موجودی کافی نبوده به این قسمت برسه!
حالا بریم بررسی کنیم داستان چی بوده.
مشکل از SQLAlchemy Session بود
یه موضوعی هست توی SQLAlchemy در رابطه با Sessionها.
برنامهنویس این سرویس بندهخدا فکر میکرده وقتی داره مینویسه:
stmt = (
select(Wallet, User)
.join(User, User.id == Wallet.user_id)
.where(Wallet.user_id == user_id)
)
result = await session.execute(stmt)
wallet, user = result.one()
time.sleep(10) # PROCESS
stmt = (
select(Wallet, User)
.join(User, User.id == Wallet.user_id)
.where(Wallet.user_id == user_id)
)
result = await session.execute(stmt)
wallet, user = result.one()این یعنی SQLAlchemy دوباره داره Query میزنه به دیتابیس و مقدار جدید رو میگیره.
اما امان از دل غافل!
شما وقتی توی یه Session یه Object رو دارین و قبلاً گرفتین، اگه دوباره Query بزنین، SQLAlchemy میبینه این Object رو توی Session خودش داره.
در نتیجه ممکنه همون Object موجود در Session رو برگردونه، بدون اینکه مقدار Attributeهای اون Object رو از دیتابیس دوباره پر کنه.
مثلاً:
Database:
balance = 4.89
Session:
wallet.balance = 5.00در این حالت شما ممکنه دوباره Query بزنین، اما همچنان این مقدار رو ببینید:
wallet.balance
# 5.00در حالی که مقدار واقعی دیتابیس:
4.89است.
استفاده از populate_existing=True
برای چنین شرایطی میتونیم از:
.execution_options(populate_existing=True)استفاده کنیم:
stmt = (
select(Wallet, User)
.join(User, User.id == Wallet.user_id)
.where(Wallet.user_id == user_id)
.execution_options(
populate_existing=True
)
)
result = await session.execute(stmt)
wallet, user = result.one()اینجا به SQLAlchemy میگیم نتایج این Query رو حتی اگه Object مربوطه از قبل داخل Session وجود داشته باشه، دوباره روی Object موجود اعمال کن.
بنابراین اگر در این فاصله موجودی Wallet تغییر کرده باشه:
قبل:
Session:
$5.00
Database:
$5.00و یک Request دیگه موجودی رو تغییر بده:
Database:
$4.89Query با:
.execution_options(
populate_existing=True
)باعث میشه مقدار Object موجود در Session هم دوباره با مقدار Query پر بشه:
Session:
$4.89
Database:
$4.89ولی session.refresh(obj) چی؟
شاید در لحظه بگین:
session.refresh(obj)هم میتونستیم استفاده کنیم.
که درست هم هست.
ولی این دوتا یه فرق اساسی دارن.
وقتی میای:
await session.refresh(wallet)میگی:
عزیز من، برو همین
objمشخص رو دوباره از دیتابیس بخون و آپدیتش کن.
یعنی تمرکز روی همون Object مشخصه.
ولی وقتی از:
.execution_options(
populate_existing=True
)استفاده میکنی، داری میگی:
نتایج این Query رو حتی اگه Object مربوطه از قبل توی Session وجود داشته باشه، برو بگیر و دوباره روی Object موجود اعمال کن.
پس به شکل ساده:
session.refresh(obj)
↓
همین Object مشخص رو دوباره از Database بخوندر مقابل:
populate_existing=True
↓
نتایج این Query رو روی Objectهای موجود در Session هم اعمال کنیک نکته مهم
بماند که این پروژه رو ما چندین ماه داشتیم ریفکتور میکردیم و این اتفاق به دلیل وحشتناک بودن و مزخرف بودن Source Code افتاده بود.
در کل مهمه که توی SQLAlchemy حواستون باشه که دیتا داره از Objectی که توی Session هست خونده میشه یا Query واقعاً داره مقدار مورد نظر رو از سمت Database میگیره.
مخصوصاً وقتی با شرایطی مثل اینها روبهرو هستید:
- چند Request همزمان
- Background Task
- چند Worker
- تغییر داده توسط Transactionهای مختلف
- Queueها
- تغییر اطلاعات خارج از Session فعلی
این موارد میتونن باعث بشن چیزی که داخل Object میبینید با چیزی که واقعاً داخل Database وجود داره متفاوت باشه.
در چنین شرایطی باید بدونید SQLAlchemy Session دقیقاً چه رفتاری با Objectهایی که قبلاً Load شدن داره.
اگه خوشتون اومده باشه، بعداً میتونیم بیشتر راجع بهش صحبت کنیم.
من صفحه های دیگه ایی هم دارم:
Related Posts
2026-09-18
2026-08-27
2026-09-20