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

معالجة ResultSets

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

معالجة ResultSets

الـ ResultSet هو الكائن الذي يُعيده JDBC بعد تنفيذ أي استعلام SELECT. فكّر فيه كجدول قابل للتصفّح: يحتوي على مؤشر يبدأ قبل الصف الأول، وتُحرّكه صفًّا بصف (أو تقفز مباشرة إلى موضع محدد إن كان برنامج التشغيل يدعم ذلك). فهم كيفية التنقل بين الصفوف واستخراج القيم بأنواعها وفحص مخطّط النتائج في وقت التشغيل يجعلك أكثر كفاءة مع JDBC ويُهيّئك للحالات الحدّية التي تطفو دائمًا في بيئة الإنتاج.

نموذج المؤشر

حين تُعيد executeQuery() النتيجة، يكون المؤشر قبل الصف الأول. يجب استدعاء next() للانتقال إلى الصف الأول. تُعيد next() القيمة true طالما وُجد صف، وfalse حين تنتهي البيانات — وهو ما يجعلها شرطًا طبيعيًا لحلقة while:

String sql = "SELECT id, username, email FROM users"; try (Statement stmt = conn.createStatement(); ResultSet rs = stmt.executeQuery(sql)) { while (rs.next()) { long id = rs.getLong("id"); String username = rs.getString("username"); String email = rs.getString("email"); System.out.printf("%-5d %-20s %s%n", id, username, email); } }
أغلق ResultSet و Statement دائمًا. اجعلهما داخل كتلة try-with-resources. الـ ResultSet المفتوح يُبقي مؤشرًا على الخادم مفتوحًا، ممّا يستهلك ذاكرة على جانبَي العميل وقاعدة البيانات.

توابع الاستخراج بالنوع

يوفّر JDBC نسختين من كل تابع استخراج: واحدة تأخذ اسم العمود (سلسلة String) وأخرى تأخذ رقم العمود (عدد صحيح يبدأ من 1). أسماء الأعمدة أكثر وضوحًا وتصمد في مواجهة تغيير ترتيب الأعمدة عند استخدام SELECT *؛ أما أرقام الأعمدة فهي أسرع قليلًا في الحلقات الحرجة أداءً.

أكثر توابع الاستخراج المكتوبة استخدامًا:

  • getString(col) — يتوافق مع VARCHAR وTEXT وCHAR
  • getLong(col) / getInt(col) — يتوافق مع BIGINT / INT
  • getDouble(col) / getBigDecimal(col) — للأعداد العشرية والأرقام الدقيقة
  • getBoolean(col) — يتوافق مع BOOLEAN / TINYINT(1)
  • getTimestamp(col) — يتوافق مع DATETIME / TIMESTAMP
  • getDate(col) — يتوافق مع DATE (الجزء الزمني يُضبط على منتصف الليل)
  • getBytes(col) — البيانات الثنائية (BLOB)
  • getObject(col) — نوع Java الذي يختاره برنامج التشغيل؛ مفيد حين يكون النوع مجهولًا وقت الترجمة
// أعمدة المال يجب أن تكون دائمًا BigDecimal وليس double BigDecimal price = rs.getBigDecimal("price"); // التواريخ — استخدم getTimestamp ثم حوّلها إلى java.time Timestamp ts = rs.getTimestamp("created_at"); LocalDateTime createdAt = ts != null ? ts.toLocalDateTime() : null;
استخدم BigDecimal للقيم المالية. يُدخل getDouble أخطاء تقريب في الفاصلة العائمة. للأعمدة المالية أو العلمية، استخدم getBigDecimal دائمًا.

التعامل مع NULL في SQL

القيمة NULL في SQL ليست مكافئة لـ null في Java. حين تستدعي تابع استخراج نوع بدائي مثل getLong() على عمود يحتوي NULL، يُعيد JDBC القيمة 0 — لا استثناء. للتمييز بين الصفر الحقيقي والقيمة الغائبة، استدعِ wasNull() مباشرة بعد الاستخراج:

long score = rs.getLong("score"); if (rs.wasNull()) { System.out.println("score is NULL"); } else { System.out.println("score = " + score); }

أما توابع استخراج الكائنات (getString، getBigDecimal، getTimestamp) فتُعيد null بالجافا مباشرةً، لذا لا تحتاج wasNull() إلا مع توابع الأنواع البدائية.

لا تستدعِ wasNull() قبل استدعاء تابع الاستخراج. تعكس الحالة التي خلّفها آخر عمود تمّت قراءته، لا أي عمود مستقبلي. ترتيب الاستدعاءات مهم.

ResultSetMetaData: فحص الأعمدة في وقت التشغيل

يصف ResultSetMetaData شكل النتيجة — عدد الأعمدة وأسماؤها وأنواعها وقابليتها للقيم الفارغة وعروض العرض. هذا لا غنى عنه حين تكتب كودًا عامًا (مُصدِّر CSV، أداة مقارنة بيانات، طابعة بيانات اختبار) يجب أن يتعامل مع استعلامات اعتباطية:

ResultSetMetaData meta = rs.getMetaData(); int columnCount = meta.getColumnCount(); // طباعة صف الترويسة for (int i = 1; i <= columnCount; i++) { System.out.printf("%-20s", meta.getColumnLabel(i)); } System.out.println(); // طباعة كل صف بصورة عامة while (rs.next()) { for (int i = 1; i <= columnCount; i++) { Object value = rs.getObject(i); System.out.printf("%-20s", value != null ? value : "NULL"); } System.out.println(); }

أهم توابع ResultSetMetaData:

  • getColumnCount() — عدد الأعمدة في النتيجة
  • getColumnLabel(i) — الاسم المستعار (AS) أو الاسم الأصلي
  • getColumnName(i) — الاسم الأصلي للعمود في الجدول (يختلف عن التسمية عند وجود اسم مستعار)
  • getColumnTypeName(i) — نوع النوع كنص خاص بقاعدة البيانات (مثل VARCHAR)
  • getColumnType(i) — ثابت java.sql.Types (مثل Types.VARCHAR == 12)
  • isNullable(i)columnNoNulls أو columnNullable أو columnNullableUnknown
  • getColumnDisplaySize(i) — الحد الأقصى لعدد الأحرف عند العرض

بناء محوّل صفوف عام

نمط شائع هو تجريد منطق تحويل النتائج إلى كائنات باستخدام واجهة دالية، وهو ما يتناسب بشكل طبيعي مع التعابير اللامبدية التي تعرفها:

@FunctionalInterface public interface RowMapper<T> { T map(ResultSet rs) throws SQLException; } public <T> List<T> query(String sql, RowMapper<T> mapper) throws SQLException { List<T> results = new ArrayList<>(); try (Statement stmt = conn.createStatement(); ResultSet rs = stmt.executeQuery(sql)) { while (rs.next()) { results.add(mapper.map(rs)); } } return results; } // الاستخدام record User(long id, String username) {} List<User> users = query( "SELECT id, username FROM users", rs -> new User(rs.getLong("id"), rs.getString("username")) );
هذا تحديدًا ما يفعله Spring JdbcTemplate. فهم النسخة الخام من JDBC يُخبرك لماذا تُوجد RowMapper<T> وما الذي يحدث خلف الكواليس في كل طبقة تجريدية مبنية على JDBC.

ResultSets القابلة للتمرير والتعديل

الـ ResultSet افتراضيًا هو للأمام فقط وللقراءة فقط. إن احتجت للتحرك للخلف أو تعديل الصفوف في مكانها، مرّر ثوابت إضافية عند إنشاء العبارة:

Statement stmt = conn.createStatement( ResultSet.TYPE_SCROLL_INSENSITIVE, // يسمح بـ rs.absolute() و rs.previous() ResultSet.CONCUR_UPDATABLE // يسمح بـ rs.updateString() و rs.updateRow() ); ResultSet rs = stmt.executeQuery("SELECT id, status FROM orders WHERE status = 'PENDING'"); while (rs.next()) { rs.updateString("status", "PROCESSING"); rs.updateRow(); // يكتب التغيير مباشرة إلى قاعدة البيانات }

أنواع التمرير: TYPE_FORWARD_ONLY (الافتراضي، الأكثر كفاءة)، TYPE_SCROLL_INSENSITIVE (لقطة ثابتة، لا ترى التغييرات المتزامنة)، TYPE_SCROLL_SENSITIVE (عرض حي، يعتمد على برنامج التشغيل ونادر الاستخدام).

الـ ResultSets القابلة للتعديل نادرًا ما تكون الأداة المناسبة. تتطلّب أن يُعيّن الاستعلام مباشرةً إلى جدول واحد يحتوي مفتاحًا أساسيًا، ولا تدعمها جميع برامج التشغيل بالكامل. استخدم عبارة UPDATE صريحة للوضوح وقابلية النقل.

الخلاصة

مؤشر الـ ResultSet يبدأ قبل الصف الأول؛ حرّكه بـ next() داخل حلقة while. استخدم توابع الاستخراج المكتوبة باسم العمود لمزيد من الوضوح، وBigDecimal للمبالغ المالية، وwasNull() بعد توابع الأنواع البدائية للتمييز بين الصفر والقيمة الغائبة. يمنحك ResultSetMetaData مخطّط الأعمدة في وقت التشغيل ويُشغّل المحوّلات العامة. تغليف منطق التحويل في واجهة دالية RowMapper<T> أسلوب نظيف وقابل للاختبار وهو أساس الأطر عالية المستوى.