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

تنفيذ العبارات

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

تنفيذ العبارات

بمجرد الحصول على Connection فعّال، تأتي الخطوة التالية وهي إرسال SQL إلى قاعدة البيانات. تمنحك JDBC الواجهة Statement لهذا الغرض، وتقدّم طريقتين رئيسيتين للتنفيذ: executeQuery للقراءة، وexecuteUpdate للكتابة. إتقان الفارق بينهما — ومعرفة متى تستخدم كلًّا منهما — هو أساس كل تفاعل مع قاعدة البيانات ستكتبه.

إنشاء Statement

يُنشأ كائن Statement من كائن الاتصال. وهو أيضًا مورد يجب إغلاقه بعد الاستخدام، لذا استخدم try-with-resources:

import java.sql.Connection; import java.sql.DriverManager; import java.sql.Statement; public class StatementDemo { public static void main(String[] args) throws Exception { String url = "jdbc:mysql://localhost:3306/shop"; try (Connection conn = DriverManager.getConnection(url, "root", "secret"); Statement stmt = conn.createStatement()) { // استخدم stmt هنا } // يُغلق conn و stmt تلقائيًا } }
Statement ليست آمنة للاستخدام بخيوط متعددة. لا تشارك نسخة واحدة من Statement بين أكثر من خيط تنفيذ. يجب على كل خيط إنشاء نسخته الخاصة من Connection الخاص به.

executeQuery — قراءة البيانات

تُستخدم executeQuery(String sql) لعبارات SQL التي تُعيد مجموعة نتائج — وتكون غالبًا SELECT. تُعيد كائنًا من نوع ResultSet تتنقّل عبره للوصول إلى الصفوف. تُطلق الطريقة SQLException إن كانت SQL مشوّهة أو رفضتها قاعدة البيانات، وتُطلقها أيضًا إن مرّرت عبارة DML (مثل UPDATE) بدلًا من استعلام قراءة.

import java.sql.*; public class QueryDemo { public static void main(String[] args) throws Exception { String url = "jdbc:mysql://localhost:3306/shop"; String sql = "SELECT id, name, price FROM products WHERE price < 100"; try (Connection conn = DriverManager.getConnection(url, "root", "secret"); Statement stmt = conn.createStatement(); ResultSet rs = stmt.executeQuery(sql)) { while (rs.next()) { int id = rs.getInt("id"); String name = rs.getString("name"); double price = rs.getDouble("price"); System.out.printf("%-3d %-20s %.2f%n", id, name, price); } } } }

نقاط مهمة حول executeQuery:

  • تُعيد كائن ResultSet — لن يكون null أبدًا، لكنه قد يكون فارغًا (صفر صفوف).
  • يبدأ مؤشر ResultSet قبل الصف الأول؛ استدعِ rs.next() للتقدّم.
  • يُعدّ ResultSet بدوره موردًا — اجعله داخل try-with-resources أو أغلقه صراحةً.
  • إغلاق Statement يُغلق تلقائيًا الـ ResultSet المرتبط به.

executeUpdate — كتابة البيانات

تُستخدم executeUpdate(String sql) لأي عبارة تُعدّل البيانات أو البنية: INSERT وUPDATE وDELETE وCREATE TABLE وDROP TABLE وما شابهها. تُعيد قيمة int تمثّل عدد الصفوف المتأثرة. لعبارات DDL مثل CREATE تكون القيمة المُعادة دائمًا 0.

import java.sql.*; public class UpdateDemo { public static void main(String[] args) throws Exception { String url = "jdbc:mysql://localhost:3306/shop"; try (Connection conn = DriverManager.getConnection(url, "root", "secret"); Statement stmt = conn.createStatement()) { // INSERT — تُعيد 1 إن أُدرج صف واحد int inserted = stmt.executeUpdate( "INSERT INTO products (name, price) VALUES ('Widget', 9.99)" ); System.out.println("Rows inserted: " + inserted); // UPDATE — تُعيد عدد الصفوف المُطابِقة التي تغيّرت int updated = stmt.executeUpdate( "UPDATE products SET price = 8.99 WHERE name = 'Widget'" ); System.out.println("Rows updated: " + updated); // DELETE — تُعيد عدد الصفوف المحذوفة int deleted = stmt.executeUpdate( "DELETE FROM products WHERE price < 1.00" ); System.out.println("Rows deleted: " + deleted); } } }
تحقّق دائمًا من القيمة المُعادة بـ executeUpdate. إن توقّعت تحديث صف واحد فجاء العدد 0، فجملة WHERE لم تُطابق شيئًا. تجاهل هذه القيمة يُخفي الخطأ المنطقي في صمت.

الطريقة execute — البديل الشامل

تتوفّر طريقة ثالثة هي execute(String sql) تتعامل مع أي عبارة SQL بصرف النظر عمّا تُعيده. تُعيد boolean: القيمة true تعني أن النتيجة الأولى ResultSet، وfalse تعني أنها عدد صفوف مُحدَّثة. تستدعي بعدها stmt.getResultSet() أو stmt.getUpdateCount() وفقًا لذلك.

boolean hasResultSet = stmt.execute("SELECT 1"); if (hasResultSet) { try (ResultSet rs = stmt.getResultSet()) { // معالجة الصفوف } } else { int count = stmt.getUpdateCount(); System.out.println("Affected rows: " + count); }

في الممارسة العملية تُستخدم execute حين تشغّل SQL ديناميكية أو مُدخَلة من المستخدم لا تعرف نوعها وقت الترجمة — كأداة إدارة قواعد بيانات تقبل استعلامات عشوائية. في كود التطبيقات العادية، افضّل executeQuery أو executeUpdate لأن النية تكون واضحة والمُجمّع يكشف أي استخدام خاطئ.

لماذا لا نخلط بينهما؟

استدعاء executeQuery بعبارة غير SELECT، أو executeUpdate باستعلام SELECT، يُسفر عن SQLException وقت التشغيل (يتفاوت السلوك التفصيلي من مشغّل لآخر، لكن الاعتماد عليه خطأ). كذلك يُعبّر التمييز عن النية لكل مطوّر يقرأ كودك: هذا الفرع يقرأ، ذاك يكتب. ويتناسب ذلك مع تجمّع الاتصالات وتوجيه القراءة إلى نسخة النسخ، حيث تذهب القراءات إلى النسخة وتذهب الكتابات إلى الخادم الأساسي.

لا تبني نصوص SQL بدمج مُدخَلات المستخدم. الكود في هذا الدرس يستخدم قيمًا ثابتة للتركيز على الواجهة البرمجية. في الدرس التالي ستتعلّم PreparedStatement الذي يمنع حقن SQL ويجب استخدامه مع أي بيانات خارجية.

استرداد المفاتيح المُوَلَّدة تلقائيًا

عند تنفيذ INSERT في جدول يحتوي على مفتاح أساسي بزيادة تلقائية، كثيرًا ما تحتاج إلى المعرّف المُولَّد فورًا. مرّر العلم Statement.RETURN_GENERATED_KEYS إلى executeUpdate، ثم اقرأ المفتاح من ResultSet خاص:

int rows = stmt.executeUpdate( "INSERT INTO products (name, price) VALUES ('Gadget', 29.99)", Statement.RETURN_GENERATED_KEYS ); try (ResultSet keys = stmt.getGeneratedKeys()) { if (keys.next()) { long newId = keys.getLong(1); System.out.println("New product ID: " + newId); } }

الخلاصة

  • executeQuery — لعبارات SELECT؛ تُعيد ResultSet.
  • executeUpdate — لعبارات INSERT / UPDATE / DELETE / DDL؛ تُعيد عدد الصفوف.
  • execute — البديل الشامل حين لا يُعرف نوع SQL وقت الترجمة.
  • أغلق Statement وResultSet دائمًا باستخدام try-with-resources.
  • استخدم RETURN_GENERATED_KEYS لاسترداد المعرّفات المُنشأة تلقائيًا بعد INSERT.
  • انتقل إلى PreparedStatement فور دخول بيانات المستخدم في SQL — لا تدمجها أبدًا بتسلسل النصوص.