اذهب إلى المحتوى

السؤال

نشر

السلام عليكم
لدي تكست بوكس أعرض فيه رقم فاتورة وهو اخر رقم في جدول استخدم دالة تالية
public static int CODE_GENE_SAFE(string TABLE_NAME, string COLUMN_NAME)
{
    int CODE = 0;
    using (SqlConnection con = new SqlConnection(sqlCon.ConnectionString))
    {
        con.Open();
        using (SqlCommand cmd = new SqlCommand(
            $"SELECT ISNULL(MAX([{COLUMN_NAME}]), 0) + 1 FROM [{TABLE_NAME}]", con))
        {
            CODE = Convert.ToInt32(cmd.ExecuteScalar());
        }
    }
    return CODE;
}
لكن المستخدم (A) يضغط جديد → يحصل على الرقم 100.
في نفس اللحظة المستخدم (B) يضغط جديد → يحصل أيضًا على الرقم 100.
المستخدم (A) يحفظ أولًا → يتم حفظ الفاتورة رقم 100.
المستخدم (B) يحفظ بعده → يحدث خطأ لأن الرقم 100 أصبح موجودًا بالفعل.
هل توجد طريقة اخري افضل
txtInvoiceNo.Text = CODE_GENE_SAFE("Invoices", "InvoiceID").ToString();

Recommended Posts

  • 1
نشر

وعليكم السلام ورحمة الله وبركاته.

الحل الأفضل في مشكلتك الحاليه هي إنشاء جدول عدادات (Counter Table) بمعنى بدل أن تأخذ الرقم من البيانات، اجعله في صف واحد بجدول مستقل، يمثل "آخر رقم صادر حتى الآن". وعندما يحتاج أي مستخدم رقمًا جديدً تطلب منه أمرًا واحدًا مدمجًا: زِد هذه القيمة بمقدار واحد، وأعطني القيمة الجديدة فورًا". القراءة والتعديل هنا ليسا خطوتين منفصلتين، بل عملية واحدة.

وبالتالي تضمن أن أي تعديل على صف واحد من جلستين مختلفتين في نفس اللحظة لا يمكن أن يحدث في آنٍ واحد الثانية تجبر على الانتظار حتى تكتمل الأولى تمامًا، ثم تنفذ على القيمة المحدثة فعليًا وليس على القيمة القديمة. 

  • 0
نشر

وعليكم السلام.

رايي هو ان تجعل التوليد لرقم الفاتورة من طرف قاعدة البيانات مع زيادة تلقائية لتجنب المشاكل. مع انه توجد احتمالية لظهور فجوات، الا ان هذا لن يؤثر غالبا. اذا كنت لا تريد الفجوات نهائيا، استخدم حل الاخ عبدالباسط مع وضع التعديل والإدراج داخل نفس العملية الموحدة (Transaction) لتضمن ان تكون الارقام متتالية تماما.

using (SqlConnection con = new SqlConnection(connectionString))
{
    con.Open();
    SqlTransaction transaction = con.BeginTransaction(); // بدء المعاملة

    try
    {
        // 1. زيادة العداد وجلب الرقم الجديد داخل نفس المعاملة
        string updateCounterSql = @"UPDATE SystemCounters 
                                    SET LastValue = LastValue + 1 
                                    OUTPUT INSERTED.LastValue 
                                    WHERE TableName = 'Invoices'";
        
        SqlCommand cmdCounter = new SqlCommand(updateCounterSql, con, transaction);
        int newInvoiceId = Convert.ToInt32(cmdCounter.ExecuteScalar());

        // 2. إدراج الفاتورة باستخدام الرقم الجديد
        string insertInvoiceSql = @"INSERT INTO Invoices (InvoiceID, CustomerName, Total) 
                                    VALUES (@InvoiceID, @CustomerName, @Total)";
        
        SqlCommand cmdInvoice = new SqlCommand(insertInvoiceSql, con, transaction);
        cmdInvoice.Parameters.AddWithValue("@InvoiceID", newInvoiceId);
        cmdInvoice.Parameters.AddWithValue("@CustomerName", "أحمد");
        cmdInvoice.Parameters.AddWithValue("@Total", 1500);
        
        cmdInvoice.ExecuteNonQuery();

        // 3. تأكيد العملية بالكامل فقط إذا نجح كل شيء
        transaction.Commit(); 
    }
    catch
    {
        // إذا حدث أي خطأ في أي خطوة، يتم التراجع عن زيادة العداد وعن إدراج الفاتورة
        transaction.Rollback(); 
        throw;
    }
}

 

  • 0
نشر

وعليكم السلام ورحمة الله،

المشكلة أن الدالة تفصل بين عملية قراءة آخر رقم (SELECT MAX) وعملية حفظ الفاتورة في خطوتين منفصلتين بينهما فارق زمني، فحين يضغط مستخدمان على "جديد" في نفس اللحظة تقريبا ينفذ كلاهما استعلام القراءة قبل أن يقوم أي منهما بحفظ فاتورته، فيحصل كلاهما على نفس القيمة (100) لأن قاعدة البيانات في تلك اللحظة لا تزال لا تحتوي على أي فاتورة بهذا الرقم بالنسبة لأي منهما.

وحين يحفظ الأول تصبح القيمة 100 محجوزة فعليا لكن الثاني يحاول الحفظ بنفس الرقم القديم الذي قرأه مسبقا فيفشل.

وهذا النوع من الأخطاء يعرف بال Race Condition وجذره أن العملية غير ذرية (Not Atomic)، أي أن القراءة والكتابة ليستا مضمونتين كوحدة واحدة غير قابلة للمقاطعة فيسمح النظام لأكثر من جلسة بالتعامل مع نفس البيانات القديمة في آن واحد دون قفل أو تزامن يمنع هذا التداخل.

  • 0
نشر
بتاريخ 19 ساعة قال عبد الوهاب بومعراف:

وعليكم السلام ورحمة الله،

المشكلة أن الدالة تفصل بين عملية قراءة آخر رقم (SELECT MAX) وعملية حفظ الفاتورة في خطوتين منفصلتين بينهما فارق زمني، فحين يضغط مستخدمان على "جديد" في نفس اللحظة تقريبا ينفذ كلاهما استعلام القراءة قبل أن يقوم أي منهما بحفظ فاتورته، فيحصل كلاهما على نفس القيمة (100) لأن قاعدة البيانات في تلك اللحظة لا تزال لا تحتوي على أي فاتورة بهذا الرقم بالنسبة لأي منهما.

وحين يحفظ الأول تصبح القيمة 100 محجوزة فعليا لكن الثاني يحاول الحفظ بنفس الرقم القديم الذي قرأه مسبقا فيفشل.

وهذا النوع من الأخطاء يعرف بال Race Condition وجذره أن العملية غير ذرية (Not Atomic)، أي أن القراءة والكتابة ليستا مضمونتين كوحدة واحدة غير قابلة للمقاطعة فيسمح النظام لأكثر من جلسة بالتعامل مع نفس البيانات القديمة في آن واحد دون قفل أو تزامن يمنع هذا التداخل.

حل اخي عبد الوهاب ماهو

  • 0
نشر

المشكلة ليست في الدالة نفسها فقط، بل في توقيت توليد الرقم، أنت تولد الرقم هكذا:

SELECT ISNULL(MAX(InvoiceID), 0) + 1

ثم تعرضه للمستخدم في TextBox، وبعد ذلك قد يحفظ المستخدم بعد ثوانٍ أو دقائق، خلال تلك المدة مستخدم آخر قد يقرأ نفس القيمة، فيحصل الاثنان على نفس الرقم.

وذلك هو الـ Race Condition، الأفضل أن تجعل قاعدة البيانات هي المسؤولة عن توليد الرقم أثناء الحفظ نفسه، وليس قبل الحفظ، أي لو رقم الفاتورة هو مجرد رقم تسلسلي داخلي، فاستخدم عمود IDENTITY.

CREATE TABLE Invoices
(
    InvoiceID INT IDENTITY(1,1) PRIMARY KEY,
    InvoiceDate DATE NOT NULL,
    CustomerID INT NOT NULL,
    TotalAmount DECIMAL(18,2) NOT NULL
);

عند الحفظ، أدخل البيانات ثم أرجع الرقم الذي تم توليده تلقائيًا:

public static int InsertInvoice(DateTime invoiceDate, int customerId, decimal totalAmount)
{
    const string sql = @"
        INSERT INTO Invoices
        (
            InvoiceDate,
            CustomerID,
            TotalAmount
        )
        OUTPUT INSERTED.InvoiceID
        VALUES
        (
            @InvoiceDate,
            @CustomerID,
            @TotalAmount
        );";

    using (SqlConnection con = new SqlConnection(sqlCon.ConnectionString))
    using (SqlCommand cmd = new SqlCommand(sql, con))
    {
        cmd.Parameters.Add("@InvoiceDate", SqlDbType.Date).Value = invoiceDate;
        cmd.Parameters.Add("@CustomerID", SqlDbType.Int).Value = customerId;
        cmd.Parameters.Add("@TotalAmount", SqlDbType.Decimal).Value = totalAmount;

        con.Open();

        int invoiceId = Convert.ToInt32(cmd.ExecuteScalar());

        return invoiceId;
    }
}

ثم بعد الحفظ:

int invoiceId = InsertInvoice(DateTime.Today, customerId, total);

txtInvoiceNo.Text = invoiceId.ToString();

وعند الضغط على زر جديد، لا تعرض رقمًا محجوزًا، اكتب مثلاً:

txtInvoiceNo.Text = "تلقائي";

ثم بعد الحفظ الفعلي:

txtInvoiceNo.Text = invoiceId.ToString();

لأن عرض الرقم قبل الحفظ هو الذي يسبب المشكلة.

  • 0
نشر

وعليكم السلام ورحمة الله تعالى وبركاته،

المشكلة الحقيقية ليست في صياغة استعلام SQL نفسه بل في أن عملية "قراءة آخر رقم" و"حفظ الفاتورة" منفصلتان زمنيا فتنفيذ SELECT MAX(...) + 1 يحدث في لحظة وحفظ الفاتورة الفعلي يحدث في لحظة لاحقة قد تفصلها ثوان وربما دقائق كأن تحدث ريثما يكمل المستخدم باقي بيانات الفاتورة وخلال هذه الفجوة الزمنية يمكن لأي مستخدم آخر أن يقرأ نفس القيمة القديمة من قاعدة البيانات لأنها لم تحجز أو تقفل بعد فيتصادم الاثنان على نفس الرقم عند الحفظ وهذا النمط من الأخطاء معروف باسم Race Condition.

لحل المشكلة يمكننا الاعتماد على قيد فريد (Unique Constraint) مع إعادة المحاولة عند التعارض (Retry on Conflict) بدلا من تحويل التوليد إلى IDENTITY أو بناء جدول عداد منفصل أين يمكنك ترك منطق التوليد كما هو تقريبا لكن تجعل قاعدة البيانات نفسها كالحكم الذي يرفض التكرار، ثم تتعامل مع هذا الرفض برمجيا بإعادة المحاولة كالتالي:

  1. أضف قيدا فريدا (UNIQUE constraint) على عمود InvoiceID إن لم يكن موجودا أصلا كمفتاح أساسي.
  2. عند الحفظ حاول تنفيذ عملية الإدراج مباشرة بالرقم المتولد.

وإن فشلت العملية بسبب تعارض في القيد الفريد (duplicate key) أعد حساب الرقم من جديد (SELECT MAX أو أي منطق آخر) وحاول الإدراج مرة أخرى ضمن حلقة محاولات محدودة هكذا:

public static int InsertInvoiceWithRetry(string tableName, string columnName, /* باقي بيانات الفاتورة */)
{
    int maxRetries = 5;
    for (int attempt = 0; attempt < maxRetries; attempt++)
    {
        int newCode = CODE_GENE_SAFE(tableName, columnName);

        try
        {
            using (SqlConnection con = new SqlConnection(sqlCon.ConnectionString))
            using (SqlCommand cmd = new SqlCommand(
                $"INSERT INTO [{tableName}] ([{columnName}], /* أعمدة أخرى */) VALUES (@Code, /* قيم أخرى */)", con))
            {
                cmd.Parameters.AddWithValue("@Code", newCode);
                // إضافة باقي المعاملات هنا
                con.Open();
                cmd.ExecuteNonQuery();
                return newCode; // نجح الحفظ، أعد الرقم الفعلي
            }
        }
        catch (SqlException ex) when (ex.Number == 2627 || ex.Number == 2601) // خطأ تكرار مفتاح/قيد فريد
        {
            // تجاهل الخطأ وأعد المحاولة برقم جديد
            continue;
        }
    }
    throw new Exception("فشل توليد رقم فاتورة فريد بعد عدة محاولات.");
}

وعند العرض في الواجهة لا تعرض الرقم النهائي إلا بعد نجاح الحفظ الفعلي بحيث يكون TextBox في وضع "تلقائي" أو فارغ قبل الضغط على حفظ.

انضم إلى النقاش

يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.

زائر
أجب على هذا السؤال...

×   لقد أضفت محتوى بخط أو تنسيق مختلف.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   جرى استعادة المحتوى السابق..   امسح المحرر

×   You cannot paste images directly. Upload or insert images from URL.

  • إعلانات

  • تابعنا على



×
×
  • أضف...