rust-sqlite-sqlx-basics_آکادمی تخصصی ریسمان
تاریخ انتشار :
میانگین: 5.0

آموزش SQLite در Rust؛ مفاهیم اولیه و اتصال با SQLx

در این مقاله با مفاهیم اولیه کار با پایگاه داده SQLite در زبان برنامه‌نویسی Rust آشنا می‌شوید. یاد می‌گیرید SQLite چیست، چگونه کار می‌کند و چه تفاوتی با پایگاه داده‌هایی مانند PostgreSQL و MySQL دارد. سپس کتابخانه SQLx را معرفی می‌کنیم و نحوه افزودن وابستگی‌های موردنیاز به پروژه Rust را بررسی خواهیم کرد. با مثال‌های کاربردی، روش اتصال به دیتابیس، ایجاد جدول و اجرای دستورات اصلی SQL را یاد می‌گیرید. همچنین با مفاهیم مهمی مانند کوئری‌های پارامتری، جلوگیری از SQL Injection، مدیریت خطاها و تراکنش‌ها آشنا می‌شوید. این آموزش نقطه شروع مناسبی برای ساخت برنامه‌های دسکتاپ و نرم‌افزارهای آفلاین با Rust، SQLite و Tauri است.

Share
Pin
Like
Send
Share
Send
Send
Share

۱. SQLite چیست و چگونه کار می‌کند؟

اگر در حال یادگیری زبان برنامه‌نویسی Rust هستید و می‌خواهید برنامه‌ای بسازید که اطلاعات کاربران، محصولات، سفارش‌ها یا پرونده‌های بیماران را ذخیره کند، دیر یا زود به یک پایگاه داده نیاز پیدا می‌کنید. یکی از گزینه‌های کاربردی برای چنین پروژه‌هایی SQLite است؛ یک موتور پایگاه داده رابطه‌ای که به‌جای تکیه بر یک سرور مستقل، مستقیماً داخل فرایند برنامه اجرا می‌شود. این ویژگی باعث می‌شود بتوانید بدون راه‌اندازی یک سرویس جداگانه، اطلاعات را در یک فایل پایگاه داده ذخیره کنید. برای مثال، یک نرم‌افزار مدیریت کلینیک که روی ویندوز نصب می‌شود، می‌تواند اطلاعات بیماران و نوبت‌ها را در فایلی مانند physio.db نگهداری کند.

 

تصور کنید اطلاعات یک کلینیک را در چند فایل متنی ذخیره کرده‌اید. با افزایش تعداد بیماران، پیدا کردن یک پرونده، جلوگیری از ثبت کد ملی تکراری و ارتباط دادن هر بیمار به بیمه‌اش دشوار می‌شود. پایگاه داده رابطه‌ای این مشکلات را با ساختار مشخص، محدودیت‌های داده و زبان پرس‌وجوی SQL حل می‌کند. SQLite این امکانات را در قالب یک موتور سبک ارائه می‌دهد و به شما اجازه می‌دهد داده‌ها را با دستورهایی مانند SELECT، INSERT، UPDATE و DELETE مدیریت کنید. Rust نیز به کمک کتابخانه‌هایی مانند SQLx یا rusqlite می‌تواند با این موتور ارتباط برقرار کند؛ البته این کتابخانه‌ها بخشی از زبان Rust نیستند و باید به‌عنوان وابستگی پروژه اضافه شوند.

نکته مهم این است که SQLite فقط یک روش ساده برای نوشتن فایل نیست. این موتور از تراکنش‌ها، شاخص‌ها، محدودیت‌های جدولی و قابلیت‌های متداول SQL پشتیبانی می‌کند. بنابراین برای بسیاری از نرم‌افزارهای دسکتاپ، ابزارهای توسعه، اپلیکیشن‌های آفلاین و پروژه‌های کوچک تا متوسط گزینه‌ای جدی محسوب می‌شود. البته انتخاب آن باید براساس الگوی دسترسی، تعداد نویسندگان هم‌زمان، حجم کار و نیازهای معماری نرم‌افزار انجام شود؛ نه صرفاً به این دلیل که راه‌اندازی آن آسان است.

۲. تفاوت SQLite با PostgreSQL و MySQL

اولین تفاوت اساسی SQLite با پایگاه داده‌هایی مانند PostgreSQL و MySQL، معماری اجرای آن‌هاست. در معماری متداول PostgreSQL یا MySQL، یک سرویس پایگاه داده اجرا می‌شود و برنامه‌های دیگر از طریق اتصال شبکه یا سازوکارهای ارتباطی به آن درخواست می‌فرستند. در مقابل، SQLite معمولاً مستقیماً از طریق کتابخانه خود به فایل پایگاه داده دسترسی پیدا می‌کند. به همین دلیل، برای شروع کار با SQLite نیازی به نصب و پیکربندی یک سرور مجزا ندارید.

 

این تفاوت را در یک مثال واقعی بهتر می‌توان دید. فرض کنید نرم‌افزار پذیرش یک کلینیک فقط روی یک رایانه اجرا می‌شود و اطلاعات بیماران باید حتی در زمان قطع اینترنت در دسترس باشند. ذخیره اطلاعات در یک فایل SQLite می‌تواند نیاز این نرم‌افزار را برطرف کند. اما اگر چندین شعبه کلینیک در شهرهای مختلف بخواهند به یک مجموعه اطلاعات مرکزی دسترسی داشته باشند، معماری سرورمحور مانند PostgreSQL معمولاً انتخاب مناسب‌تری است. در این حالت، برنامه‌های کاربران به یک سرویس مرکزی متصل می‌شوند و مدیریت دسترسی، پشتیبان‌گیری و کنترل هم‌زمانی در سطح سرور انجام می‌شود.

ویژگی

SQLite

PostgreSQL

MySQL

معماری رایج

موتور تعبیه‌شده در برنامه

سرویس پایگاه داده

سرویس پایگاه داده

نیاز به سرور مستقل

خیر

بله

بله

نگهداری معمول داده‌ها

یک فایل پایگاه داده

فایل‌های تحت مدیریت سرور

فایل‌های تحت مدیریت سرور

استفاده آفلاین در دسکتاپ

بسیار مناسب

نیازمند راه‌اندازی محلی یا دسترسی به سرور

نیازمند راه‌اندازی محلی یا دسترسی به سرور

مناسب برای چند نویسنده هم‌زمان

با محدودیت‌های نوشتن

مناسب برای بارهای هم‌زمان گسترده

مناسب برای بارهای هم‌زمان گسترده

راه‌اندازی اولیه

ساده

نیازمند پیکربندی سرویس

نیازمند پیکربندی سرویس

این جدول یک مقایسه کلی است و نباید آن را جایگزین ارزیابی فنی پروژه دانست. SQLite می‌تواند در بسیاری از برنامه‌ها حجم قابل‌توجهی از اطلاعات را مدیریت کند، اما نحوه اجرای پرس‌وجوها، تعداد تراکنش‌های نوشتن و شیوه استقرار برنامه تعیین می‌کنند که آیا این انتخاب در عمل مناسب است یا خیر.

۳. چرا SQLite برای پروژه‌های Rust مناسب است؟

Rust به‌دلیل کنترل دقیق حافظه، سیستم نوع قدرتمند و مدیریت صریح خطاها برای توسعه نرم‌افزارهای قابل‌اعتماد مناسب است. ترکیب این زبان با SQLite می‌تواند ساخت یک نرم‌افزار دسکتاپ مستقل را ساده‌تر کند؛ زیرا لازم نیست علاوه بر برنامه اصلی، یک سرویس جداگانه برای پایگاه داده نصب و مدیریت کنید. برای مثال، اگر با Tauri یک برنامه ویندوزی می‌سازید، بخش رابط کاربری می‌تواند با React نوشته شود و بخش بک‌اند Rust عملیات ذخیره‌سازی و بازیابی اطلاعات را انجام دهد. فایل SQLite نیز اطلاعات را به‌شکل پایدار نگهداری می‌کند.

یکی دیگر از مزیت‌های این ترکیب، امکان طراحی معماری منظم است. می‌توانید کد مربوط به اتصال پایگاه داده را در یک ماژول مستقل قرار دهید، عملیات مربوط به بیماران را در لایه مخزن داده یا Repository پیاده‌سازی کنید و از طریق توابع مشخص، نتایج را به لایه فرمان‌های Tauri تحویل دهید. این جداسازی باعث می‌شود رابط کاربری مستقیماً مسئول اجرای SQL نباشد و تغییرات بعدی ساده‌تر انجام شوند. البته SQLite به‌تنهایی معماری مناسب ایجاد نمی‌کند؛ اگر همه کوئری‌ها، قواعد تجاری و مدیریت خطا را در یک تابع بزرگ جمع کنید، همچنان با کدی دشوار برای نگهداری روبه‌رو خواهید بود.

در نهایت، SQLite برای پروژه‌هایی مناسب است که ذخیره‌سازی محلی، استقرار ساده و دسترسی سریع به اطلاعات برایشان اهمیت دارد. در برنامه مدیریت بیماران، برای نمونه، می‌توان جداول جداگانه‌ای برای بیماران، بیمه‌ها، صندوق‌های بیمه و نوبت‌ها تعریف کرد و با کلیدهای خارجی میان آن‌ها ارتباط برقرار کرد. به این ترتیب، داده‌ها به‌جای پراکندگی در فایل‌های مختلف، در ساختاری قابل جست‌وجو و قابل اعتبارسنجی قرار می‌گیرند.

۴. مزایا و محدودیت‌های SQLite

یکی از مزایای مهم SQLite، مستقل بودن آن از یک سرویس پایگاه داده جداگانه است. نصب و راه‌اندازی ساده، نگهداری اطلاعات در یک فایل، پشتیبانی از تراکنش‌ها و قابل‌حمل بودن فایل پایگاه داده، استفاده از آن را برای نرم‌افزارهای دسکتاپ جذاب می‌کند. همچنین می‌توانید یک نسخه آزمایشی از پایگاه داده بسازید، آن را برای آزمایش نرم‌افزار استفاده کنید و بدون نیاز به راه‌اندازی زیرساخت سرور، رفتار کوئری‌ها را بررسی کنید. مستندات رسمی SQLite نیز بر ویژگی‌هایی مانند خودکفایی، پیکربندی حداقلی و تراکنش‌پذیری آن تأکید دارند.

 

اما محدودیت اصلی را نباید نادیده گرفت: SQLite برای همه سناریوهای هم‌زمانی مناسب نیست. چند اتصال می‌توانند عملیات خواندن را انجام دهند، ولی نوشتن هم‌زمان با محدودیت‌ها و قفل‌های فایل مدیریت می‌شود. اگر تعداد زیادی از کاربران به‌طور مداوم بخواهند داده‌ها را تغییر دهند، رقابت برای دسترسی نوشتن ممکن است به انتظار یا خطاهای قفل منجر شود. در چنین شرایطی باید تعداد تراکنش‌ها، مدت باز ماندن آن‌ها، تنظیمات زمان انتظار و احتمال نیاز به پایگاه داده سرورمحور را بررسی کنید.

 

مسئله دیگر، دسترسی چند برنامه به یک فایل پایگاه داده است. اگر فایل را روی یک پوشه شبکه‌ای قرار دهید یا چند فرایند از مسیرهای مختلف به آن دسترسی پیدا کنند، دیگر نمی‌توان فرض کرد که صرفاً با داشتن یک فایل، همه مسائل هم‌زمانی و امنیت حل شده‌اند. برای پروژه‌های جدی، باید از سازوکار مناسب پشتیبان‌گیری، محدود کردن دسترسی فایل، مدیریت تراکنش‌ها و کنترل نسخه ساختار پایگاه داده استفاده کنید. بنابراین انتخاب SQLite باید حاصل شناخت نیازهای پروژه باشد، نه یک تصمیم همیشگی برای تمام محصولات.

۵. آشنایی با جدول، ردیف، ستون و کلید اصلی

برای استفاده درست از SQLite، ابتدا باید ساختار یک پایگاه داده رابطه‌ای را بشناسید. داده‌ها معمولاً در قالب جدول ذخیره می‌شوند و هر جدول از ستون‌ها و ردیف‌ها تشکیل شده است. ستون مشخص می‌کند چه نوع اطلاعاتی نگهداری می‌شود؛ مثلاً نام، نام خانوادگی یا شماره تماس. ردیف نیز مجموعه مقادیر مربوط به یک رکورد مشخص است. اگر جدولی به نام Patients داشته باشیم، هر ردیف می‌تواند نماینده یک بیمار و هر ستون نماینده یکی از ویژگی‌های او باشد.

برای مثال، جدول بیماران می‌تواند چنین ساختاری داشته باشد:

Id

FirstName

LastName

NationalCode

1

علی

احمدی

0012345678

2

سارا

رضایی

0023456789

در این ساختار، ستون Id می‌تواند کلید اصلی یا PRIMARY KEY باشد. کلید اصلی هر رکورد را به‌طور یکتا شناسایی می‌کند و نباید مقدار تکراری یا تهی داشته باشد. داشتن کلید اصلی باعث می‌شود بتوانید یک بیمار را بدون وابستگی به نام یا شماره تلفن او پیدا کنید. نام افراد ممکن است تکراری باشد و حتی شماره تماس نیز در طول زمان تغییر کند؛ بنابراین شناسه داخلی پایدار برای ارتباط بین جدول‌ها انتخاب منطقی‌تری است.

در یک سامانه واقعی، ممکن است جدول بیماران به جدول بیمه‌ها ارتباط داشته باشد. در این صورت، ستون InsuranceId در جدول بیماران می‌تواند به شناسه یک رکورد در جدول بیمه‌ها اشاره کند. این ارتباط با استفاده از کلید خارجی تعریف می‌شود و می‌تواند از ثبت شناسه بیمه‌ای که وجود ندارد جلوگیری کند. برای فعال بودن چنین محدودیت‌هایی در SQLite باید وضعیت PRAGMA foreign_keys را نیز در اتصال‌های مورد استفاده بررسی و در صورت نیاز فعال کنید. صرف تعریف کلید خارجی در ساختار جدول، بدون توجه به تنظیمات اتصال، همیشه به معنای اعمال شدن محدودیت در هر اتصال نیست.

۶. انتخاب کتابخانه مناسب برای اتصال Rust به SQLite

زبان Rust به‌صورت پیش‌فرض یک API اختصاصی برای تمام عملیات SQLite در اختیار شما نمی‌گذارد. برای برقراری ارتباط با پایگاه داده، معمولاً از یک Crate استفاده می‌کنیم؛ Crate در اکوسیستم Rust به بسته‌ای گفته می‌شود که قابلیت‌های مشخصی را در اختیار پروژه قرار می‌دهد. دو گزینه شناخته‌شده برای این کار SQLx و rusqlite هستند. هر دو امکان اجرای کوئری‌های SQL را فراهم می‌کنند، اما رویکرد برنامه‌نویسی و امکانات آن‌ها یکسان نیست.

SQLx یک جعبه‌ابزار SQL است که از اجرای ناهمگام پشتیبانی می‌کند و برای برنامه‌هایی که از async/await استفاده می‌کنند، گزینه مناسبی است. این کتابخانه قابلیت‌هایی مانند اتصال به SQLite، اجرای کوئری پارامتری، تبدیل نتایج به ساختارهای Rust و مدیریت تراکنش‌ها را ارائه می‌دهد. اگر بک‌اند برنامه شما با Tauri نوشته شده و قرار است عملیات دیتابیس را از طریق فرمان‌های ناهمگام اجرا کند، SQLx می‌تواند انتخاب مناسبی باشد. مستندات نسخه جاری آن نیز در بخش اختصاصی SQLite، ساختار اتصال‌ها و قابلیت‌های این درایور را توضیح می‌دهد.

 

در مقابل، rusqlite یک رابط نسبتاً مستقیم و مبتنی بر فراخوانی‌های معمول برای استفاده از SQLite در Rust است. این کتابخانه برای برنامه‌هایی که به عملیات همگام و کنترل مستقیم‌تر بر اتصال و دستورات نیاز دارند، گزینه‌ای قابل بررسی است. با این حال، نباید تصور کنید که یکی از این دو کتابخانه در همه پروژه‌ها سریع‌تر یا بهتر است؛ تصمیم باید با توجه به مدل اجرای برنامه، پیچیدگی کوئری‌ها، نیازهای ناهمگام و شیوه استقرار گرفته شود.

۷. تفاوت SQLx و rusqlite

برای تصمیم‌گیری دقیق‌تر، ویژگی‌های مهم این دو کتابخانه را در جدول زیر مقایسه کنید.

ویژگی

SQLx

rusqlite

مدل برنامه‌نویسی

پشتیبانی از عملیات ناهمگام

عمدتاً همگام

اتصال به SQLite

دارد

دارد

اجرای کوئری پارامتری

دارد

دارد

مدیریت تراکنش

دارد

دارد

استفاده در بک‌اند ناهمگام Tauri

انتخاب مناسب

نیازمند توجه به مسدود نشدن اجرای برنامه

یادگیری اولیه

نیازمند آشنایی با async/await

معمولاً ساده‌تر برای شروع

نگاشت نتیجه به ساختار Rust

دارد

دارد

نکته مهم در برنامه‌های دسکتاپ این است که عملیات پایگاه داده نباید بی‌دلیل رابط کاربری را متوقف کند. اگر از rusqlite استفاده می‌کنید، عملیات طولانی را نباید مستقیماً روی تردی اجرا کنید که مسئول پاسخ‌گویی به رابط کاربری است. در مقابل، استفاده از SQLx نیز به‌معنای حذف همه مشکلات عملکردی نیست؛ کوئری نامناسب یا تراکنش طولانی همچنان می‌تواند باعث تأخیر شود.

در ادامه این آموزش، SQLx را انتخاب می‌کنیم تا مثال‌ها با معماری ناهمگام Rust سازگار باشند. اگر قبلاً پروژه Tauri خود را با SQLx راه‌اندازی کرده‌اید، می‌توانید همین مثال‌ها را با ساختار پروژه خود تطبیق دهید.

۸. آماده‌سازی پروژه Rust

برای شروع، باید ابزارهای پایه Rust و Cargo روی سیستم نصب باشند. Cargo مدیر بسته و ابزار ساخت پروژه در اکوسیستم Rust است و وظیفه دریافت وابستگی‌ها، کامپایل کد و اجرای برنامه را بر عهده دارد. اگر هنوز پروژه‌ای ندارید، یک پروژه ساده با دستور زیر بسازید:

cargo new rust_sqlite_demo
cd rust_sqlite_demo

پس از اجرای این دستورات، ساختار اولیه پروژه شامل فایل Cargo.toml و پوشه src ایجاد می‌شود. فایل src/main.rs نقطه شروع برنامه است و تنظیمات بسته و وابستگی‌ها در Cargo.toml قرار می‌گیرند. در یک پروژه بزرگ‌تر، می‌توانید فایل‌های جداگانه‌ای برای اتصال پایگاه داده، مدل‌ها، کوئری‌ها و مدیریت خطاها ایجاد کنید. اما در مرحله اول بهتر است مثال‌ها را در یک پروژه کوچک اجرا کنید تا خطاهای کامپایل و اتصال را راحت‌تر تشخیص دهید.

اگر هدف شما توسعه یک برنامه Tauri است، لزومی ندارد پروژه جداگانه‌ای بسازید. کافی است وابستگی‌ها را در فایل Cargo.toml بخش Rust برنامه خود اضافه کنید. در هر دو حالت، اصل کار یکسان است: SQLx از طریق درایور SQLite با فایل پایگاه داده ارتباط برقرار می‌کند و کد Rust عملیات موردنیاز را اجرا می‌کند.

۹. افزودن وابستگی‌های موردنیاز به Cargo.toml

برای استفاده از SQLx، وابستگی‌های لازم باید در فایل Cargo.toml تعریف شوند. در این مثال از SQLx نسخه 0.8 استفاده می‌کنیم تا مجموعه وابستگی‌ها و مثال‌ها مشخص و قابل تکرار باشند. این انتخاب به معنای جدیدترین نسخه بودن آن نیست؛ نسخه و ویژگی‌ها را باید متناسب با پروژه واقعی خود انتخاب کنید.

[dependencies]
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
sqlx = { version = "0.8", features = ["runtime-tokio", "sqlite"] }

در این تنظیمات، tokio زمان اجرای ناهمگام را فراهم می‌کند و SQLx با ویژگی runtime-tokio از آن استفاده می‌کند. ویژگی sqlite نیز درایور SQLite را فعال می‌کند. SQLx برای کار با عملیات ناهمگام به تنظیم مناسب زمان اجرا نیاز دارد؛ بنابراین اضافه کردن نام کتابخانه به‌تنهایی کافی نیست. مستندات SQLx درباره ویژگی‌های زمان اجرا و درایورها توضیح می‌دهد که این تنظیمات چگونه رفتار کتابخانه را تعیین می‌کنند.

 

پس از ذخیره فایل، دستور زیر را اجرا کنید:

cargo check

این دستور کد پروژه و سازگاری وابستگی‌ها را بررسی می‌کند. اگر خطایی دریافت کردید، ابتدا نسخه Rust، ویژگی‌های فعال‌شده در SQLx و پیام کامل خطا را بررسی کنید. در پروژه‌های موجود نیز بهتر است قبل از تغییر نسخه یک وابستگی، محدودیت‌های نسخه و سایر کتابخانه‌هایی را که به آن وابسته‌اند بررسی کنید؛ به‌خصوص در برنامه‌هایی که چند درایور یا افزونه پایگاه داده دارند.

۱۰. ایجاد اولین اتصال به پایگاه داده

اکنون می‌توانیم اولین اتصال به SQLite را ایجاد کنیم. یکی از ویژگی‌های کاربردی SQLite این است که در حالت معمول، اگر فایل پایگاه داده وجود نداشته باشد و اتصال با تنظیمات مناسب باز شود، فایل جدید ساخته می‌شود. البته مسیر فایل، مجوزهای دسترسی و تنظیمات اتصال تعیین می‌کنند که این عملیات واقعاً موفق شود یا خیر.

کد زیر را در src/main.rs قرار دهید:

use sqlx::sqlite::SqlitePoolOptions;

#[tokio::main]
async fn main() -> Result<(), sqlx::Error> {
    let pool = SqlitePoolOptions::new()
        .max_connections(5)
        .connect("sqlite:physio.db")
        .await?;

    println!("اتصال به SQLite برقرار شد.");

    pool.close().await;

    Ok(())
}

در این مثال، SqlitePoolOptions برای پیکربندی مجموعه اتصال‌ها استفاده می‌شود. مجموعه اتصال یا Connection Pool امکان استفاده مجدد از اتصال‌ها را فراهم می‌کند و در برنامه‌های ناهمگام که چند عملیات به پایگاه داده نیاز دارند، مدیریت اتصال را ساده‌تر می‌کند. گزینه max_connections(5) سقف اتصال‌های این مجموعه را تعیین می‌کند؛ این مقدار یک مثال آموزشی است و لزوماً بهترین تنظیم برای هر برنامه SQLite نیست. SQLx توضیح می‌دهد که Pool را می‌توان در طول عمر برنامه ایجاد کرد و میان بخش‌های مختلف آن به اشتراک گذاشت.

 

عبارت sqlite:physio.db مسیر نسبی فایل را مشخص می‌کند. در این حالت، محل ایجاد فایل به مسیر کاری برنامه وابسته است؛ بنابراین نباید فرض کنید فایل همیشه در کنار فایل اجرایی قرار می‌گیرد. در برنامه‌های دسکتاپ، بهتر است مسیر پایگاه داده را براساس پوشه داده‌های اختصاصی برنامه تعیین کنید. همچنین عبارت await? به این معناست که عملیات اتصال باید کامل شود و در صورت بروز خطا، خطا به فراخواننده بازگردانده شود.

در پایان، pool.close().await مجموعه اتصال را می‌بندد. در یک برنامه واقعی که قرار است در طول اجرای برنامه از پایگاه داده استفاده کند، معمولاً Pool را هنگام شروع برنامه ایجاد می‌کنیم و تا زمان خاموش شدن برنامه نگه می‌داریم، نه اینکه پس از هر عملیات آن را ببندیم.

۱۱. ایجاد جدول با دستور CREATE TABLE

داشتن فایل پایگاه داده به‌تنهایی به معنی آماده بودن ساختار اطلاعات نیست. برای ذخیره اطلاعات بیماران باید جدولی ایجاد کنیم و ستون‌های موردنیاز را تعریف کنیم. دستور CREATE TABLE برای همین کار استفاده می‌شود. در طراحی جدول باید نام ستون‌ها، نوع داده، کلید اصلی و محدودیت‌هایی مانند یکتا بودن مقادیر را به‌درستی تعیین کنیم.

فرض کنید می‌خواهیم جدول بیماران را ایجاد کنیم:

sqlx::query(
    r#"
    CREATE TABLE IF NOT EXISTS Patients (
        Id INTEGER PRIMARY KEY,
        FirstName TEXT NOT NULL,
        LastName TEXT NOT NULL,
        NationalCode TEXT NOT NULL UNIQUE,
        PhoneNumber TEXT,
        IsActive INTEGER NOT NULL DEFAULT 1
    )
    "#,
)
.execute(&pool)
.await?;

در این مثال، IF NOT EXISTS مانع از ایجاد دوباره جدولی با همان نام می‌شود. ستون‌های FirstName و LastName نباید مقدار تهی داشته باشند، درحالی‌که PhoneNumber می‌تواند خالی یا NULL باشد. محدودیت UNIQUE نیز از ثبت کد ملی تکراری جلوگیری می‌کند. چون کد ملی شناسه‌ای است که نباید با محاسبات عددی روی آن کار کنیم، ذخیره آن به‌صورت TEXT انتخاب مناسبی است؛ صفرهای ابتدایی آن نیز به این ترتیب حفظ می‌شوند.

نکته دیگری که باید جدی بگیرید، طراحی ساختار پایگاه داده در طول عمر برنامه است. CREATE TABLE IF NOT EXISTS برای ایجاد اولیه جدول مفید است، اما جایگزین سیستم مهاجرت پایگاه داده یا Database Migration نیست. اگر در نسخه بعدی برنامه ستون جدیدی اضافه کنید، اجرای مجدد این دستور به‌تنهایی ساختار موجود را به‌روزرسانی نمی‌کند. در پروژه واقعی، تغییرات ساختار باید با مهاجرت‌های نسخه‌بندی‌شده و قابل تکرار مدیریت شوند.

۱۲. افزودن اطلاعات با دستور INSERT

بعد از ایجاد جدول، نوبت به ذخیره داده می‌رسد. دستور INSERT INTO برای افزودن رکورد جدید استفاده می‌شود. در SQLx بهتر است مقادیر متغیر را از طریق پارامترها ارسال کنید و از قرار دادن مستقیم داده کاربر داخل رشته SQL خودداری کنید.

sqlx::query(
    r#"
    INSERT INTO Patients
        (FirstName, LastName, NationalCode, PhoneNumber)
    VALUES (?, ?, ?, ?)
    "#,
)
.bind("علی")
.bind("احمدی")
.bind("0012345678")
.bind("09120000000")
.execute(&pool)
.await?;

در این مثال، علامت ? جای پارامترها را مشخص می‌کند و متدهای bind مقادیر را به ترتیب به آن‌ها اختصاص می‌دهند. ترتیب مهم است؛ اگر نام و نام خانوادگی را جابه‌جا ارسال کنید، پایگاه داده الزاماً متوجه اشتباه معنایی شما نمی‌شود. همچنین اگر کد ملی تکراری باشد، محدودیت UNIQUE باعث می‌شود عملیات درج با خطا روبه‌رو شود. بنابراین خطای درج باید به‌درستی مدیریت شود و نباید هر خطایی را بدون بررسی نادیده بگیرید.

در برنامه‌های واقعی معمولاً بهتر است منطق درج در یک تابع مشخص مانند create_patient قرار بگیرد. این تابع می‌تواند اعتبارسنجی ورودی، اجرای کوئری، بررسی نتیجه و تبدیل خطاهای پایگاه داده به پیام مناسب برای لایه بالاتر را انجام دهد. با این روش، منطق ذخیره‌سازی از کد رابط کاربری جدا می‌شود و آزمایش آن ساده‌تر خواهد بود.

۱۳. خواندن اطلاعات با دستور SELECT

دستور SELECT برای بازیابی اطلاعات استفاده می‌شود. برخلاف عملیات درج، ممکن است یک کوئری چندین ردیف برگرداند؛ بنابراین باید مشخص کنیم نتیجه چگونه دریافت و به نوع‌های Rust تبدیل می‌شود. SQLx متدهایی مانند fetch_one، fetch_optional و fetch_all دارد که هرکدام برای سناریوی متفاوتی مناسب هستند.

برای دریافت فهرست بیماران می‌توانیم بنویسیم:

let patients = sqlx::query(
    "SELECT Id, FirstName, LastName FROM Patients ORDER BY Id"
)
.fetch_all(&pool)
.await?;

for patient in patients {
    let id: i64 = patient.try_get("Id")?;
    let first_name: String = patient.try_get("FirstName")?;
    let last_name: String = patient.try_get("LastName")?;

    println!("{id}: {first_name} {last_name}");
}

در این مثال، fetch_all تمام ردیف‌های نتیجه را دریافت می‌کند و try_get مقدار هر ستون را با بررسی خطا به نوع مشخص‌شده تبدیل می‌کند. اگر نام ستون اشتباه باشد یا نوع مقدار با نوع مورد انتظار سازگار نباشد، عملیات ممکن است خطا برگرداند. به همین دلیل، در پروژه‌های بزرگ بهتر است از ساختارهای مدل مشخص استفاده کنید تا نگاشت داده‌ها خواناتر و خطاهای نوع داده زودتر شناسایی شوند.

همچنین استفاده از SELECT * در کوئری‌های اصلی همیشه بهترین انتخاب نیست. وقتی فقط سه ستون نیاز دارید، انتخاب همان سه ستون باعث می‌شود وابستگی کد به ساختار جدول کمتر شود و داده غیرضروری دریافت نشود. برای نمایش فهرست بیماران نیز بهتر است صفحه‌بندی، مرتب‌سازی و فیلتر را در سمت پایگاه داده انجام دهید، نه اینکه تمام رکوردها را بدون محدودیت به حافظه برنامه منتقل کنید.

۱۴. به‌روزرسانی و حذف اطلاعات

برای اصلاح اطلاعات موجود از دستور UPDATE و برای حذف رکورد از DELETE استفاده می‌شود. در هر دو حالت، شرط WHERE اهمیت زیادی دارد. اگر شرط را فراموش کنید، عملیات ممکن است روی تمام رکوردهای جدول اعمال شود. این اشتباه در یک برنامه واقعی می‌تواند باعث از دست رفتن اطلاعات یا تغییر ناخواسته تعداد زیادی از پرونده‌ها شود.

نمونه به‌روزرسانی شماره تماس یک بیمار:

let result = sqlx::query(
    "UPDATE Patients SET PhoneNumber = ? WHERE Id = ?"
)
.bind("09121111111")
.bind(1_i64)
.execute(&pool)
.await?;

println!("تعداد ردیف‌های تغییرکرده: {}", result.rows_affected());

متغیر result اطلاعات نتیجه اجرای دستور را نگه می‌دارد و rows_affected() تعداد ردیف‌های تحت تأثیر را گزارش می‌کند. با این حال، صفر بودن تعداد ردیف‌های تغییرکرده همیشه به معنای خطا نیست؛ ممکن است شناسه وجود نداشته باشد یا مقدار جدید با مقدار قبلی یکسان باشد. بنابراین باید رفتار برنامه را براساس معنای عملیات مورد انتظار طراحی کنید.

حذف اطلاعات نیز باید با دقت انجام شود:

sqlx::query("DELETE FROM Patients WHERE Id = ?")
    .bind(1_i64)
    .execute(&pool)
    .await?;

در سامانه‌های پزشکی یا مالی، حذف فیزیکی رکورد همیشه بهترین راهکار نیست. گاهی استفاده از حذف منطقی، مانند ستون IsDeleted، برای حفظ سابقه و امکان گزارش‌گیری مناسب‌تر است. البته حذف منطقی به‌تنهایی کنترل دسترسی، ثبت تاریخچه تغییرات یا سیاست نگهداری اطلاعات را تأمین نمی‌کند؛ این موارد باید جداگانه طراحی شوند.

۱۵. استفاده از پارامترها و جلوگیری از SQL Injection

یکی از اشتباهات جدی در برنامه‌نویسی پایگاه داده، ساختن دستور SQL با چسباندن رشته‌های ورودی کاربر است. فرض کنید کاربر در فرم جست‌وجوی بیماران نامی وارد می‌کند و شما آن را مستقیماً داخل کوئری قرار می‌دهید. اگر ورودی به‌صورت ناامن در دستور SQL قرار بگیرد، ممکن است ساختار کوئری تغییر کند و مهاجم بتواند عملیاتی انجام دهد که برنامه‌نویس قصد اجرای آن را نداشته است. به این آسیب‌پذیری SQL Injection گفته می‌شود.

روش اشتباه:

let name = "علی";

let sql = format!(
    "SELECT * FROM Patients WHERE FirstName = '{name}'"
);

در این مثال، مقدار name مستقیماً وارد رشته SQL می‌شود. اگر این مقدار از ورودی کاربر دریافت شود، چنین الگویی می‌تواند خطرناک باشد. روش صحیح استفاده از پارامترهای کوئری است:

let patients = sqlx::query(
    "SELECT Id, FirstName, LastName
     FROM Patients
     WHERE FirstName = ?"
)
.bind(name)
.fetch_all(&pool)
.await?;

در این روش، مقدار ورودی به‌عنوان داده ارسال می‌شود، نه بخشی از دستور SQL. استفاده از پارامترها علاوه بر افزایش امنیت، کد را خواناتر می‌کند و مدیریت انواع داده را به کتابخانه و درایور می‌سپارد.

البته یک نکته فنی مهم وجود دارد: پارامترها برای جایگزینی مقادیر هستند، نه نام جدول، نام ستون یا جهت مرتب‌سازی. برای مثال، اگر کاربر بتواند مقدار sort_by را تعیین کند، نباید آن را بدون بررسی به متن کوئری اضافه کنید. باید نام ستون‌های مجاز را با یک فهرست مشخص محدود کنید و سپس دستور موردنظر را بسازید. بنابراین استفاده از bind ضروری است، اما به‌تنهایی تمام مشکلات امنیتی یک سیستم پایگاه داده را حل نمی‌کند.

۱۶. آشنایی با نوع داده‌ها و مقادیر NULL

SQLite در مقایسه با بسیاری از پایگاه‌های داده رابطه‌ای، رویکرد انعطاف‌پذیری نسبت به نوع داده دارد. در این موتور، نوع اعلام‌شده ستون همیشه به این معنا نیست که فقط یک نوع مشخص از مقدار در آن ستون پذیرفته می‌شود؛ مگر اینکه از سازوکارهای محدودکننده مناسب، مانند جداول STRICT در نسخه‌های پشتیبانی‌شده، استفاده کنید. این ویژگی گاهی کار را ساده می‌کند، اما اگر بدون توجه به نوع داده طراحی کنید، ممکن است اطلاعات ناسازگار در پایگاه داده ذخیره شوند.

 

انواع رایج ذخیره‌سازی در SQLite عبارت‌اند از:

نوع ذخیره‌سازی

کاربرد متداول

NULL

نبود مقدار

INTEGER

اعداد صحیح

REAL

اعداد ممیز شناور

TEXT

متن و رشته

BLOB

داده‌های باینری

برای مثال، شناسه بیمار می‌تواند از نوع INTEGER باشد، نام و نام خانوادگی از نوع TEXT و شماره تماس نیز از نوع TEXT ذخیره شود. شماره تلفن و کد ملی نباید به‌عنوان اعداد محاسباتی در نظر گرفته شوند؛ زیرا صفرهای ابتدایی آن‌ها معنا دارند. تاریخ نیز باید با یک قرارداد مشخص ذخیره شود، مثلاً به شکل متن استاندارد YYYY-MM-DD، تا مقایسه و مرتب‌سازی آن قابل پیش‌بینی باشد.

در Rust، اگر یک ستون می‌تواند مقدار تهی داشته باشد، معمولاً باید از نوع Option استفاده کنید. برای نمونه، شماره تماس اختیاری می‌تواند به‌صورت Option تعریف شود. مقدار Some("09120000000") نشان‌دهنده وجود شماره تماس و مقدار None نشان‌دهنده نبود آن است. این تطبیق بین مدل داده و ساختار زبان، به شما کمک می‌کند حالت‌های اختیاری را به‌صورت صریح مدیریت کنید.

تفاوت NULL با رشته خالی نیز مهم است. رشته خالی یعنی مقدار متنی وجود دارد، اما طول آن صفر است؛ در مقابل، NULL به معنای نبود مقدار است. اگر این دو حالت را بدون قرارداد مشخص در برنامه ترکیب کنید، ممکن است جست‌وجو، گزارش‌گیری و اعتبارسنجی داده‌ها رفتار غیرمنتظره‌ای داشته باشند.

۱۷. مدیریت خطاها در Rust و SQLx

برنامه‌ای که فقط در شرایط موفق کار می‌کند، هنوز برای استفاده واقعی آماده نیست. هنگام اتصال به SQLite ممکن است مسیر فایل اشتباه باشد، مجوز نوشتن وجود نداشته باشد، محدودیت یکتا نقض شود یا یک کوئری به دلیل نام اشتباه ستون اجرا نشود. Rust با استفاده از نوع‌هایی مانند Result به شما اجازه می‌دهد این وضعیت‌ها را به‌شکل صریح مدیریت کنید.

در مثال‌های قبلی، تابع main این امضا را داشت:

async fn main() -> Result<(), sqlx::Error>

این امضا یعنی تابع یا با موفقیت و بدون مقدار خروجی معنادار پایان می‌یابد، یا یک خطای sqlx::Error برمی‌گرداند. عملگر ? نیز در صورت وقوع خطا، اجرای مسیر فعلی را متوقف می‌کند و خطا را به فراخواننده منتقل می‌کند. این روش برای نمونه‌های آموزشی مناسب است؛ اما در یک برنامه تجاری، انتقال خام خطا به رابط کاربری معمولاً انتخاب مناسبی نیست.

برای مثال، هنگام ثبت بیمار با کد ملی تکراری، بهتر است پیام مشخصی مانند «این کد ملی قبلاً ثبت شده است» نمایش داده شود. در مقابل، خطای باز نشدن فایل پایگاه داده باید در گزارش فنی ثبت شود و به کاربر پیام قابل‌فهمی ارائه دهد. بهتر است خطاهای داخلی، اطلاعات حساس مسیر فایل و جزئیات پیاده‌سازی را بدون ضرورت در رابط کاربری افشا نکنید.

یک الگوی ساده برای لایه بالاتر می‌تواند چنین باشد:

async fn create_patient(
    pool: &sqlx::SqlitePool,
    first_name: &str,
    last_name: &str,
    national_code: &str,
) -> Result<(), sqlx::Error> {
    sqlx::query(
        "INSERT INTO Patients
         (FirstName, LastName, NationalCode)
         VALUES (?, ?, ?)"
    )
    .bind(first_name)
    .bind(last_name)
    .bind(national_code)
    .execute(pool)
    .await?;

    Ok(())
}

این تابع یک مسئولیت مشخص دارد: درج بیمار. در مراحل بعدی می‌توانید اعتبارسنجی ورودی، تبدیل خطاها و ثبت گزارش را در لایه‌های مناسب اضافه کنید. مهم این است که ساختار برنامه طوری طراحی شود که هر تابع وظیفه مشخصی داشته باشد و خطاها به‌جای نادیده گرفته شدن، به‌درستی مدیریت شوند.

۱۸. تراکنش‌ها و حفظ یکپارچگی اطلاعات

تراکنش یا Transaction مجموعه‌ای از عملیات پایگاه داده است که باید به‌عنوان یک واحد منطقی مدیریت شود. تصور کنید در نرم‌افزار کلینیک می‌خواهید یک نوبت جدید ثبت کنید و هم‌زمان وضعیت یک ظرفیت پذیرش را تغییر دهید. اگر عملیات اول موفق شود ولی عملیات دوم شکست بخورد، ممکن است اطلاعات ناسازگار شوند. تراکنش کمک می‌کند این عملیات به‌صورت یک مجموعه کنترل شوند و در صورت بروز خطا، تغییرات تأییدنشده به حالت قبل بازگردند.

در SQLx می‌توانید تراکنش را به این شکل آغاز کنید:

let mut tx = pool.begin().await?;

sqlx::query(
    "INSERT INTO Patients
     (FirstName, LastName, NationalCode)
     VALUES (?, ?, ?)"
)
.bind("علی")
.bind("احمدی")
.bind("0012345678")
.execute(&mut *tx)
.await?;

tx.commit().await?;

در این مثال، ابتدا تراکنش آغاز می‌شود، سپس عملیات درج درون آن انجام می‌شود و در پایان با commit تغییرات تأیید می‌شوند. اگر عملیات لازم دیگری هم دارید، باید آن را نیز پیش از تأیید نهایی در تراکنش اجرا کنید. اگر یک عملیات ضروری با خطا روبه‌رو شود، نباید تراکنش را به‌صورت موفق تأیید کنید.

تراکنش‌ها در SQLite برای حفظ یکپارچگی داده‌ها اهمیت زیادی دارند، اما نباید بیش از نیاز باز بمانند. تراکنش طولانی می‌تواند دسترسی نوشتن سایر عملیات را به تأخیر بیندازد. همچنین تراکنش جایگزین محدودیت‌های پایگاه داده نیست؛ بهتر است قواعدی مانند یکتا بودن کد ملی و صحت روابط بین جدول‌ها تا حد امکان در ساختار دیتابیس نیز اعمال شوند. به این ترتیب، صحت داده‌ها فقط به اجرای صحیح منطق برنامه وابسته نخواهد بود.

۱۹. نکات مهم برای استفاده از SQLite در برنامه‌های دسکتاپ

اگر قصد دارید SQLite را در برنامه‌ای با Rust و Tauri استفاده کنید، صرفاً اتصال موفق به پایگاه داده کافی نیست. باید محل ذخیره فایل، نحوه اشتراک‌گذاری اتصال، مدیریت هم‌زمانی، امنیت اطلاعات و شیوه ارتقای ساختار پایگاه داده را نیز در نظر بگیرید. برای مثال، قرار دادن فایل physio.db در مسیر کاری فعلی برنامه ممکن است هنگام اجرای برنامه از میان‌بر، نصب‌کننده یا محیط توسعه نتیجه متفاوتی ایجاد کند. بهتر است مسیر داده‌ها را براساس امکانات پلتفرم و محل مناسب نگهداری داده‌های کاربر تعیین کنید.

در معماری Tauri، یک الگوی مناسب این است که رابط React از طریق فرمان‌های Tauri درخواست ثبت یا دریافت اطلاعات را ارسال کند و کد Rust مسئول اجرای عملیات پایگاه داده باشد. در این معماری، اطلاعات ورودی باید در سمت Rust اعتبارسنجی شوند؛ زیرا رابط کاربری را نمی‌توان مرز امنیتی قابل‌اعتمادی در نظر گرفت. سپس نتایج در قالب ساختارهای قابل سریال‌سازی به رابط کاربری بازگردانده می‌شوند. این جداسازی، توسعه فرم‌های ثبت بیمار، فهرست نوبت‌ها و گزارش‌های مالی را ساده‌تر می‌کند.

نکته دیگر، تنظیم اتصال‌های SQLite است. اگر چند عملیات هم‌زمان انجام می‌شوند، باید رفتار قفل‌ها، زمان انتظار و تعداد اتصال‌های مجموعه را آزمایش کنید. فعال کردن حالت WAL نیز می‌تواند در برخی الگوهای کاری، هم‌زمانی خواندن و نوشتن را بهبود دهد؛ اما چند نویسنده را به چند عملیات نوشتن کاملاً مستقل و هم‌زمان تبدیل نمی‌کند. انتخاب تنظیمات باید با توجه به حجم واقعی عملیات و شرایط استقرار انجام شود.

در نهایت، از همان ابتدا برای پشتیبان‌گیری و ارتقای دیتابیس برنامه‌ریزی کنید. فایل پایگاه داده ممکن است در طول عمر برنامه اطلاعات ارزشمندی جمع‌آوری کند؛ بنابراین نسخه پشتیبان، آزمایش بازیابی، مهاجرت ساختار و جلوگیری از دسترسی غیرمجاز به فایل بخشی از طراحی نرم‌افزار هستند. اگر اطلاعات حساس نگهداری می‌کنید، باید الزامات امنیتی و قانونی مربوط به حفاظت از آن‌ها را نیز جداگانه بررسی کنید.

 

طراحی اصولی پایگاه داده SQLite در Rust و مدیریت ساختار جداول

چرا طراحی پایگاه داده قبل از کدنویسی اهمیت دارد؟

یکی از مهم‌ترین مراحل ساخت نرم‌افزار با Rust و SQLite، طراحی صحیح ساختار پایگاه داده پیش از نوشتن توابع مربوط به ذخیره‌سازی اطلاعات است. اگر جداول، ستون‌ها و ارتباط میان داده‌ها به‌درستی تعریف نشوند، با افزایش حجم اطلاعات، اصلاح ساختار برنامه دشوارتر خواهد شد. طراحی مناسب پایگاه داده به جلوگیری از ثبت داده‌های تکراری، کاهش خطاهای منطقی و ساده‌تر شدن گزارش‌گیری کمک می‌کند. به همین دلیل، بهتر است پیش از نوشتن کوئری‌های SQL، موجودیت‌های اصلی برنامه و ارتباط میان آن‌ها مشخص شوند.

انتخاب نوع داده مناسب در SQLite

SQLite برخلاف بسیاری از پایگاه‌های داده رابطه‌ای، سیستم نوع‌دهی انعطاف‌پذیری دارد و بیشتر مقادیر را بر اساس کلاس ذخیره‌سازی آن‌ها مدیریت می‌کند. کلاس‌های اصلی ذخیره‌سازی شامل NULL، INTEGER، REAL، TEXT و BLOB هستند. هنگام طراحی جدول باید نوع داده هر ستون با کاربرد آن هماهنگ باشد؛ برای مثال، شناسه بیمار می‌تواند از نوع INTEGER و نام او از نوع TEXT باشد. استفاده درست از نوع داده باعث خوانایی بهتر ساختار دیتابیس و کاهش خطاهای برنامه‌نویسی می‌شود.

ایجاد جدول با کلید اصلی و محدودیت‌های مناسب

کلید اصلی یا PRIMARY KEY برای شناسایی یکتای هر رکورد استفاده می‌شود. در یک جدول بیماران، معمولاً می‌توان از یک شناسه عددی به‌عنوان کلید اصلی استفاده کرد و برای کد ملی نیز محدودیت یکتا بودن در نظر گرفت. این محدودیت کمک می‌کند یک کد ملی به‌صورت ناخواسته برای چند بیمار ثبت نشود. البته در برنامه واقعی باید مقدارهای ورودی نیز اعتبارسنجی شوند؛ محدودیت‌های دیتابیس مکمل اعتبارسنجی برنامه هستند، نه جایگزین کامل آن.

CREATE TABLE IF NOT EXISTS patients (
    id INTEGER PRIMARY KEY,
    first_name TEXT NOT NULL,
    last_name TEXT NOT NULL,
    national_code TEXT NOT NULL UNIQUE,
    phone_number TEXT,
    is_active INTEGER NOT NULL DEFAULT 1
);

در این مثال، نام و نام خانوادگی بیمار اجباری هستند و کد ملی نمی‌تواند تکراری باشد. ستون is_active نیز به‌صورت پیش‌فرض مقدار یک می‌گیرد تا وضعیت فعال بودن بیمار مشخص شود. اگر قرار است شماره تلفن اختیاری باشد، می‌توان آن را بدون NOT NULL تعریف کرد. توجه داشته باشید که محدودیت‌های دقیق باید با قوانین واقعی کسب‌وکار و شیوه ذخیره‌سازی اطلاعات هماهنگ باشند.

تعریف ارتباط میان جداول با کلید خارجی

در بسیاری از نرم‌افزارها، اطلاعات به یک جدول محدود نمی‌شوند. برای مثال، هر بیمار ممکن است به یک شرکت بیمه و هر شرکت بیمه به چند صندوق بیمه مرتبط باشد. در چنین شرایطی بهتر است اطلاعات بیمه در جدول مستقل نگهداری شود و جدول بیماران فقط شناسه بیمه را ذخیره کند. این طراحی از تکرار نام و توضیحات بیمه در رکوردهای متعدد جلوگیری می‌کند و تغییر اطلاعات بیمه را ساده‌تر می‌سازد. ارتباط میان جداول معمولاً با استفاده از کلید خارجی یا FOREIGN KEY تعریف می‌شود.

CREATE TABLE IF NOT EXISTS insurances (
    id INTEGER PRIMARY KEY,
    insurance_name TEXT NOT NULL UNIQUE
);

CREATE TABLE IF NOT EXISTS patients (
    id INTEGER PRIMARY KEY,
    first_name TEXT NOT NULL,
    last_name TEXT NOT NULL,
    insurance_id INTEGER,
    FOREIGN KEY (insurance_id)
        REFERENCES insurances(id)
);

تعریف کلید خارجی به‌تنهایی کافی نیست؛ در SQLite باید فعال بودن بررسی کلیدهای خارجی نیز در اتصال دیتابیس تضمین شود. برای نمونه، هنگام استفاده از SQLx می‌توان در زمان ایجاد اتصال یا از طریق گزینه‌های اتصال، قابلیت foreign key را فعال کرد. همچنین باید رفتار حذف یا تغییر رکوردهای مرتبط را مشخص کرد؛ برای مثال، آیا حذف یک بیمه باید مجاز باشد یا تا زمانی که بیمارانی به آن وابسته‌اند، باید از حذف آن جلوگیری شود؟ پاسخ به این پرسش به قواعد برنامه بستگی دارد.

اجرای کوئری‌های پارامتری با SQLx

هنگام ثبت اطلاعات، نباید مقادیر ورودی کاربر را با اتصال رشته‌ها مستقیماً داخل دستور SQL قرار داد. این کار علاوه بر ایجاد مشکلات مربوط به کوتیشن و قالب‌بندی، می‌تواند زمینه حملات SQL Injection را فراهم کند. در SQLx بهتر است از کوئری‌های پارامتری و متدهایی مانند bind استفاده شود. با این روش، دستور SQL از مقادیر ورودی جدا می‌ماند و کتابخانه آن‌ها را به شکل مناسب به دیتابیس ارسال می‌کند.

sqlx::query(
    "INSERT INTO patients (first_name, last_name, national_code)
     VALUES (?, ?, ?)"
)
.bind("علی")
.bind("رضایی")
.bind("1234567890")
.execute(&pool)
.await?;

در این نمونه، علامت سؤال‌ها جایگاه پارامترها را مشخص می‌کنند و مقادیر با فراخوانی bind به آن‌ها متصل می‌شوند. ترتیب فراخوانی bind باید با ترتیب پارامترهای دستور SQL مطابقت داشته باشد. در برنامه واقعی، مقدارها باید از فرم یا لایه ورودی برنامه دریافت شوند و پیش از ذخیره‌سازی نیز اعتبارسنجی شوند. برای داده‌های حساس، علاوه بر کوئری پارامتری، باید مجوزهای دسترسی و سیاست‌های نگهداری اطلاعات نیز رعایت شوند.

چرا باید از ذخیره اطلاعات تکراری پرهیز کنیم؟

فرض کنید نام شرکت بیمه مستقیماً در جدول بیماران ذخیره شود. اگر نام آن شرکت تغییر کند، ممکن است مجبور شوید تعداد زیادی رکورد را اصلاح کنید؛ ضمن اینکه احتمال باقی ماندن نام‌های قدیمی وجود دارد. با جدا کردن اطلاعات بیمه در جدول مستقل، نام شرکت فقط در یک محل نگهداری می‌شود و سایر جداول به شناسه آن ارجاع می‌دهند. این رویکرد نمونه‌ای از نرمال‌سازی پایگاه داده است که به کاهش تکرار و حفظ یکپارچگی اطلاعات کمک می‌کند. البته ساختار نهایی باید متناسب با نوع گزارش‌ها و نیازهای واقعی نرم‌افزار طراحی شود.

مدیریت تغییرات ساختار دیتابیس با Migration

در طول توسعه نرم‌افزار ممکن است ستون جدیدی به جدول اضافه شود یا ساختار یک موجودیت تغییر کند. اگر تغییرات دیتابیس به‌صورت دستی و بدون ثبت نسخه انجام شوند، هماهنگ نگه داشتن نسخه‌های مختلف برنامه دشوار خواهد شد. Migration راهکاری برای ثبت و اجرای مرحله‌به‌مرحله تغییرات ساختار پایگاه داده است. در SQLx می‌توان از قابلیت Migration و پوشه‌ای مانند migrations استفاده کرد تا تغییرات در فایل‌های مجزا ذخیره شوند و هنگام راه‌اندازی برنامه، نسخه دیتابیس با ساختار مورد انتظار هماهنگ شود.

برای مثال، ممکن است در نسخه نخست جدول بیماران فقط شامل نام، نام خانوادگی و کد ملی باشد؛ اما در نسخه بعدی به ستون تاریخ تولد نیاز پیدا کنید. به‌جای حذف دیتابیس و ساخت دوباره آن، می‌توانید یک Migration جدید برای افزودن ستون ایجاد کنید. این روش در برنامه‌های دسکتاپ اهمیت زیادی دارد، زیرا کاربران ممکن است اطلاعات ارزشمندی را طی ماه‌ها یا سال‌ها در دیتابیس محلی ذخیره کرده باشند. پیش از اعمال تغییرات ساختاری روی اطلاعات واقعی، تهیه نسخه پشتیبان و آزمایش Migration ضروری است.

 

طراحی اصولی SQLite در Rust فقط به نوشتن چند دستور CREATE TABLE محدود نمی‌شود. انتخاب نوع داده مناسب، تعیین کلید اصلی، تعریف محدودیت‌های یکتا، طراحی روابط میان جداول و استفاده از Migration، همگی در پایداری نرم‌افزار نقش دارند. در کنار این موارد، استفاده از کوئری‌های پارامتری و اعتبارسنجی ورودی‌ها نیز برای جلوگیری از خطا و کاهش خطرات امنیتی ضروری است. اگر از ابتدای پروژه ساختار دیتابیس را با دقت طراحی کنید، توسعه قابلیت‌هایی مانند جست‌وجو، گزارش‌گیری، مدیریت بیمه و ثبت اطلاعات بیماران در مراحل بعدی بسیار ساده‌تر خواهد شد.

جمع‌بندی و مسیر ادامه آموزش

در این قسمت با مفاهیم پایه SQLite در Rust آشنا شدیم و دیدیم که چگونه می‌توان یک پروژه ساده ایجاد کرد، وابستگی SQLx را افزود، به پایگاه داده متصل شد و دستورات اصلی SQL را اجرا کرد. همچنین درباره طراحی جدول، کلید اصلی، کلید خارجی، پارامترهای امن، مدیریت خطا و تراکنش‌ها صحبت کردیم. این مفاهیم پایه‌ای هستند که قبل از ساخت لایه داده یک نرم‌افزار واقعی باید به‌خوبی درک شوند.

اگر قصد دارید این مسیر را به‌شکل حرفه‌ای ادامه دهید، مرحله بعدی باید از مثال‌های ساده به سمت طراحی یک ساختار قابل نگهداری حرکت کند. در یک پروژه واقعی، بهتر است مدل‌های Rust را تعریف کنید، کوئری‌ها را در ماژول‌های مشخص قرار دهید، از مهاجرت‌های پایگاه داده استفاده کنید و توابعی برای جست‌وجو، صفحه‌بندی، مرتب‌سازی و شمارش رکوردها بنویسید. همچنین باید آزمون‌هایی برای خطاهای متداول، مانند کد ملی تکراری، ورودی نامعتبر و شکست عملیات تراکنشی طراحی کنید.

برای نمونه، در یک سامانه مدیریت کلینیک، می‌توان در ادامه جدولی برای بیمه‌ها و صندوق‌های بیمه ایجاد کرد و سپس با استفاده از JOIN اطلاعات بیماران را همراه با نام بیمه نمایش داد. بعد از آن، نوبت به طراحی Repository، مدیریت اتصال مشترک در Tauri و تبدیل خروجی کوئری‌ها به مدل‌های مناسب رابط React می‌رسد. وقتی این اصول به‌درستی پیاده‌سازی شوند، اضافه کردن امکانات جدید به برنامه بسیار ساده‌تر خواهد شد.

مهم‌ترین نکته این است که SQLite را فقط به‌عنوان یک فایل ذخیره اطلاعات نبینید؛ آن را بخشی از معماری نرم‌افزار در نظر بگیرید. انتخاب درست ساختار داده، مدیریت خطا و امنیت کوئری‌ها از همان ابتدای پروژه، از مشکلات جدی در مراحل بعدی جلوگیری می‌کند.
 برای پیاده‌سازی CRUD در SQLite میتوانید این مقاله آموزش را مشاهده کنید.

کلیدواژه ها

SQLite در Rust, آموزش SQLite, آموزش Rust, کار با SQLite در Rust, SQLx در Rust, اتصال Rust به SQLite, پایگاه داده SQLite, آموزش SQLx, برنامه نویسی Rust, دیتابیس در Rust, آموزش Tauri, SQLite با Tauri, SQL Injection در Rust, تراکنش در SQLite

پرسش و پاسخ

1 . آیا برای استفاده از SQLite در Rust باید SQLite را جداگانه نصب کنم؟
معمولاً برای استفاده از SQLite از طریق کتابخانه‌هایی مانند SQLx نیازی به راه‌اندازی یک سرور مستقل ندارید. با این حال، نحوه تأمین کتابخانه بومی SQLite به تنظیمات ساخت پروژه و ویژگی‌های فعال‌شده وابسته است. بنابراین در محیط توسعه و هنگام ساخت نسخه نهایی، باید مطمئن شوید وابستگی‌های بومی موردنیاز به‌درستی در دسترس هستند.

2 . برای پروژه Tauri، SQLx بهتر است یا rusqlite؟
اگر بک‌اند شما از عملیات ناهمگام استفاده می‌کند و می‌خواهید اتصال‌ها و کوئری‌ها را با async/await مدیریت کنید، SQLx گزینه مناسبی است. اگر برنامه کوچک‌تری دارید و عملیات همگام برای معماری شما کافی است، rusqlite نیز قابل بررسی است. انتخاب نهایی باید براساس ساختار پروژه، نیازهای عملکردی و نحوه مدیریت عملیات پایگاه داده انجام شود.

3 . آیا SQLite برای ذخیره اطلاعات هزاران بیمار مناسب است؟
بله، هزاران رکورد به‌خودی‌خود حجم غیرمعمولی برای SQLite محسوب نمی‌شوند. عملکرد واقعی به طراحی جدول‌ها، شاخص‌ها، پیچیدگی کوئری‌ها، سخت‌افزار و الگوی دسترسی بستگی دارد. اگر تعداد زیادی کاربر هم‌زمان در حال نوشتن اطلاعات هستند، باید محدودیت‌های هم‌زمانی را ارزیابی کنید و احتمال استفاده از پایگاه داده سرورمحور را نیز در نظر بگیرید.

4 . آیا فایل SQLite را می‌توان مستقیماً بین رایانه‌ها کپی کرد؟
فایل SQLite قابل‌حمل است، اما کپی کردن فایل هنگام فعال بودن برنامه و وجود تراکنش‌های باز ممکن است نسخه پشتیبان ناسازگاری ایجاد کند. برای تهیه نسخه پشتیبان از یک پایگاه داده فعال، بهتر است از روش‌های پشتیبان‌گیری مناسب SQLite استفاده کنید و بازیابی نسخه پشتیبان را نیز آزمایش کنید.

5 . آیا استفاده از SQLite به معنی امن بودن اطلاعات است؟
خیر. SQLite امکاناتی برای حفظ یکپارچگی داده‌ها و اجرای کوئری‌های پارامتری دارد، اما امنیت کامل به طراحی برنامه وابسته است. باید ورودی‌ها را اعتبارسنجی کنید، دسترسی به فایل پایگاه داده را محدود کنید، خطاها را درست مدیریت کنید و در صورت نگهداری اطلاعات حساس، الزامات رمزنگاری و حفاظت از داده‌ها را نیز بررسی کنید.

دیدگاه ها

برای ارسال دیدگاه وارد حساب کاربری خود شوید.