عمليات CRUD
CRUD — وهي اختصار لـ Create (إنشاء) وRead (قراءة) وUpdate (تحديث) وDelete (حذف) — تمثّل العمود الفقري لأي تطبيق يعتمد على قاعدة بيانات. في هذا الدرس نستعرض هذه العمليات الأربع من مبادئها الأولى وصولًا إلى الأنماط المعتمدة في بيئة الإنتاج، مع تحليل المقايضات في كل خطوة. أنت تعرف مسبقًا كيفية فتح Connection وإنشاء Statement؛ هنا نربط هذه القطع معًا في كود وصول بيانات موثوق وحقيقي.
الإعداد: جدول نموذجي
تعمل جميع الأمثلة في هذا الدرس على جدول products بسيط:
CREATE TABLE products (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(100) NOT NULL,
price DECIMAL(10, 2) NOT NULL,
stock INT NOT NULL DEFAULT 0
);
سنستخدم دالة مساعدة getConnection() تُعيد اتصالًا من مجموعة اتصالات أو من مدير الاتصالات. التفاصيل تأتي في دروس لاحقة؛ افترض الآن أنها متاحة.
Create — إدراج الصفوف
استخدم دائمًا PreparedStatement للإدراج عند وجود بيانات مُدخلة من المستخدم. النقطة الجوهرية هنا هي كيفية استرداد المفتاح الأساسي المُولَّد تلقائيًا بعد الإدراج:
public long insertProduct(Connection conn, String name, double price, int stock)
throws SQLException {
String sql = "INSERT INTO products (name, price, stock) VALUES (?, ?, ?)";
// RETURN_GENERATED_KEYS يطلب من المُشغِّل حفظ المفتاح الجديد
try (PreparedStatement ps = conn.prepareStatement(sql,
Statement.RETURN_GENERATED_KEYS)) {
ps.setString(1, name);
ps.setDouble(2, price);
ps.setInt(3, stock);
int affected = ps.executeUpdate(); // يُعيد عدد الصفوف المُدرَجة
if (affected == 0) {
throw new SQLException("فشل الإدراج، لم تتأثر أي صفوف.");
}
try (ResultSet keys = ps.getGeneratedKeys()) {
if (keys.next()) {
return keys.getLong(1); // العمود الأول = المفتاح الأساسي المُولَّد
} else {
throw new SQLException("فشل الإدراج، لم يُعَد أي مفتاح مُولَّد.");
}
}
}
}
لماذا نتحقق من affected == 0؟ تُعيد executeUpdate() عدد الصفوف. إذا أثّر مُشغِّل (trigger) أو قيد على مستوى قاعدة البيانات على عملية الإدراج بصمت، سيكون العدد 0 وسيحاول كودك قراءة مفتاح غير موجود. التحقق المبكر يجعل الأخطاء صريحة وواضحة.
استدعاء الدالة واستخدام المعرّف المُعاد:
try (Connection conn = getConnection()) {
long newId = insertProduct(conn, "Wireless Mouse", 29.99, 150);
System.out.println("تم الإدراج بمعرّف = " + newId);
}
Read — الاستعلام عن الصفوف
قراءة البيانات تعني تنفيذ SELECT والتكرار على ResultSet. نمطان شائعان: جلب صف واحد بالمفتاح الأساسي، أو جلب مجموعة بمعايير تصفية.
جلب صف واحد بالمعرّف:
public Optional<Product> findById(Connection conn, long id) throws SQLException {
String sql = "SELECT id, name, price, stock FROM products WHERE id = ?";
try (PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setLong(1, id);
try (ResultSet rs = ps.executeQuery()) {
if (rs.next()) {
return Optional.of(mapRow(rs));
}
return Optional.empty();
}
}
}
private Product mapRow(ResultSet rs) throws SQLException {
return new Product(
rs.getLong("id"),
rs.getString("name"),
rs.getBigDecimal("price"), // يُفضَّل BigDecimal للقيم النقدية
rs.getInt("stock")
);
}
استخدم أسماء الأعمدة لا مؤشراتها عند قراءة ResultSet. الوصول بالمؤشر مثل rs.getString(2) ينكسر بصمت إذا تغير ترتيب قائمة SELECT. الوصول بالاسم مثل rs.getString("name") موثَّق ذاتيًا ومقاوم للتغييرات في الاستعلام.
جلب قائمة بمعيار تصفية:
public List<Product> findByMinStock(Connection conn, int minStock)
throws SQLException {
String sql = "SELECT id, name, price, stock FROM products WHERE stock >= ?";
List<Product> results = new ArrayList<>();
try (PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setInt(1, minStock);
try (ResultSet rs = ps.executeQuery()) {
while (rs.next()) {
results.add(mapRow(rs));
}
}
}
return results;
}
Update — تعديل الصفوف الموجودة
التحديثات تتّبع نفس نمط PreparedStatement. أعِد دائمًا عدد الصفوف المتأثرة حتى يتمكن المُستدعي من التحقق من وجود الصف فعلًا:
public int updatePrice(Connection conn, long id, double newPrice)
throws SQLException {
String sql = "UPDATE products SET price = ? WHERE id = ?";
try (PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setDouble(1, newPrice);
ps.setLong(2, id);
return ps.executeUpdate(); // 1 إذا وُجد الصف، 0 إذا لم يُعثر عليه
}
}
نمط الاستخدام الذي يميّز "الصف غير موجود" عن الأخطاء الأخرى:
int rows = updatePrice(conn, productId, 24.99);
if (rows == 0) {
throw new NoSuchElementException("المنتج " + productId + " غير موجود.");
}
لا تبنِ جملة WHERE بدمج مدخلات المستخدم نصيًا أبدًا. حتى تحديث بسيط مثل "UPDATE products SET price = " + price + " WHERE id = " + id عُرضة لحقن SQL. اربط القيم دائمًا عبر معاملات PreparedStatement.
Delete — حذف الصفوف
الحذف متطابق هيكليًا مع التحديث: PreparedStatement مع جملة WHERE يُعيد عدد الصفوف المتأثرة:
public int deleteProduct(Connection conn, long id) throws SQLException {
String sql = "DELETE FROM products WHERE id = ?";
try (PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setLong(1, id);
return ps.executeUpdate();
}
}
الجمع معًا — عرض CRUD مصغّر
فيما يلي عرض موجز قائم بذاته يُشغّل كل عملية بالتسلسل لترى كيف تتكامل:
try (Connection conn = getConnection()) {
// إنشاء
long id = insertProduct(conn, "USB-C Hub", 49.99, 75);
System.out.println("تم الإنشاء: id=" + id);
// قراءة
findById(conn, id).ifPresentOrElse(
p -> System.out.println("تمت القراءة: " + p),
() -> System.out.println("غير موجود.")
);
// تحديث
int updated = updatePrice(conn, id, 44.99);
System.out.println("الصفوف المُحدَّثة: " + updated);
// حذف
int deleted = deleteProduct(conn, id);
System.out.println("الصفوف المحذوفة: " + deleted);
}
المقايضات الرئيسية وأفضل الممارسات
- أغلق الموارد بترتيب عكسي. أغلق
ResultSet قبل PreparedStatement قبل Connection. استخدام try-with-resources المتداخلة يضمن ذلك تلقائيًا.
- الأعمدة النقدية: استخدم
BigDecimal لا double. تحفظ rs.getBigDecimal("price") القيم العشرية الدقيقة؛ أما getDouble فتُدخل أخطاء تقريب للفاصلة العائمة تتراكم بمرور الوقت.
- تحقق من عدد الصفوف المتأثرة. عدد 0 على تحديث أو حذف يعني أن شرط WHERE لم يطابق شيئًا — هذا عادةً خطأ وليس نجاحًا.
- دوال ذات مسؤولية واحدة. كل دالة تفعل شيئًا واحدًا (بحث، إدراج، تحديث، حذف). هذا يُبقي المنطق قابلًا للاختبار ويُسهّل تغيير SQL لاحقًا دون المساس بالمُستدعين.
- دع الاستثناءات تنتشر. التقاط
SQLException في طبقة الوصول للبيانات وابتلاعها يُخفي الأخطاء. إما انشرها أو لفّها في استثناء دومين غير محقوق (DataAccessException) ليقرر المُستدعي كيفية التعامل معها.
الخلاصة
كل عملية CRUD تتّبع نفس الإيقاع الثلاثي: أعدّ SQL بعناصر نائبة، اربط المعاملات، نفّذ وتحقق من النتيجة. للإدراج، التقط المفتاح المُولَّد بـ RETURN_GENERATED_KEYS. للقراءة، فضّل أسماء الأعمدة على مؤشراتها وعيّن الصفوف لكائنات النطاق في دالة مساعدة مخصصة. للتحديث والحذف، افحص عدد الصفوف المتأثرة للتمييز بين "الصف غير موجود" والعملية الناجحة بتأثير صفري. في الدرس التالي سنجمع عمليات CRUD المتعددة في معاملات قاعدة بيانات ذرية.