JDBC وقواعد البيانات

المعاملات (Transactions)

15 دقيقة الدرس 7 من 13

المعاملات (Transactions)

المعاملة (Transaction) هي وحدة عمل يجب أن تنجح بالكامل أو تُترك قاعدة البيانات دون أي تغيير. تمنحك JDBC تحكّمًا دقيقًا في المعاملات من خلال ثلاثة مفاهيم مترابطة: علامة autoCommit، واستدعاءات commit() وrollback() الصريحة، ومستويات العزل التي تحدّد ما تستطيع المعاملات المتزامنة رؤيته من عمل بعضها البعض.

لماذا تهمّ المعاملات؟

تخيّل تحويل أموال: تخصم من حساب A وتُضيف إلى حساب B. إذا تعطّل التطبيق بين العبارتين، تحتاج قاعدة البيانات إلى التراجع عن عملية الخصم تلقائيًا. توفّر المعاملات ضمانات ACID الأربع التي تجعل ذلك آمنًا:

  • الذرية (Atomicity) — تُنفَّذ كل العبارات معًا أو لا تُنفَّذ أي منها.
  • الاتساق (Consistency) — تنتقل قاعدة البيانات من حالة صالحة إلى أخرى.
  • العزل (Isolation) — لا تتداخل المعاملات المتزامنة مع بعضها (بدرجة قابلة للتهيئة).
  • الديمومة (Durability) — بعد الإيداع (commit)، تبقى البيانات محفوظة حتى في حالة الأعطال.

autoCommit: السلوك الافتراضي

تضبط JDBC افتراضيًا autoCommit = true على كل اتصال جديد. كل عبارة SQL تُنفَّذ تُودَع فورًا في قاعدة البيانات — دون أي فرصة للتراجع عنها.

لكي تتحكّم في المعاملات بنفسك، يجب تعطيل هذا الإعداد قبل إصدار أي عبارة DML:

Connection conn = DriverManager.getConnection(url, user, pass); conn.setAutoCommit(false); // تعطيل الإيداع التلقائي — الآن نتحكّم نحن في متى تُحفظ البيانات
نسيت إيقاف autoCommit؟ لحظة تنفيذ conn.executeUpdate(...) مع تفعيل autoCommit، يصبح التغيير دائمًا لا رجعة فيه. لا يوجد شيء للتراجع عنه. اضبط دائمًا autoCommit(false) في بداية أي عملية متعددة العبارات تريدها ذرية.

commit() و rollback()

بمجرد إيقاف autoCommit، تُخبر قاعدة البيانات بجعل كل التغييرات المعلّقة دائمة باستخدام conn.commit()، أو بتجاهلها باستخدام conn.rollback().

النمط الاصطلاحي يُغلّف العملية في كتلة try/catch/finally:

String debit = "UPDATE accounts SET balance = balance - ? WHERE id = ?"; String credit = "UPDATE accounts SET balance = balance + ? WHERE id = ?"; Connection conn = null; try { conn = DriverManager.getConnection(url, user, pass); conn.setAutoCommit(false); try (PreparedStatement ps1 = conn.prepareStatement(debit); PreparedStatement ps2 = conn.prepareStatement(credit)) { ps1.setBigDecimal(1, amount); ps1.setLong(2, fromAccountId); ps1.executeUpdate(); ps2.setBigDecimal(1, amount); ps2.setLong(2, toAccountId); ps2.executeUpdate(); } conn.commit(); // يُحفظ كلا الصفّين معًا بشكل ذري System.out.println("Transfer complete."); } catch (SQLException e) { if (conn != null) { try { conn.rollback(); // لا يُحفظ أيٌّ من الصفّين System.err.println("Transfer rolled back: " + e.getMessage()); } catch (SQLException rbEx) { System.err.println("Rollback failed: " + rbEx.getMessage()); } } } finally { if (conn != null) { try { conn.setAutoCommit(true); // استعادة الوضع الافتراضي لإعادة استخدام مجموعة الاتصالات conn.close(); } catch (SQLException ignored) {} } }
استعد autoCommit قبل إعادة الاتصال إلى المجموعة. إذا استعرت اتصالًا من مجموعة (HikariCP، c3p0 إلخ) وتركته في وضع الإيداع اليدوي، سيجد المستعير التالي نفسه في معاملة لم يبدأها قط. أعد الضبط دائمًا إلى true — أو دع إعداد connectionInitSql في المجموعة يفعل ذلك نيابة عنك.

نقاط الحفظ (Savepoints)

تتيح نقطة الحفظ التراجع إلى نقطة محددة داخل المعاملة دون التخلّي عن كل شيء. هذا مفيد حين تمرّ عملية كبيرة بعدة مراحل وتفشل المرحلة الأخيرة فقط.

conn.setAutoCommit(false); // المرحلة الأولى — إدراج صف الرأس insertHeader(conn, orderId); Savepoint afterHeader = conn.setSavepoint("after_header"); // وضع علامة على هذه النقطة try { // المرحلة الثانية — إدراج بنود الطلب (قد تفشل بسبب فحص المخزون) insertLineItems(conn, orderId, items); conn.commit(); } catch (SQLException e) { conn.rollback(afterHeader); // تراجع عن المرحلة الثانية فقط؛ صف الرأس لا يزال معلّقًا // معالجة الفشل الجزئي — ربما إدراج صف "في انتظار التوفّر" conn.commit(); }
لا تدعم جميع قواعد البيانات نقاط الحفظ (MySQL InnoDB تدعمها؛ بعض المحركات الخفيفة لا تدعمها). تحقّق من DatabaseMetaData.supportsSavepoints() عند بدء التشغيل إذا كانت قابلية النقل مهمة.

مستويات العزل (Isolation Levels)

حين تعمل معاملات متعددة في وقت واحد يمكن أن تتداخل. يحدّد معيار SQL أربعة شذوذات تزامن وأربعة مستويات عزل تمنعها تدريجيًا:

  • القراءة القذرة (Dirty Read) — قراءة بيانات كتبتها معاملة غير مُودَعة قد تتراجع لاحقًا.
  • القراءة غير القابلة للتكرار (Non-Repeatable Read) — صف قرأته مرة يحمل قيمًا مختلفة عند قراءته مجددًا (معاملة أخرى أودعت تحديثًا في الأثناء).
  • القراءة الشبحية (Phantom Read) — استعلام نطاق يُعيد صفوفًا مختلفة عند إعادة تشغيله (معاملة أخرى أدرجت صفوفًا أو حذفتها).
  • التحديث الضائع (Lost Update) — معاملتان متزامنتان تقرآن ثم تكتبان على نفس الصف؛ تُضيَّع إحدى الكتابتين بصمت.

تعرض JDBC أربعة مستويات عزل كثوابت على Connection:

  • TRANSACTION_READ_UNCOMMITTED — يسمح بالقراءة القذرة. نادر الاستخدام؛ أعلى إنتاجية.
  • TRANSACTION_READ_COMMITTED — يمنع القراءة القذرة. الافتراضي في معظم قواعد البيانات (PostgreSQL، SQL Server، Oracle). القراءة غير القابلة للتكرار والأشباح لا تزال ممكنة.
  • TRANSACTION_REPEATABLE_READ — يمنع القراءة القذرة والقراءة غير القابلة للتكرار. افتراضي MySQL InnoDB. الأشباح لا تزال ممكنة في بعض المحركات.
  • TRANSACTION_SERIALIZABLE — يمنع جميع الشذوذات. تتصرّف القراءات كما لو أنّ المعاملات نُفِّذت واحدة تلو الأخرى. أعلى صواب، أدنى إنتاجية.

ضبط مستوى العزل قبل بدء العمل:

conn.setAutoCommit(false); conn.setTransactionIsolation(Connection.TRANSACTION_REPEATABLE_READ); // الآن تعرض القراءات داخل هذه المعاملة لقطة متسقة ResultSet rs = stmt.executeQuery("SELECT balance FROM accounts WHERE id = 42"); rs.next(); BigDecimal balance = rs.getBigDecimal("balance"); // ... عمل آخر ... // قراءة نفس الصف مجددًا ستُعيد نفس الرصيد // حتى لو أودعت معاملة أخرى تحديثًا في الأثناء rs = stmt.executeQuery("SELECT balance FROM accounts WHERE id = 42"); rs.next(); BigDecimal sameBalance = rs.getBigDecimal("balance"); // مضمون التساوي في REPEATABLE_READ conn.commit();
العزل الأعلى يعني عبء قفل أكبر. يمكن أن يُسبّب SERIALIZABLE تنافسًا شديدًا على الأقفال وحالات توقّف (deadlocks) في ظل التزامن العالي. اجعل READ_COMMITTED افتراضك وارفع المستوى فقط للعمليات التي يُسبّب فيها الشذوذ خللًا فعليًا (إعادة الحسابات المالية، فحوصات المخزون إلخ).

اختيار المستوى المناسب عمليًا

قاعدة بديهية واقعية:

  • READ_COMMITTED — معظم عمليات CRUD، استعلامات التقارير، معالجات واجهة برمجة الويب.
  • REPEATABLE_READ — العمليات التي تقرأ قيمة ثم تستخدمها في كتابة (دورات القراءة-التعديل-الكتابة، مثل زيادة عداد).
  • SERIALIZABLE — التحويلات المالية، أنظمة الحجز حيث يجب أن يكون التخصيص المزدوج مستحيلًا؛ استخدمها فقط في المعاملة المحددة التي تحتاجها، لا عالميًا.

الخلاصة

تتمحور المعاملات في JDBC حول ثلاثة عناصر تحكّم: setAutoCommit(false) للتحكّم اليدوي، وcommit() / rollback() لإنهاء العمل أو التراجع عنه، وsetTransactionIsolation() للإعلان عن مدى رؤية المعاملات المتزامنة لعمل بعضها البعض. تضيف نقاط الحفظ نقاط تفتيش داخل المعاملة للسير المعقّدة. المهارة الهندسية الأساسية هي مطابقة مستوى العزل مع الشذوذ الذي تحتاج فعلًا إلى منعه، بدلًا من الاعتماد افتراضيًا على المستوى الأكثر تقييدًا في كل مكان.