تراکنش MongoDB

تراکنش MongoDB

انتشار:

تراکنش (Transaction) دیتابیس تو خیلی از محیط‌های عملیاتی مورد نیازه و کاربرد داره. در پایگاه‌داده مانگو (MongoDB) با استفاده از امکان ذخیره object و آرایه داخل یک سند (Single Document) می‌تونید بدون استفاده از Transaction از Atomic بودن عملیات خودتون مطمئن باشید، اما ممکنه بر اساس نیاز یا طراحی سیستم بخواید از چندین call (ارسال دستور به پایگاه‌داده) مرتبط و وابسته به هم و در نتیجه از Transaction استفاده کنید. تو این مطلب به نحوه‌ی ایجاد، استفاده، ذخیره/ثبت و لغو یک تراکنش در مانگو می‌پردازیم.

تراکنش MongoDB در ورژن 4.0 با استفاده از Replica Set و در ورژن 4.2 با استفاده از Sharded Cluster معرفی شد. پس اگر ورژن سرور دیتابیس شما از 4 کمتر باشه باید اول به فکر آپدیت اون باشید. در ادامه با استفاده از Replica Set که ساده‌تر پیکربندی میشه تراکنش رو انجام میدیم. دلیل این نیاز، وابستگی تراکنش‌ها به ابزارهایی است که تو محیط Replication تعریف شدن و وجود دارن، برای مطالعه بیشتر در این مورد میتونید از این لینک استفاده کنید.

 

 

پیکربندی Replica Set

همونطور که گفتم اول به Replica Set نیاز داریم. خلاصه بخوام بگم Replica Set چیه و چرا اول باید وجود داشته باشه؛ Replica Set به مجموعه‌ای از سرورهای MongoDB میگن که برای تکثیر داده (Replication) به صورت خودکار بین سرورها استفاده میشه. اما نکته اینجاست که شاید این تکثیر شدن پسند شما نباشه (چون حافظه رو حسابی اشغال میکنه)، اما مسئله‌ای نیست و میشه Replica Set با یک سرور هم داشته باشیم. حالا ما سرور تکی (Standalone) رو تبدیل می‌کنیم به Replica Set با یدونه گره (Node). برای اینکار باید فایل کانفیگ مانگو رو تغییر بدیم.

* لازمه که بگم این تغییر باعث از بین رفتن دیتا نمیشه اما خوبه که از دیتا بکاپ بگیرید.

 

خاموش کردن دیتابیس

قبل از شروع سرور رو خاموش کنید:

اگر از سرویس ویندوز استفاده می‌کنید داخل Services، سرویس مانگو رو Stop کنید و اگر از سرویس لینوکس استفاده می‌کنید با دستور systemctl stop mongod مانگو رو خاموش کنید.

 

تغییر تنظیمات پیکربندی

داخل فایل پیکربندی (کانفیگ) مانگو، این قسمت رو اضافه کنید:

replication:
  replSetName: "rs0"

آدرس فایل پیکربندی:

تو ویندوز داخل پوشه bin (محل نصب مانگو): C:\...\bin\mongod.conf

و لینوکس (به طور پیش فرض): /etc/mongod.conf

این دو خط به دیتابیس میگه که Replication صورت بگیره و اسم مجموعه Replication رو میذاره rs0 (میشه عنوان دلخواه انتخاب کرد).

 

نکته:

اگر از authorization در قسمت secutiry استفاده می‌کنید باید یه فایل key هم به قسمت security اضافه کنید. برای اینکار اول یک کلید تصادفی ایجاد می‌کنیم و بعد آدرس اون فایل رو به کانفیگ مانگو اضافه می‌کنیم.

ایجاد یک فایل کلید تصادفی با openssl (مسیر فایل رو به مسیر دلخواهتون تغییر بدید): openssl rand -base64 741 > /path/to/keyfile

آدرس فایل رو به کانفیگ اضافه کنید (تو قسمت security): keyFile: "/path/to/keyfile"

 

روشن کردن دیتابیس

بعد از تغییر کانفیگ باید سرور رو روشن کنیم. تو این مرحله ممکنه سرور روشن نشه و خطا بده، مثلا اگر به نکته آخر مرحله قبل توجه نکرده باشید یا اینکه process مانگو به فایل کلیدی که آدرس دادید دسترسی نداشته باشه!

برای روشن کردن سرور کافیه برعکس کاری که برای خاموش کردن انجام دادید رو بکنید. برای مثال، روشن کردن سروریس مانگو تو لینوکس: systemctl start mongod

بعد از دستور روشن شدن، وضعیت رو چک کنید که به مشکلی نخورده باشه: systemctl status mongod

 

راه‌اندازی Replica Set

حالا باید یکبار دستورات زیر رو داخل shell مانگو اجرا کنیم.

ویندوز: اگر ورژن قدیمی باشه میتونید فایل mongo.exe کنار فایل کانفیگ رو اجرا کنید ولی اگر ورژن جدید باشه این فایل وجود نداره و اول باید MongoDB Shell رو دانلود و فایل mongosh.exe رو اجرا کنید.

لینوکس: اگر مانگو global نصب شده باشه دستور mongosh تو ترمینال کافیه؛ اگر بخواید هاست، پورت و یوزرنیم مشخص کنید باید با دستور mongosh --host localhost --port 123 --username digital وارد بشید.

اگر رمز داشته باشید، رمز رو به عنوان ورودی میگیره و وارد محیط shell مانگو میشید. ابتدا دستور rs.initiate() رو برای راه‌اندازی Replica Set اجرا کنید و با دستور rs.status() وضعیت Replica Set رو بررسی کنید.

 

نمونه کد تراکنش در مانگو

مثالی که اینجا داریم، کد بروزرسانی تنظیمات همین وبلاگه که با TypeScript و mongoose نوشته شده. این کد تو یه حلقه for each با استفاده از تابع findOneAndUpdate دونه دونه تنظیمات با ساختار key, value رو پیدا و بروزرسانی میکنه. اینجا میخوایم اگر بروزرسانی یکی از تنظیمات به مشکل خورد، کلا تغییری تو داده‌های دیتابیس نداشته باشیم و به قولی رول بک کنیم.

برای کار با تراکنش مانگو باید session رو کنترل کنیم. مراحل:

  • ایجاد/شروع session
  • شروع تراکنش
  • انجام عملیات مورد نظر داخل try/catch
    • اعمال تراکنش و ذخیره تغییرات انتهای try
    • لغو تراکنش داخل catch
    • پایان session داخل finally (چه عملیات موفق باشه، چه خطایی وجود داشته باشه)

کد تایپ اسکریپت + مدل mongoose:

// قبل از این کد، ارتباط با دیتابیس و ایجاد مدل انجام شده 
// this.#settingsModel: mongoose model

const session = await this.#settingsModel.startSession(); // شروع نشست
session.startTransaction(); // شروع تراکنش

try {
	for (const setting of settings) {
		const res = await this.#settingsModel.findOneAndUpdate(
			{ key: setting.key, isDeleted: false }, // فیلتر پیدا کردن آیتم
			{ value: setting.value, lastUpdate: setting.lastUpdate } // تغییرات بر روی داده
			, { session }); // پاس دادن نشست به عملیات
		if (!res)
			// ایجاد خطا و در نتیجه اجرای لغو تراکنش
			throw new Error(`Update result null on key ${setting.key}`);
	}

	// اعمال تراکنش و ذخیره تغییرات
	await session.commitTransaction();
	return true;
} catch (err) {
	logger.error(err);
	// لغو تراکنش و تغییرات
	await session.abortTransaction();
	return false;
} finally {
	// پایان نشست
	await session.endSession();
}

تو این مثال مدیریت تراکنش (شروع، اعمال، لغو) کاملا تحت کنترل ما بود، اما اگر این جزییات و کنترل مهم نباشه، با حجم کد کمتری هم میشه همین نتیجه رو گرفت. با استفاده از تابع withTransaction و پاس دادن یک تابع به عنوان عملیات مورد نظر، عملیات انجام میشه و اگر تابع ورودی ما ارور بده، تراکنش لغو و اگر بدون مشکل اجرا بشه تراکنش انجام و ثبت میشه. تبدیل کد بالا به مدل جدید:

const session = await this.#settingsModel.startSession();

try {
    await session.withTransaction(async () => {
        for (const setting of settings) {
            const res = await this.#settingsModel.findOneAndUpdate(
                { key: setting.key, isDeleted: false },
                { value: setting.value, lastUpdate: setting.lastUpdate },
                { session }
            );
            if (!res)
                throw new Error(`Update result null on key ${setting.key}`);
        }
    });
    return true;
} catch (err) {
    logger.error(err);
    return false;
} finally {
    await session.endSession();
}