پچھلی پوسٹ میں، ہم نے سیکھا تھا کہ جب transaction.atomic
سے مزین فنکشن کہا جاتا ہے تو کیا ہوتا ہے اور with transaction.atomic()
کیا ہوتا ہے۔ خلاصہ میں:
BEGIN;
DB کو بھیجا جاتا ہے (اگر DB PSQL ہے یا دوسرا SQL متغیر ہے)۔
اب، ہم اس بات پر بات کریں گے کہ لین دین میں کیا ڈالنا ہے اور کن چیزوں سے بچنا ہے۔ مندرجہ ذیل لین دین کے رویوں کی وجہ سے، ٹرانزیکشن بلاک میں رکھے جانے پر کچھ آپریشنز خطرناک ہوتے ہیں۔
متبادل اور کوڈ کی مثالوں کے لیے پڑھتے رہیں۔
بنیادی خطرہ یہ ہے کہ لین دین اس وقت تک تالے رکھتا ہے جب تک کہ وہ میزوں اور قطاروں پر متضاد کارروائیوں کو روکنے اور لین دین کو الٹ جانے کی اجازت دینے کے لیے مکمل نہ ہو جائیں - یہ ٹرانزیکشن میں DB کی کارروائیوں کو ایٹمی بنانے کے لیے ضروری ہے۔ اس کا مطلب یہ ہے کہ ایک طویل عرصے سے چلنے والا لین دین جو ایک سے زیادہ ٹیبلز پر چلتا ہے یا چند اہم لین دین میں بندش کا سبب بن سکتا ہے اور ان میزوں/قطاروں کو پڑھنے/لکھنے سے روکتا ہے۔
مختصراً، اگر ہم ٹرانزیکشن بلاک میں غلط کوڈ ڈالتے ہیں، تو ہم DB کے دیگر تمام کنکشنز کو اس پر کارروائی کرنے سے روک کر مؤثر طریقے سے DB کو ہٹا سکتے ہیں۔
ثانوی خطرہ یہ ہے کہ لین دین کو الٹنے کی ضرورت ہے اور ان کے الٹ جانے کی توقع ہے۔ اگر لین دین میں کوئی خرابی ہوتی ہے تو DB خود بخود ہر آپریشن کو ریورس کر دیتا ہے۔ لہذا، ہم نے لین دین میں جو DB آپریشنز ڈالے ہیں وہ الٹ جانے چاہئیں - زیادہ تر حصے کے لیے، ہمیں PSQL کے ساتھ اس کے بارے میں فکر کرنے کی ضرورت نہیں ہے۔ لیکن دوسرے کوڈز کا کیا ہوگا؟
اکثر اوقات، جب ہم اپنا ڈیٹا تبدیل کرتے ہیں، تو ہمیں فالو اپ ٹاسکس کرنے کی ضرورت ہوتی ہے جیسے کہ فائرنگ کے واقعات، سروسز کو اپ ڈیٹ کرنا، پش نوٹیفیکیشن بھیجنا وغیرہ۔ یہ کام واپس نہیں کیے جا سکتے ہیں - ہم کسی ایونٹ، درخواست، یا اطلاع کو نہیں بھیج سکتے۔ اگر کوئی خرابی پیش آتی ہے، تو ڈیٹا کی تبدیلیوں کو واپس کر دیا جاتا ہے، لیکن ہم پہلے ہی پش نوٹیفکیشن بھیج چکے ہیں کہ "آپ کی رپورٹ تیار ہو گئی ہے؛ اسے دیکھنے کے لیے یہاں کلک کریں۔" کیا ہوتا ہے جب صارف یا دیگر سروسز اس غلط معلومات پر عمل کرتی ہیں؟ ناکامیوں کا ایک جھرنا ہو گا۔ اس لیے، کوئی بھی کوڈ جس کو الٹ نہیں کیا جا سکتا ہے، لین دین میں نہیں ہونا چاہیے، یا لین دین میں کوئی خرابی واقع ہونے پر ہمیں اپنے سسٹم کو خراب حالت میں چھوڑنے کا خطرہ ہے۔
یہ وہ چیزیں ہیں جن پر منحصر ہے کہ کتنے ڈیٹا پر کارروائی ہو رہی ہے اور DB ٹریفک، زیادہ دیر تک تالے رکھنے کی وجہ سے بندش کا سبب بن سکتی ہے۔ یہ سب چیزیں ٹھیک ہیں اگر ان میں زیادہ وقت نہ لگے۔
سست سوالات — ڈیٹا کی بازیافت عام طور پر تیز ہوتی ہے، سوائے ان تین منظرناموں کے۔ یہ استفسارات لین دین کو سست کر دیں گے اور اس کے لاک ہونے کے وقت کو بڑھا دیں گے، جس کے دوسرے صارفین پر منفی اثرات مرتب ہوں گے۔
@transaction.atomic def process_large_order_report(start_date, end_date, min_order_value=1000): # Complex query with multiple joins and aggregations large_orders = Order.objects.filter( created_at__range=(start_date, end_date), total_amount__gte=min_order_value, status='completed' ).select_related( 'customer', 'shipping_address', 'billing_address' ).prefetch_related( 'items__product__category', 'items__product__supplier' ).annotate( item_count=Count('items'), total_weight=Sum('items__product__weight'), discount_percentage=F('discount_amount') * 100 / F('total_amount') ).filter( # Additional complex filtering Q(customer__user__is_active=True) & (Q(items__product__category__name='Electronics') | Q(items__product__category__name='Furniture')) & ~Q(shipping_address__country='US') ).order_by('-total_amount') # do the transactional work with the large_orders queryset
# fixed def process_large_order_report(start_date, end_date, min_order_value=1000): # Complex query with multiple joins and aggregations large_orders = Order.objects.filter( created_at__range=(start_date, end_date), total_amount__gte=min_order_value, status='completed' ).select_related( 'customer', 'shipping_address', 'billing_address' ).prefetch_related( 'items__product__category', 'items__product__supplier' ).annotate( item_count=Count('items'), total_weight=Sum('items__product__weight'), discount_percentage=F('discount_amount') * 100 / F('total_amount') ).filter( # Additional complex filtering Q(customer__user__is_active=True) & (Q(items__product__category__name='Electronics') | Q(items__product__category__name='Furniture')) & ~Q(shipping_address__country='US') ).order_by('-total_amount') # Start the transaction block with transaction.atomic(): # do the transactional work with the large_orders queryset
ایک سے زیادہ ٹیبلز پر آپریشنز - ایک سے زیادہ ٹیبلز پر آپریشنز کے ساتھ لین دین ہر ٹیبل کو اس وقت تک لاک کر سکتا ہے جب تک کہ یہ مکمل نہ ہو جائے۔ یہ خاص طور پر جینگو کی نقل مکانی میں عام ہے - نقل مکانی کو چھوٹا رکھنے اور ایک وقت میں ایک یا چند میزوں پر توجہ مرکوز کرنے کی ایک اور وجہ۔
class Migration(migrations.Migration): dependencies = [("migrations", "0001_initial")] # too many operations operations = [ migrations.RemoveField("Author", "age"), migrations.AddField("Author", "rating", models.IntegerField(default=0)), migrations.AlterField("Book", "price", models.DecimalField(max_digits=5, decimal_places=2)), ]
# fixed # 1st migration class Migration(migrations.Migration): dependencies = [("migrations", "0001_initial")] operations = [ migrations.RemoveField("Author", "age"), ] # 2nd migration class Migration(migrations.Migration): dependencies = [("migrations", "0002_initial")] operations = [ migrations.AddField("Author", "rating", models.IntegerField(default=0)), ] # 3rd migration class Migration(migrations.Migration): dependencies = [("migrations", "0003_initial")] operations = [ migrations.AlterField("Book", "price", models.DecimalField(max_digits=5, decimal_places=2)), ]
ڈیٹا مائیگریشنز - چونکہ ٹرانزیکشنز کو ایک لاک پر رکھا جاتا ہے جب تک کہ استفسار کیا جاتا ہے جب تک کہ ٹرانزیکشن ناکام ہو جائے یا ختم نہ ہو جائے، ایک ہجرت جو ٹیبل کی ہر قطار پر کام کرتی ہے، یا تو ہر قطار پر پڑھنے یا لکھنے کو روک کر پوری ٹیبل کو لاک کر دیتی ہے۔
def migrate_user_profiles(): # Get all users with legacy profiles users_with_profiles = User.objects.filter( legacy_profile__isnull=False ).select_related('legacy_profile') # Process all users in a single transaction with transaction.atomic(): # Track progress total = users_with_profiles.count() print(f"Migrating {total} user profiles...") # Process each user for i, user in enumerate(users_with_profiles): if i % 100 == 0: print(f"Processed {i}/{total} profiles") legacy = user.legacy_profile legacy.update_new_user_profile()
# fixed def migrate_user_profiles(): # Get all users with legacy profiles users_with_profiles = User.objects.filter( legacy_profile__isnull=False ).select_related('legacy_profile') # Process all users in a single transaction # Track progress total = users_with_profiles.count() print(f"Migrating {total} user profiles...") # Process each user for i, user in enumerate(users_with_profiles): if i % 100 == 0: print(f"Processed {i}/{total} profiles") with transaction.atomic(): legacy = user.legacy_profile legacy.update_new_user_profile()
انہیں بعد میں چلائیں۔ یہ سوالات ضروری ہیں، لیکن ہمیں انہیں کاروباری اوقات کے دوران چلانے کی ضرورت نہیں ہے۔ یہاں کی بہترین پالیسی یہ ہے کہ جدول کتنا اہم ہے، اس بات کا تعین کرکے کہ منتقلی میں کتنا وقت لگ سکتا ہے، منتقلی کو اس وقت انجام دینا جب DB کے پاس ٹریفک کم سے کم ہو، اور ایک رول بیک پلان تیار کرکے بندش کے خطرے کو کم کرنا ہے۔
لین دین میں جتنا وقت لگتا ہے اسے کم کریں۔ یہ ٹیبل کو تقسیم کرکے اور انفرادی پارٹیشنز پر ہجرت چلا کر کیا جا سکتا ہے۔ PSQL پارٹیشنز اور جینگو
*جیانگو صرف PSQL اور SQLite کی منتقلی کے ارد گرد لین دین کو سمیٹتا ہے۔
class Migration(migrations.Migration): dependencies = [("migrations", "0001_initial")] # this migration, if on a large table, can slow down and block other operations # do it later operations = [ migrations.RemoveField("Users", "middle_name"), ]
ناقابل واپسی آپریشنز - لین دین میں ہر چیز کو الٹ جانا چاہیے اگر لین دین کو واپس کر دیا جائے؛ اگر ہم ٹرانزیکشن میں API کال کرتے ہیں، تو اسے کالعدم نہیں کیا جا سکتا۔
def transaction(user_data, user_files): with transaction.atomic(): user = User.objects.create(**user_data) async_notification_service.send_email(user.email, "You can login now!") Account.objects.create(user=user, balance=0) # rest of user creation proccess
def transaction(user_data, user_files): with transaction.atomic(): user = User.objects.create(**user_data) Account.objects.create(user=user, balance=0) # rest of user creation proccess # the transaction is still in progress, so it can still be rolled back, it is not # committed until the transaction block is exited, so putting the notification here # is not a good idea - especially if the job starts immediately tries to read the data # this creates a race condition async_notification_service.send_email(user.email, "You can login now!") def transaction(user_data, user_files): with transaction.atomic(): user = User.objects.create(**user_data) Account.objects.create(user=user, balance=0) # rest of user creation proccess transaction.on_commit(partial(async_notification_service.send_email, user.email, "You can login now!"))
کالوں کو مسدود کرنا — چونکہ لین دین دیگر تمام سوالات کو میزوں/قطاروں پر کام کرنے سے روکتا ہے، لین دین تبدیل ہو رہا ہے، اس لیے کوئی بھی کوڈ جو لین دین کی مدت کو بڑھاتا ہے، ڈی بی کو لاک کر دے گا، جس سے ڈی بی پر منحصر ایپس میں ٹائم آؤٹ اور غیر جوابی کارروائی ہو گی۔
def transaction(user_data, user_files): with transaction.atomic(): user = User.objects.create(**user_data) for file_data in user_files: # transaction waits for this upload and so do all other connections that need access to table/rows the transaction # uses url = Cloudinary.upload_file(file_data['data']) Files.objects.create(**file_data['meta_data'], user=user, url=url) Account.objects.create(user=user, balance=0) # rest of user creation proccess
# not bad def transaction(user_data, user_files): user = None with transaction.atomic(): user = User.objects.create(**user_data) Account.objects.create(user=user, balance=0) # rest of user creation proccess for file_data in user_files: url = Cloudinary.upload_file(file_data['data']) Files.objects.create(**file_data['meta_data'], user=user, url=url) # best fix from functools import partial def transaction(user_data, user_files): user = None with transaction.atomic(): user = User.objects.create(**user_data) Account.objects.create(user=user, balance=0) # rest of user creation proccess # partials create a callable with the function and arguments # so that the function is called with the arguments when the transaction is committed # TODO: diff between partial and lambda here??? transaction.on_commit(partial(create_user_files, user_files, user)) def create_user_files(user_files, user): for file_data in user_files: url = Cloudinary.upload_file(file_data['data']) Files.objects.create(**file_data['meta_data'], user=user, url=url)
اگلی پوسٹ میں، ہم PSQL میں غوطہ لگائیں گے اور تلاش کریں گے: