۱. آشنایی با CRUD و نقش آن در برنامه
CRUD مخفف چهار عملیات اصلی Create، Read، Update و Delete است که تقریباً در هر نرمافزار دادهمحوری به آنها نیاز داریم. فرض کنید در حال توسعهی یک نرمافزار مدیریت کلینیک هستید و میخواهید اطلاعات بیماران را ذخیره کنید. برای ثبت بیمار جدید به Create، برای مشاهدهی فهرست بیماران به Read، برای اصلاح شماره تلفن یا نشانی به Update و برای حذف یک رکورد به Delete نیاز خواهید داشت. SQLite اطلاعات را در یک پایگاه دادهی فایلمحور نگهداری میکند و Rust امکان ساخت لایهای سریع، ایمن و قابل نگهداری برای دسترسی به این اطلاعات را فراهم میکند.
نکتهی مهم این است که پیادهسازی CRUD صرفاً به نوشتن چهار دستور SQL محدود نمیشود. باید نوع دادهها، اعتبارسنجی ورودیها، مدیریت خطاها، امنیت کوئریها و رفتار برنامه هنگام پیدا نشدن رکورد را نیز مشخص کنید. اگر این موارد را نادیده بگیرید، ممکن است برنامه در شرایط عادی درست کار کند، اما هنگام دریافت دادهی نامعتبر یا اجرای همزمان چند عملیات با مشکل مواجه شود. بنابراین در این آموزش، CRUD را بهعنوان بخشی از معماری برنامه بررسی میکنیم، نه مجموعهای از کوئریهای پراکنده.
چهار عملیات اصلی پایگاه داده
|
عملیات |
دستور SQL |
کاربرد |
|---|---|---|
|
Create |
|
افزودن رکورد جدید |
|
Read |
|
خواندن اطلاعات |
|
Update |
|
تغییر اطلاعات موجود |
|
Delete |
|
حذف رکورد |
برای اجرای این عملیات در Rust میتوان از کتابخانهی rusqlite یا SQLx استفاده کرد. کتابخانهی rusqlite رابطی مستقیم برای SQLite ارائه میدهد؛ در مقابل، SQLx برای اجرای کوئریهای ناهمگام و استفاده از connection pool مناسب است. مستندات رسمی هر دو کتابخانه، روش اجرای کوئری و ارسال پارامترها را توضیح میدهند.
در ادامه از rusqlite برای ارائهی یک مثال ساده و قابل اجرا استفاده میکنیم. اگر پروژهی شما مبتنی بر Tauri و SQLx است، ساختار مفهومی CRUD یکسان خواهد بود، اما APIهای اجرایی متفاوت هستند.
۲. آمادهسازی پروژه Rust و SQLite
ابتدا یک پروژهی جدید Rust ایجاد کنید. در ترمینال، دستورهای زیر را اجرا کنید:
cargo new sqlite_crud
cd sqlite_crud
سپس وابستگی موردنیاز را اضافه کنید:
cargo add rusqlite --features bundled
ویژگی bundled باعث میشود نسخهی SQLite موردنیاز کتابخانه همراه وابستگی ساخته شود و در بسیاری از محیطها نیاز به نصب جداگانهی کتابخانهی SQLite سیستم کاهش پیدا کند. البته این ویژگی به معنای حذف تمام نیازمندیهای ساخت نیست؛ ابزارهای کامپایل مناسب همچنان باید در محیط توسعه وجود داشته باشند.
اگر از قبل در پروژهی Tauri خود SQLx را راهاندازی کردهاید، صرفاً برای اجرای CRUD نیازی نیست rusqlite را نیز به همان پروژه اضافه کنید. انتخاب همزمان دو لایهی دسترسی به SQLite معمولاً پیچیدگی غیرضروری ایجاد میکند، مگر اینکه دلیل فنی مشخصی برای استفاده از هر دو داشته باشید.
ساختار اولیهی پروژه
برای شروع، فایل src/main.rs کافی است. با بزرگتر شدن پروژه، بهتر است مدلها، عملیات پایگاه داده و لایهی فرمانهای برنامه را از یکدیگر جدا کنید. چنین جداسازیای باعث میشود تغییرات آینده، مانند افزودن صفحهبندی، جستوجو یا گزارشگیری، سادهتر شوند.
۳. طراحی جدول و مدل داده
قبل از نوشتن توابع CRUD، باید مشخص کنیم چه دادههایی ذخیره میشوند و چه محدودیتهایی دارند. برای مثال، جدول بیماران را با فیلدهای شناسه، نام، نام خانوادگی و شماره ملی در نظر بگیرید. شناسه را بهصورت کلید اصلی تعریف میکنیم تا SQLite بتواند هر رکورد را بهطور مستقل شناسایی کند. شماره ملی نیز در این مثال یکتا خواهد بود؛ بنابراین ثبت دو بیمار با شماره ملی یکسان نباید مجاز باشد.
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
);
استفاده از NOT NULL مانع ذخیرهی مقدار NULL در ستونهای مشخصشده میشود، اما بهتنهایی رشتهی خالی را رد نمیکند. به همین دلیل، اعتبارسنجی نام و شماره ملی باید در لایهی برنامه نیز انجام شود. محدودیت UNIQUE نیز برای جلوگیری از تکرار شماره ملی ضروری است؛ بررسی تکراری بودن فقط در رابط کاربری کافی نیست، زیرا ممکن است دو درخواست همزمان از مرحلهی بررسی عبور کنند.
تعریف ساختار داده در Rust
اکنون مدل متناظر با جدول را تعریف میکنیم. در این مثال، از i64 برای شناسه استفاده میکنیم، زیرا با نوع عدد صحیح متداول برای کلید اصلی SQLite سازگار است.
#[derive(Debug, Clone)]
struct Patient {
id: i64,
first_name: String,
last_name: String,
national_code: String,
}
#[derive(Debug)]
struct NewPatient {
first_name: String,
last_name: String,
national_code: String,
}
چرا دو ساختار جداگانه داریم؟ هنگام ایجاد بیمار جدید، شناسه هنوز در اختیار برنامه نیست و SQLite آن را تولید میکند؛ اما هنگام خواندن رکورد، شناسه بخشی از اطلاعات خروجی است. تفکیک Patient از NewPatient این تفاوت را روشن میکند و احتمال ارسال شناسهی اشتباه هنگام درج اطلاعات را کاهش میدهد.
در پروژههای بزرگتر میتوانید مدلهای دیگری مانند UpdatePatient یا ساختارهای مخصوص درخواستهای API نیز تعریف کنید. نکتهی اصلی این است که مدل پایگاه داده را با ورودی فرم یا پاسخ رابط کاربری یکی فرض نکنید؛ این لایهها ممکن است در طول توسعه به شکلهای متفاوتی تغییر کنند.
۴. پیادهسازی عملیات Create
عملیات Create رکورد جدیدی را در پایگاه داده ثبت میکند. در rusqlite، متد execute برای اجرای دستورهایی مانند INSERT، UPDATE و DELETE مناسب است. پارامترها را باید جدا از متن SQL ارسال کنیم تا مقدار ورودی بهعنوان داده تفسیر شود، نه بخشی از دستور SQL. مستندات رسمی rusqlite نیز استفاده از پارامترهای متصلشده به کوئری را برای جلوگیری از SQL Injection توضیح میدهد.
use rusqlite::{params, Connection, Result};
fn create_patient(
conn: &Connection,
patient: &NewPatient,
) -> Result {
conn.execute(
"INSERT INTO patients
(first_name, last_name, national_code)
VALUES (?1, ?2, ?3)",
params![
patient.first_name,
patient.last_name,
patient.national_code
],
)?;
Ok(conn.last_insert_rowid())
}
در این تابع، ?1، ?2 و ?3 جایگاه پارامترها هستند. مقادیر واقعی جداگانه ارسال میشوند و نیازی به ساختن رشتهی SQL با format! نداریم. نتیجهی execute تعداد ردیفهای تغییرکرده را برمیگرداند؛ در صورت موفقیت، last_insert_rowid() شناسهی آخرین ردیف درجشده در همان اتصال را برمیگرداند.
عملگر ? در Rust خطای احتمالی را به فراخواننده منتقل میکند. این رفتار بهتر از نادیده گرفتن خطا یا متوقف کردن برنامه با unwrap() است، زیرا لایهی بالاتر میتواند خطا را ثبت کند یا پیام مناسبی به کاربر نمایش دهد. البته خطاهای پایگاه داده هنوز باید در مرز برنامه به پیامهای قابل فهم تبدیل شوند.
نمونهی استفاده از Create
fn main() -> Result<()> {
let conn = Connection::open("clinic.db")?;
conn.execute(
"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
)",
[],
)?;
let patient = NewPatient {
first_name: "علی".to_string(),
last_name: "احمدی".to_string(),
national_code: "0012345678".to_string(),
};
let id = create_patient(&conn, &patient)?;
println!("شناسه بیمار ثبتشده: {id}");
Ok(())
}
این نمونه جدول را ایجاد میکند و یک بیمار را ثبت میکند. توجه داشته باشید که مقدار شماره ملی صرفاً نمونه است و اعتبار واقعی آن بررسی نمیشود. در برنامهی واقعی باید اعتبارسنجی متناسب با قواعد کسبوکار را پیش از درج انجام دهید.
۵. پیادهسازی عملیات Read
خواندن دادهها معمولاً دو حالت دارد: دریافت یک رکورد با شناسهی مشخص و دریافت مجموعهای از رکوردها. در حالت اول، ممکن است شناسه وجود نداشته باشد؛ بنابراین بهتر است خروجی تابع از نوع Option باشد. این نوع داده بهصورت صریح مشخص میکند که نتیجه یا شامل یک بیمار است یا اصلاً رکوردی پیدا نشده است.
دریافت یک بیمار با شناسه
use rusqlite::{params, Connection, OptionalExtension, Result};
fn get_patient(
conn: &Connection,
id: i64,
) -> Result> {
conn.query_row(
"SELECT id, first_name, last_name, national_code
FROM patients
WHERE id = ?1",
params![id],
|row| {
Ok(Patient {
id: row.get("id")?,
first_name: row.get("first_name")?,
last_name: row.get("last_name")?,
national_code: row.get("national_code")?,
})
},
)
.optional()
}
متد query_row برای دریافت یک ردیف مناسب است. با استفاده از optional()، وضعیت نبودن رکورد به Ok(None) تبدیل میشود، در حالی که خطاهای واقعی مانند مشکل در اجرای کوئری یا تبدیل نوع داده همچنان بهصورت خطا باقی میمانند. برای استفاده از این متد، باید trait مربوط به OptionalExtension را وارد کنیم.
دریافت فهرست بیماران
برای دریافت چند رکورد، از prepare، سپس query_map و در نهایت جمعآوری نتایج استفاده میکنیم. این روش به ما اجازه میدهد هر ردیف را به یک نمونهی Patient تبدیل کنیم.
fn get_patients(conn: &Connection) -> Result> {
let mut stmt = conn.prepare(
"SELECT id, first_name, last_name, national_code
FROM patients
ORDER BY id DESC"
)?;
let rows = stmt.query_map([], |row| {
Ok(Patient {
id: row.get("id")?,
first_name: row.get("first_name")?,
last_name: row.get("last_name")?,
national_code: row.get("national_code")?,
})
})?;
rows.collect()
}
استفاده از ORDER BY باعث میشود ترتیب خروجی مشخص باشد؛ بدون آن، نباید به ترتیب ظاهری رکوردها تکیه کنید. همچنین collect() در این نمونه خطای هر ردیف را حفظ میکند و در صورت موفقیت، خروجی را به شکل Vec برمیگرداند. این شیوه برای تعداد محدود رکوردها مناسب است، اما در برنامهای که هزاران بیمار دارد بهتر است صفحهبندی را نیز پیادهسازی کنید.
برای مثال، در یک نرمافزار کلینیک میتوان فهرست بیماران را با LIMIT و OFFSET صفحهبندی کرد. مقدار این پارامترها باید اعتبارسنجی شود؛ بهخصوص LIMIT باید مثبت و در محدودهی معقول باشد. اگر کاربر بخواهد بر اساس نام خانوادگی مرتبسازی کند، نام ستون را مستقیماً از ورودی دریافت نکنید؛ نام ستون باید از فهرست مجاز سمت برنامه انتخاب شود.
۶. پیادهسازی عملیات Update
عملیات Update برای تغییر اطلاعات یک رکورد موجود استفاده میشود. در این مرحله باید شناسهی رکورد را بهعنوان شرط WHERE تعیین کنیم؛ حذف این شرط میتواند تمام ردیفهای جدول را تغییر دهد. چنین اشتباهی فقط یک مشکل نحوی نیست، بلکه میتواند به از دست رفتن دادههای واقعی منجر شود.
fn update_patient(
conn: &Connection,
id: i64,
patient: &NewPatient,
) -> Result {
let affected = conn.execute(
"UPDATE patients
SET first_name = ?1,
last_name = ?2,
national_code = ?3
WHERE id = ?4",
params![
patient.first_name,
patient.last_name,
patient.national_code,
id
],
)?;
Ok(affected > 0)
}
این تابع اگر حداقل یک ردیف تغییر کرده باشد true برمیگرداند. با این حال، باید تفاوت میان «رکورد وجود ندارد» و «مقادیر جدید با مقادیر قبلی یکساناند» را در نظر بگیرید؛ تعداد ردیفهای گزارششده لزوماً معیار کاملی برای تشخیص تغییر واقعی داده در تمام سناریوها نیست. اگر برنامه به تشخیص دقیق این دو وضعیت نیاز دارد، باید سیاست مشخصی برای بررسی وجود رکورد و مقایسهی مقادیر تعریف کند.
بررسی تعداد ردیفهای تغییرکرده
برای یک برنامهی ساده، مقدار بازگشتی execute اغلب کافی است. اما اگر لازم است خطاهای دامنهای را دقیقتر مدیریت کنید، بهتر است بهجای بازگرداندن یک مقدار بولی، نتیجهای مشخصتر تعریف کنید؛ مثلاً Updated، NotFound و Unchanged. همچنین محدودیت یکتایی شماره ملی ممکن است باعث خطا شود، بنابراین تغییر شماره ملی باید با مدیریت خطای پایگاه داده همراه باشد.
نکتهی امنیتی مهم این است که کاربر نباید بتواند با ارسال شناسهی دلخواه، اطلاعات بیماری را که مجاز به ویرایش آن نیست تغییر دهد. پارامتریسازی SQL از تزریق SQL جلوگیری میکند، اما جایگزین کنترل مجوز دسترسی نیست. احراز هویت و مجوزدهی باید در لایهی مناسب برنامه پیادهسازی شوند.
۷. پیادهسازی عملیات Delete
عملیات Delete برای حذف رکوردها استفاده میشود. در SQLite، دستور DELETE رکوردهای مطابق شرط WHERE را حذف میکند؛ بنابراین همیشه باید مشخص باشد که کدام رکورد هدف عملیات است. برای مثال، حذف یک بیمار بر اساس شناسه به شکل زیر انجام میشود:
fn delete_patient(
conn: &Connection,
id: i64,
) -> Result {
let affected = conn.execute(
"DELETE FROM patients WHERE id = ?1",
params![id],
)?;
Ok(affected > 0)
}
این تابع در صورت حذف حداقل یک ردیف true برمیگرداند و اگر شناسه پیدا نشود، مقدار false میدهد. البته حذف فیزیکی همیشه بهترین تصمیم نیست. در نرمافزارهای درمانی، مالی یا اداری ممکن است سوابق برای گزارشگیری، پیگیری تغییرات یا الزامات نگهداری اطلاعات موردنیاز باشند. در چنین شرایطی، حذف نرم معمولاً انتخاب مناسبتری است.
حذف فیزیکی در برابر حذف نرم
در حذف فیزیکی، رکورد از جدول حذف میشود. در حذف نرم، رکورد باقی میماند اما با یک فیلد مانند is_deleted علامتگذاری میشود تا در فهرست عادی نمایش داده نشود. برای پیادهسازی حذف نرم، ابتدا ستون مربوط را اضافه میکنیم:
ALTER TABLE patients
ADD COLUMN is_deleted INTEGER NOT NULL DEFAULT 0;
در این ساختار، مقدار صفر به معنی فعال بودن رکورد و مقدار یک به معنی حذف نرم است. برای حذف نرم بیمار میتوان تابع زیر را نوشت:
fn soft_delete_patient(
conn: &Connection,
id: i64,
) -> Result {
let affected = conn.execute(
"UPDATE patients
SET is_deleted = 1
WHERE id = ?1 AND is_deleted = 0",
params![id],
)?;
Ok(affected > 0)
}
پس از این تغییر، کوئری دریافت فهرست باید فقط بیماران حذفنشده را برگرداند:
SELECT id, first_name, last_name, national_code
FROM patients
WHERE is_deleted = 0
ORDER BY id DESC;
نکتهی مهم: حذف نرم بهتنهایی راهکار کامل حسابرسی یا نگهداری سوابق نیست. اگر برنامه باید سابقهی تغییرات را حفظ کند، لازم است زمان حذف، عامل انجامدهنده و سیاست نگهداری داده نیز جداگانه طراحی شوند. همچنین بهتر است محدودیتهای یکتایی و روابط کلید خارجی را با سیاست حذف نرم سازگار کنید؛ زیرا رکوردی که فقط علامت حذف خورده است همچنان در جدول وجود دارد.
۸. مدیریت خطاها و تراکنشها
در Rust، مدیریت خطا بخشی از طراحی تابع است، نه کاری که باید در پایان به برنامه اضافه شود. در مثالهای قبلی از rusqlite::Result و عملگر ? استفاده کردیم تا خطاها به فراخواننده منتقل شوند. این کار باعث میشود تابع پایگاه داده مسئول اجرای عملیات باشد و لایهی بالاتر تصمیم بگیرد چگونه خطا را به کاربر نمایش دهد یا آن را ثبت کند.
برای نمونه، اگر شماره ملی تکراری وارد شود، محدودیت UNIQUE خطای پایگاه داده ایجاد میکند. این خطا نباید با پیام کلی «اطلاعات ثبت شد» پنهان شود. بهتر است خطاهای شناختهشده، مانند نقض محدودیت یکتایی، به پیام مناسب تبدیل شوند و خطاهای پیشبینینشده نیز برای بررسی فنی ثبت شوند؛ بدون آنکه اطلاعات حساس بیمار در گزارشها افشا شود.
چرا تراکنش اهمیت دارد؟
فرض کنید هنگام ثبت بیمار باید اطلاعات او را در جدول بیماران و یک جدول دیگر برای سوابق اولیه ذخیره کنید. اگر درج اول موفق شود اما درج دوم شکست بخورد، ممکن است پایگاه داده در وضعیت ناقص باقی بماند. تراکنش به شما اجازه میدهد چند عملیات مرتبط را بهصورت یک واحد منطقی اجرا کنید؛ یا همهی تغییرات ثبت شوند یا در صورت شکست، تغییرات انجامشده بازگردانده شوند.
نمونهی زیر یک تراکنش ساده را نشان میدهد:
fn create_patient_transaction(
conn: &mut Connection,
patient: &NewPatient,
) -> Result {
let tx = conn.transaction()?;
tx.execute(
"INSERT INTO patients
(first_name, last_name, national_code)
VALUES (?1, ?2, ?3)",
params![
patient.first_name,
patient.last_name,
patient.national_code
],
)?;
let id = tx.last_insert_rowid();
tx.commit()?;
Ok(id)
}
در این مثال، اتصال باید بهصورت mutable دریافت شود، زیرا ایجاد تراکنش به دسترسی قابلتغییر نیاز دارد. اگر عملیات پیش از commit() با خطا مواجه شود، تراکنش با کنار رفتن از محدودهی عمر خود و بدون ثبت نهایی، بازگردانده میشود. این رفتار برای حفظ یکپارچگی دادهها مهم است.
از تراکنش برای هر دستور ساده و مستقل الزاماً نیازی نیست استفاده کنید؛ اما هر جا چند تغییر باید با هم موفق شوند، تراکنش انتخاب درستی است. بهتر است تراکنشها کوتاه باشند و در مدت باز بودن آنها عملیات طولانی و نامرتبط انجام نشود.
۹. طراحی لایه Repository
وقتی تعداد جدولها و عملیاتها زیاد میشود، قرار دادن تمام کوئریها در main.rs نگهداری برنامه را دشوار میکند. راهکار متداول، ایجاد لایهی Repository است؛ لایهای که مسئول دسترسی به دادههاست و جزئیات اجرای SQL را از منطق اصلی برنامه جدا میکند.
برای مثال، ساختار پروژه میتواند به این صورت باشد:
src/
├── main.rs
├── models/
│ └── patient.rs
└── repositories/
└── patient_repository.rs
در فایل مدل، ساختارهای Patient و NewPatient تعریف میشوند. در فایل Repository، توابعی مانند create_patient، get_patient، get_patients، update_patient و delete_patient قرار میگیرند. فایل اصلی برنامه فقط مسئول راهاندازی و هماهنگکردن بخشهای مختلف خواهد بود.
Repository چه مشکلی را حل میکند؟
فرض کنید بعداً تصمیم بگیرید فهرست بیماران را بر اساس شماره ملی جستوجو کنید، صفحهبندی اضافه کنید یا فقط بیماران فعال را نمایش دهید. اگر تمام کوئریها در اجزای رابط کاربری پخش شده باشند، باید چندین محل را تغییر دهید و احتمال ناسازگاری بالا میرود. اما با Repository میتوانید منطق خواندن و تغییر دادهها را در یک محل مدیریت کنید.
البته Repository نباید به یک کلاس یا ماژول بسیار بزرگ تبدیل شود که تمام عملیات تمام جدولها را انجام میدهد. برای یک برنامهی کلینیک، جدا کردن PatientRepository، InsuranceRepository و AppointmentRepository میتواند ساختار معقولی باشد. هر Repository باید مسئول دادههای مرتبط با حوزهی خودش باشد و برای عملیات بین چند جدول، از کوئریهای مشخص یا لایهی سرویس استفاده شود.
ارتباط CRUD با Tauri و SQLx
اگر برنامهی شما با Tauri، React و SQLx ساخته شده است، بهتر است Repository را از فرمانهای Tauri جدا نگه دارید. Repository عملیات پایگاه داده را اجرا میکند؛ فرمان Tauri ورودی را دریافت و اعتبارسنجی میکند و نتیجه را به رابط کاربری برمیگرداند. React نیز وضعیت فرم، پیامهای خطا و نمایش فهرست را مدیریت میکند.
در SQLx میتوانید از query، query_as و متدهایی مانند fetch_optional و fetch_all استفاده کنید. این APIها برای اجرای کوئری و تبدیل نتیجه به ساختارهای Rust طراحی شدهاند.
برای مثال، دریافت یک بیمار در SQLx میتواند چنین الگویی داشته باشد:
let patient = sqlx::query_as::<_, Patient>(
"SELECT id, first_name, last_name, national_code
FROM patients
WHERE id = ?"
)
.bind(id)
.fetch_optional(pool)
.await?;
این نمونه به شرط آنکه Patient با FromRow سازگار شده باشد و pool از نوع مناسب SQLx باشد، الگوی دریافت صفر یا یک رکورد را نشان میدهد. در SQLite با SQLx، پارامتر موقعیتی ? معتبر است؛ در پروژهی واقعی باید نسخهی SQLx و قابلیتهای فعالشده در Cargo.toml را نیز با کد خود هماهنگ کنید.
یک اشتباه رایج: نباید کد rusqlite و SQLx را بدون تغییر در کنار هم استفاده کنید. rusqlite عموماً API همگام دارد، اما SQLx برای اجرای ناهمگام طراحی شده است. اگر برنامهی Tauri شما از SQLx استفاده میکند، الگوی Repository و مدیریت خطا را حفظ کنید، اما توابع را با API ناهمگام SQLx بنویسید.
۱۰. آزمایش CRUD و بررسی مشکلات رایج
بعد از پیادهسازی عملیات اصلی، باید رفتار آنها را در شرایط موفق و ناموفق بررسی کنید. صرفاً اجرای یک دستور INSERT و مشاهدهی یک ردیف جدید برای اطمینان از صحت CRUD کافی نیست. بهتر است سناریوهای ثبت موفق، شماره ملی تکراری، خواندن شناسهی ناموجود، بهروزرسانی بیمار موجود و حذف بیمار ناموجود را نیز آزمایش کنید.
برای مثال، هنگام آزمودن get_patient باید انتظار داشته باشید که شناسهی موجود یک Some(Patient) و شناسهی ناموجود یک None برگرداند. هنگام آزمایش create_patient نیز باید بررسی کنید که ثبت شماره ملی تکراری با خطا مواجه شود. این آزمونها کمک میکنند تفاوت میان «پیدا نشدن رکورد» و «خطای اجرای کوئری» حفظ شود.
جدول زیر چند مشکل متداول را خلاصه میکند:
|
مشکل |
علت احتمالی |
راهکار |
|---|---|---|
|
ثبت اطلاعات شکست میخورد |
نقض |
اعتبارسنجی ورودی و مدیریت خطا |
|
خروجی خواندن خالی است |
شناسه موجود نیست یا فیلتر اشتباه است |
بررسی شرط |
|
همهی رکوردها تغییر میکنند |
فراموش شدن |
بررسی شرط پیش از اجرای |
|
SQL Injection |
چسباندن ورودی به رشتهی SQL |
استفاده از پارامترهای متصلشده |
|
خطای تبدیل نوع داده |
ناسازگاری مدل و ستون |
تطبیق نوعهای Rust و SQLite |
|
اطلاعات ناقص ثبت میشوند |
چند عملیات بدون تراکنش |
استفاده از تراکنش برای عملیات مرتبط |
برای پروژههای بزرگتر، آزمونها را با پایگاه دادهی موقت یا درونحافظهای اجرا کنید تا هر آزمون مستقل از دادههای باقیمانده از اجرای قبلی باشد. همچنین آزمونهای یکپارچهسازی میتوانند کل مسیر را از ورودی تابع تا تغییر واقعی پایگاه داده بررسی کنند. اگر از فایل پایگاه دادهی مشترک استفاده میکنید، مراقب باشید آزمونها روی اطلاعات واقعی یا دادههای توسعهی مهم اجرا نشوند.
در نهایت، قبل از انتشار نرمافزار، باید رفتار برنامه هنگام قطع دسترسی به فایل، قفل شدن پایگاه داده، خطای دیسک و شکست عملیات تراکنش نیز بررسی شود. CRUD زمانی قابل اتکا است که فقط در شرایط ایدهآل کار نکند، بلکه خطاهای قابل پیشبینی را نیز بهشکل کنترلشده مدیریت کند.