sql JOIN : الربط بين الجداول

المُدوّنة : تحسين الإستعلام

آخر تحيين: 13-12-2015

php : التعابير العادية LEFT/RIGH/ JOIN : الرّبط الخارجي


سنقوم بتحسين الإستعلام في مدوّنتنا باستعمال sql join ، و سنأخذ صفحة show.php على سبيل المثال .

هذا الدّرس ليس فقط تطبيقا لمراجعة التعليمة JOIN . سنرى أشياء جديدة و هامّة . إذا كنتم مُبتدئين ، أنصحكم بقراءته كاملا

في صفحة show.php : التي هي صفحة خصصناها لعرض بيانات موضوع واحد مع التعاليق الخاصة به ، لهذا أجرينا استعلامين : الأول لأخذ بيانات موضوع واحد من جدول blog و الثاني لأخذ بيانات التعاليق من جدول blog_comments . و الجدولين مرتبطان بمفتاح خارجي "blog_id" الذي يُمثّل رقم الموضوع الذي ينتمي إليه كل تعليق .

blog_comments
idauthorcontentblog_id
1......تعليق11
2......تعليق21
3......تعليق32
...
blog
idtitlecontentauthor
1...رحلتي إلى جبال......
2...جزر الكارايبي......

ربط جدول التعاليق بجدول المواضيع

سنقوم أولا بربط جدولينا في الإستعلام ، يمكنكم استعمال إحدى الطّرق التي رأيناها سابقا : INNER JOIN (أو JOIN باختصار) , LEFT JOIN أو RIGHT JOIN . ستعطينا نفس النتيجة في مثالنا ، لأننا لا نريد اختيار جميع البيانات المتطابقة في الجدولين أو غير المتطابقة . بل نريد موضوعا واحدا فقط من جدول blog مع التعاليق التي تخصه في جدول blog_comments :

SELECT * 
FROM blog  
LEFT JOIN blog_comments  
ON blog.id = blog_comments.blog_id  
WHERE blog.id = 1

إذا تابعتم الدّرس السابق جيدا ، فلن تجدوا صعوبة في فهم الشيفرة .
استعملنا التعليمة : WHERE WHERE blog.id = 1 : لاختيار موضوع واحد فقط . لو لم نضفها لحصلنا على جميع المواضيع من جدول blog و التعاليق الخاصة بها من جدول blog_comments .
رقم "1" هو رقم الموضوع على سبيل المثال فقط . سنعوضّه برقم الموضوع الحقيقي .

أيضا استعمال النجمة "*" لاختيار جميع البيانات هو من سبيل المثال فقط ، لا أنصحكم باستعمالها . سنُعوّظها بأسماء الحقول التي نريدها

بما أن لدينا نفس الأسماء في الجدولين (id و author و content و creation_date )، فمن الضروري ربط كل حقل بجدوله و استعمال الأسماء الخيالية . إذا لم تقوموا بهذه الإجراءات . سيتم تنفيذ استعلامكم ، لكنكم لن تحصلوا على أي نتيجة . لأن حاسوبكم لن يعرف من أي حقل سيختار البيانات . سنستعمل هذه الأسماء أيضا على الجداول لاختزال أسماءها .
مُلخّص كل هذه الثّرثرة هو ما يلي :

SELECT b.id AS blog_id, title, b.author AS blog_author, b.content AS blog_content, b.creation_date AS blog_date,  
       c.id AS com_id, c.author AS com_author, c.content AS com_content, c.creation_date AS com_date
FROM blog b
LEFT JOIN blog_comments c ON b.id = c.blog_id 
WHERE b.id = :bid

كتبنا الكثير في SELECT . لقد تعمّدت منذ البداية ، إعطاءكم نفس أسماء الحقول للجدولين . من أجل هذه اللحظة ، حتّى تتمرّنوا على الطّريقة الصحيحة لاستعمال الحقول الخيالية . كان بالإمكان إنشاء الجدولين بأسماء حقول مُختلفة ، و من ثم الإستغناء عن استعمال الحقول الخيالية . على كل حال ، استخدام هذه الأخيرة ، جد عملي ، لاحقا ستضطرون لربط أكثر من جدولين : 3 أو 4 أو 5 ... جداول في نفس الإستعلام .
استعلامنا جاهز . لم يبقى لنا سوى عرض البيانات .

عرض موضوع واحد مع تعاليقه

سأبدأ بتذكير بسيط : بعد تنفيذ الإستعلام ، نقوم باختيار الطريقة التي سنبحث فيها عن البيانات في الجداول ، رأينا تعليمتين و هما fetch() و fetchAll() :

  • fetch : لأخذ بيانات سطر واحد من الجدول أو الجداول
    $blog = $response->fetch();
  • fetchAll : لأخذ مجموعة من السطور في آن واحد .
    $comments = $response->fetchAll();

السؤال الذي يواجه أغلب المبتدئين ، هو :
ما هي التعليمة التي سأستعملها لأخذ بيانات الجدولين ؟
إذا استعملتم التعليمة fetch ستحصلون على بيانات سطر واحد (موضوع واحد) من جدول blog . و أيضاً بيانات سطر واحد فقط من جدول blog_comments . أي ، إن كان لديكم 10 تعاليق متعلقة بهذا الموضوع ، فلن تحصلوا إلا على تعليق واحد فقط . و هذا التصرف ما لن ترغبوا فيه .
أمّا إذا استعملنا التعليمة fetchAll ، ستتوافق مع جدول blog_comments لأننا سنختار مجموعة من السّطور و هذا جيد . لكنها ليست كذلك لجدول blog .

لتجاوز هذا ، فالمسألة ليست بالأساس مشكلة . لأن هناك حلولا سهلة ، سنرى طريقتين مُختلفتين للتعامل مع مثل هكذا استعلام .
1ـ الأولى تعتمد على تقنية إعادة تنفيذ نفس الإستعلام . و سنحتاج لكلتا التعليمتين fetch و fetchAll.
2ـ و الطريقة الثانية سنقتصر على fetchAll فقط

1. PDO إعادة تنفيذ نفس الإستعلام

عندما نتحدّث عن تنفيذ الإستعلام ، فإننا نعني بذلك التعليمة execute()
تُمكّنُنا PDO من استعمال تقنية هامة ، و هي إعادة تنفيذ نفس الإستعلام عدّة مرات في صفحتنا ، دون الحاجة إلى إعادة كتابة شيفرة الإستعلام أصلا . و ذلك بإعادة استدعاء التعليمة execute فقط . و هذا ما نحتاجه بالضبط في مثالنا . سنترك استعلامنا كما هو . في المرّة الأولى سننفذه لاختيار الموضوع و في المرّة الثانية لاختيار التعاليق :

// الإستعلام
// ...

// التنفيذ الأول لاختيار الموضوع 
$response->execute();
$blog = $response->fetch();
$response->CloseCursor();
   // عرض بيانات الموضوع
   // ...
// التنفيذ الثاني لاختيار التعاليق 
$response->execute();
$comments = $response->fetchAll();
$response->CloseCursor();
// عرض بيانات التعاليق
   // ...

هذا كل ما في الأمر . أترككم لتعديل استعلامكم في صفحة show.php . و سننتقل للتعرف على الطّريقة الثانية . باستعمال fetchAll فقط .

2 . استعمال fetchAll

هذه الطّريقة لا علاقة لها بتعليمات PDO ، بل تخص لغة php . و هي ليست أصلا تقنية ، بل تعتمد على طريقة تفكيركم المنطقي للتعامل مع اللغة .
سنترك استعلامنا كما هو ، و نقوم بتنفيذه . للبحث عن البيانات في الجدولين سنستعمل التعليمة fetchAll . إذاً أثناء العرض يجب استعمال الحلق while أو foreach .

// الإستعلام
// ... 
$response->execute();
$results = $response->fetchAll();
$response->CloseCursor();

foreach($results as $result) 
{
  //عرض بيانات الموضوع 
  // ...
  // عرض بيانات التعاليق
  // ...
}

بالنسبة للتعاليق ليست هناك أي مشكلة ، سيتم عرضها كلها داخل الحلقة . بالإضافة إلى شيء هام . هو أنه سيتم إعادة عرض بيانات الموضوع مع كل تعليق . و هذا التصرّف ما لن ترغبوا فيه . لأننا نريد عرض بيانات الموضوع مرّة واحدة ، في أعلى الصفحة مثلا . ثم عرض التعاليق أسفله دفعة واحدة .
لهذا سنعتمد على تحيين متغير و نعطيه قيمة "0" .
ثم سنضع هذا المتغير داخل الحلقة ، و نقارنه باستعمال أداة الشرط if برقم الموضوع .


$j = 0; // تحيين المتغيّر
foreach($results as $result) 
{
  if($j != $result['blogId']) // في المرور الأول سيكون الشرط صحيحا
  {
    $j = $result['blogId']) // تثبيت قيمة المتغير 
     //عرض بيانات الموضوع 
     // ...
  }
  // عرض بيانات التعاليق
     // ...
}

عند أول مرور في الحلقة ، سيكون المتغير $j يساوي "0" . بما أنه ليس لدينا أي موضوع بهذه القيمة ، عند مقارنة قيمة المتغير برقم الموضوع سيكون الشرط صحيحا ، و سيتم عرض بيانات الموضوع و بيانات التعليق الأول . بعد حصولنا على مبتغانا ، نقوم فورا بتثبيت قيمة المتغير j$ بحيث نجعلها تُساوي لقيمة رقم الموضوع . بهذه الطّريقة أثناء المرور الثاني و الثالث ... إلخ، في الحلقة لعرض التعليق الثاني و الثالث و دواليك . لن يتم إعادة عرض بيانات الموضوع ، لأن الشرط سيكون دائماً خاطئاً ، بما أن قيمة j$ لم تعُد تختلف عن قيمة الموضوع $j = $result['blogId']

هذا كل ما في الأمر ، أترككم لهضم كل هذا . يمكنكم اختيار الطريقة التي تفضلّونها .
هذا الدّرس كان بمثابة تتمة للدّرس السابق لتوضيح بعض المفاهيم . لديكم الآن جميع الوسائل لإجراء أي استعلام يمرّ بمخيّلتكم .


أهديكم صفحة "show.php" باستعمال الطريقة الأولى ، أي إعادة تنفيذ الإستعلام

show.php

<?php
require 'includes/header.php';
require 'includes/db-connection.php';

// استقبال مفتاح الموضوع 
$blog_id = isset($_GET['id']) ? (int)$_GET['id']: 1;

// أخذ بيانات الموضوع و التعاليق من القاعدة ، إعتمادا على مفتاح الموضوع
$response = $db->prepare('SELECT b.id AS blog_id, title, b.author AS blog_author, b.content AS blog_content, b.creation_date AS blog_date,  
            c.id AS com_id, c.author AS com_author, c.content AS com_content, c.creation_date AS com_date
            FROM blog b
            LEFT JOIN blog_comments c ON b.id = c.blog_id 
            WHERE b.id = :bid
           ');
$response->bindValue(':bid', $blog_id, PDO::PARAM_INT);

// تنفيذ الإستعلام لأول مرة للحصول على بيانات الموضوع
$response->execute();
$blog = $response->fetch();
$response->CloseCursor();

// عرض بيانات الموضوع
if($blog)
{
    $blog_title = htmlspecialchars($blog['title']);
    $blog_author = htmlspecialchars($blog['blog_author']);
    $blog_content = nl2br(htmlspecialchars($blog['blog_content']));
    $blog_tari5 = htmlspecialchars($blog['blog_date']);

  echo '
    <article class="blog">
    <h2>
        '.$blog_title.'
    </h2>
    <div class="details">
        <a href="edit-blog.php?id='.$blog_id.'" class="button orange"> تعديل </a> - 
        <a href="delete-blog.php?id='.$blog_id.'" class="button red"> حذف </a>
    </div>
	<hr>
    <div>
        <div class="content">'.$blog_content.'</div>
        <div class="sidebar center"><span>الكاتب : '.$blog_author.'</span>
	        <time class="orange">'.date("Y-m-d س i:H", strtotime($blog_tari5)).'</time>
            <h3>مواضيع لنفس الكاتب</h3>
            <ul>
                <li><a href="#">كتمرين . يمكنك لاحقا عرض هنا ، روابط للمواضيع التي نشرها هذا الكاتب </a></li>
            </ul>
        </div>
    </div>
    </article>
<hr>';
}else {
  die('لا يوجد أي موضوع !!! ');
}

// تنفيذ الإستعلام لثاني مرة للحصول على  بيانات التعاليق
$response->execute();
$comments = $response->fetchAll();
$response->CloseCursor();

// عرض التعاليق
echo '<h3 class="center"> التعاليق </h3>';
$j = 0;

if($blog['com_id']) // إذا كان لدينا على الأقل تعليق واحد
{
    echo '<div class="comments">';

    foreach($comments as $comment) 
    {
        $com_id = (int)$comment['com_id'];
        $com_author = htmlspecialchars($comment['com_author']);
        $com_content = nl2br(htmlspecialchars($comment['com_content']));
        $com_tari5 = htmlspecialchars($comment['com_date']);

        $css_class = ($j % 2 == 1) ? 'odd' : 'even'; // الشيفرة لها علاقة بالتنميق فقط
        ++$j;

        echo '
        <article class="'.$css_class .'">
            <div class="resume" id="comment-'.$com_id.'">
                <span>'.$com_author.'</span> <time class="details"> '.date("Y-m-d س i:H د",strtotime($com_tari5)).'</time>
                <a href="edit-comment.php?id='.$com_id.'" class="button orange"> تعديل </a> - 
                <a href="delete-comment.php?id='.$com_id.'" class="button red"> حذف </a>
            </div>
            <div class="comment">
                '.$com_content.'
            </div>
        </article>';
    }
    echo '</div>';
}
else 
{
    echo '<p class="errors"> لا يوجد أي تعليق ، كن أوّل من يفتتح النقاش !!!</p>';
}

// إستمارة إضافة التعاليق
echo '<hr>
<form action="add-comment.php?id='.$blog_id.'" method="post" id="form">
  <fieldset class="form-item">
    <legend class="green">إضافة تعليق</legend>
    <label for="author"> الكاتب</label>
        <input type="text" name="author" id="author"><br>

    <label for="content">المحتوى</label>
    <textarea name="content" id="content"></textarea>
  </fieldset>

  <fieldset class="form-submit">
    <input type="submit" name="submit" value="إرسال" class="button green">
  </fieldset>
</form>
';
require 'includes/footer.php';
?>