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

آموزش SQLite در Rust – پیاده‌سازی کامل عملیات CRUD

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

Share
Pin
Like
Send
Share
Send
Send
Share

۱. آشنایی با CRUD و نقش آن در برنامه

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

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

چهار عملیات اصلی پایگاه داده

عملیات

دستور SQL

کاربرد

Create

INSERT

افزودن رکورد جدید

Read

SELECT

خواندن اطلاعات

Update

UPDATE

تغییر اطلاعات موجود

Delete

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 نیز باید بررسی کنید که ثبت شماره ملی تکراری با خطا مواجه شود. این آزمون‌ها کمک می‌کنند تفاوت میان «پیدا نشدن رکورد» و «خطای اجرای کوئری» حفظ شود.

جدول زیر چند مشکل متداول را خلاصه می‌کند:

مشکل

علت احتمالی

راهکار

ثبت اطلاعات شکست می‌خورد

نقض UNIQUE یا NOT NULL

اعتبارسنجی ورودی و مدیریت خطا

خروجی خواندن خالی است

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

بررسی شرط WHERE

همه‌ی رکوردها تغییر می‌کنند

فراموش شدن WHERE

بررسی شرط پیش از اجرای UPDATE

SQL Injection

چسباندن ورودی به رشته‌ی SQL

استفاده از پارامترهای متصل‌شده

خطای تبدیل نوع داده

ناسازگاری مدل و ستون

تطبیق نوع‌های Rust و SQLite

اطلاعات ناقص ثبت می‌شوند

چند عملیات بدون تراکنش

استفاده از تراکنش برای عملیات مرتبط

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

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

 

بهینه‌سازی عملیات CRUD در SQLite با Rust؛ از کوئری امن تا مدیریت خطا

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

 چرا امنیت کوئری‌ها در عملیات CRUD اهمیت دارد؟

استفاده از پارامترهای SQL به‌جای اتصال رشته‌ها

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

در کتابخانه rusqlite می‌توان از ماکروی params! برای ارسال پارامترها استفاده کرد. برای نمونه، هنگام ثبت بیمار جدید، نام و نام خانوادگی باید به‌صورت پارامترهای جداگانه به دستور INSERT ارسال شوند. این روش باعث می‌شود رشته‌ای مانند نامی که شامل علامت نقل‌قول است، به‌عنوان داده پردازش شود، نه بخشی از دستور SQL. همین الگو باید در عملیات SELECT، UPDATE و DELETE نیز رعایت شود.

مثال کاربردی

use rusqlite::{params, Connection, Result};

fn find_patient_by_code(
    conn: &Connection,
    national_code: &str,
) -> Result<bool> {
    let count: i64 = conn.query_row(
        "SELECT COUNT(*)
         FROM patients
         WHERE national_code = ?1",
        params![national_code],
        |row| row.get(0),
    )?;

    Ok(count > 0)
}

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

اعتبارسنجی داده‌ها قبل از ذخیره‌سازی

تفاوت اعتبارسنجی برنامه با محدودیت پایگاه داده

اعتبارسنجی در لایه برنامه و محدودیت‌های SQLite دو وظیفه متفاوت دارند. اعتبارسنجی برنامه برای ارائه پیام قابل فهم به کاربر مناسب است، در حالی که محدودیت‌هایی مانند NOT NULL و UNIQUE از یکپارچگی داده‌ها در سطح پایگاه داده محافظت می‌کنند. برای مثال، فرم ثبت بیمار می‌تواند خالی بودن نام یا نامعتبر بودن قالب شماره ملی را پیش از ارسال درخواست بررسی کند. اما پایگاه داده نیز باید از ثبت مقادیر تکراری جلوگیری کند تا خطاهای ناشی از درخواست‌های هم‌زمان کنترل شوند.

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

نمونه‌ای از اعتبارسنجی اولیه در Rust

fn validate_patient(
    first_name: &str,
    last_name: &str,
    national_code: &str,
) -> Result<(), String> {
    if first_name.trim().is_empty() {
        return Err("نام بیمار الزامی است".to_string());
    }

    if last_name.trim().is_empty() {
        return Err("نام خانوادگی الزامی است".to_string());
    }

    if national_code.len() != 10
        || !national_code.bytes().all(|b| b.is_ascii_digit())
    {
        return Err("قالب شماره ملی معتبر نیست".to_string());
    }

    Ok(())
}

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

 مدیریت خطاها در عملیات CRUD

چرا استفاده از unwrap انتخاب مناسبی نیست؟

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

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

طراحی تابع با خروجی مشخص

هنگام خواندن یک بیمار، استفاده از Option<Patient> به‌جای تلقی نشدن رکورد به‌عنوان خطا، طراحی روشن‌تری ایجاد می‌کند. اگر رکورد وجود نداشته باشد، تابع می‌تواند Ok(None) برگرداند؛ اگر اجرای کوئری واقعاً شکست بخورد، نتیجه Err(...) خواهد بود. این تفاوت باعث می‌شود رابط کاربری بتواند پیام «بیماری با این شناسه پیدا نشد» را از پیام «خواندن اطلاعات با خطا مواجه شد» جدا کند. در نتیجه، رفتار برنامه در سناریوهای مختلف قابل پیش‌بینی‌تر می‌شود.

جلوگیری از به‌روزرسانی ناخواسته اطلاعات

اهمیت شرط WHERE

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

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

 انتخاب میان حذف فیزیکی و حذف نرم

حذف نرم برای سیستم‌های دارای سوابق

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

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

 استفاده از تراکنش برای عملیات مرتبط

حفظ یکپارچگی اطلاعات

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

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

 اصول آزمایش عملیات CRUD

آزمایش شرایط موفق و ناموفق

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

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

 

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

نتیجه‌گیری

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

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

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

کلیدواژه ها

SQLite در Rust, آموزش SQLite, پیاده سازی CRUD در Rust, CRUD با Rust, rusqlite, پایگاه داده SQLite, زبان برنامه نویسی Rust, دستورات SQL در Rust, عملیات INSERT و SELECT, عملیات UPDATE و DELETE, مدیریت خطا در Rust, تراکنش SQLite, Repository Pattern در Rust, آموزش Tauri و SQLite

پرسش و پاسخ

1 . CRUD در Rust چیست؟
CRUD مجموعه‌ی چهار عملیات ایجاد، خواندن، به‌روزرسانی و حذف اطلاعات است. در SQLite این عملیات معمولاً با دستورات INSERT، SELECT، UPDATE و DELETE انجام می‌شوند.

2 . برای اتصال Rust به SQLite از rusqlite استفاده کنیم یا SQLx؟
اگر به دسترسی ساده و همگام به SQLite نیاز دارید، rusqlite انتخاب مناسبی است. اگر برنامه‌ی شما بر پایه‌ی اجرای ناهمگام، connection pool و معماری SQLx ساخته شده است، استفاده از SQLx معمولاً سازگاری بیشتری با ساختار موجود خواهد داشت.

3 . چگونه از SQL Injection در SQLite جلوگیری کنیم؟
ورودی کاربر را با اتصال رشته‌ای وارد دستور SQL نکنید. از پارامترهای متصل‌شده مانند params! در rusqlite یا متد .bind() در SQLx استفاده کنید. برای نام جدول، نام ستون و جهت مرتب‌سازی نیز باید از مقادیر مجاز و از پیش تعریف‌شده استفاده شود.

4 . آیا برای هر عملیات CRUD به تراکنش نیاز داریم؟
خیر. برای عملیات مستقل و ساده معمولاً نیازی به تعریف صریح تراکنش نیست. اما وقتی چند تغییر باید به‌صورت یک واحد منطقی انجام شوند، تراکنش کمک می‌کند از ثبت ناقص داده‌ها جلوگیری کنید.

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

6 . آیا Repository برای تمام پروژه‌های Rust ضروری است؟
خیر. در پروژه‌های کوچک ممکن است چند تابع مستقل کافی باشند؛ اما با افزایش پیچیدگی، Repository به سازمان‌دهی و نگهداری کد کمک می‌کند.

7 . تفاوت Repository و Service چیست؟
Repository عملیات دسترسی به داده را انجام می‌دهد، در حالی که Service قواعد کسب‌وکار و هماهنگی میان عملیات مختلف را مدیریت می‌کند.

8 . آیا Repository باید اتصال SQLite را خودش ایجاد کند؟
الزامی نیست. معمولاً بهتر است مدیریت اتصال در لایه‌ای جداگانه انجام شود و Repository اتصال آماده را دریافت کند.

9 . آیا می‌توان از Repository با SQLx استفاده کرد؟
بله. مفهوم Repository مستقل از کتابخانه است، اما پیاده‌سازی باید با API ناهمگام SQLx و نوع اتصال یا pool آن سازگار باشد.

10 . آیا ایجاد ایندکس همیشه سرعت SQLite را افزایش می‌دهد؟
خیر. ایندکس می‌تواند خواندن برخی داده‌ها را سریع‌تر کند، اما فضای بیشتری مصرف می‌کند و هزینه عملیات نوشتن را افزایش می‌دهد.

11 . آیا WAL مشکل قفل SQLite را کاملاً حل می‌کند؟
خیر. WAL می‌تواند هم‌زمانی برخی خواندن‌ها و نوشتن‌ها را بهتر کند، اما محدودیت‌های نوشتن هم‌زمان همچنان وجود دارند.

12 . آیا استفاده از SELECT * اشتباه است؟
برای نمونه‌های ساده قابل استفاده است، اما در کوئری‌های عملیاتی بهتر است ستون‌های موردنیاز را صریح مشخص کنید.

13 . آیا تراکنش باعث سریع‌تر شدن درج گروهی می‌شود؟
در بسیاری از سناریوها گروه‌بندی درج‌ها در یک تراکنش سربار را کاهش می‌دهد و هم‌زمان یکپارچگی داده‌ها را حفظ می‌کند.

14 . ۵. چگونه بفهمیم یک کوئری به ایندکس نیاز دارد؟
از EXPLAIN QUERY PLAN برای بررسی روش اجرای کوئری استفاده کنید و نتیجه را با اندازه‌گیری روی داده‌های نماینده ارزیابی کنید.

دیدگاه ها

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