يعرض خمس مشكلات شائعة في قواعد البيانات تنشأ في تطبيقات PHP - بما في ذلك تصميم مخطط قاعدة البيانات، والوصول إلى قاعدة البيانات، وكود منطق الأعمال الذي يستخدم قاعدة البيانات - وحلولها.
إذا كانت هناك طريقة واحدة فقط لاستخدام قاعدة البيانات صحيحة...
هناك العديد من الطرق التي يمكنك من خلالها إنشاء تصميم قاعدة البيانات، والوصول إلى قاعدة البيانات، ورمز منطق الأعمال PHP القائم على قاعدة البيانات، ولكن عادةً ما ينتهي الأمر بالخطأ. تشرح هذه المقالة خمس مشكلات شائعة تنشأ في تصميم قاعدة البيانات وكود PHP الذي يصل إلى قاعدة البيانات، وكيفية إصلاحها عند مواجهتها.
المشكلة 1: استخدام MySQL مباشرة
إحدى المشكلات الشائعة هي أن كود PHP القديم يستخدم الدالة mysql_ مباشرة للوصول إلى قاعدة البيانات. توضح القائمة 1 كيفية الوصول إلى قاعدة البيانات مباشرة.
القائمة 1. Access/get.php
<?php
الدالة get_user_id($name)
{
$db = mysql_connect( 'localhost', 'root', 'password' );
mysql_select_db( 'users' );
$res = mysql_query( "حدد المعرف من المستخدمين حيث تسجيل الدخول ='".$name."'" );
while( $row = mysql_fetch_array( $res ) ) { $id = $row[0] }
return $id;
}
var_dump( get_user_id( 'jack' ));
>
لاحظ أنه يتم استخدام وظيفة mysql_connect للوصول إلى قاعدة البيانات. لاحظ أيضًا الاستعلام، الذي يستخدم تسلسل السلسلة لإضافة المعلمة $name إلى الاستعلام.
هناك بديلان جيدان لهذه التقنية: وحدة PEAR DB وفئات كائنات بيانات PHP (PDO). كلاهما يوفر تجريدات من تحديدات قاعدة بيانات محددة. ونتيجة لذلك، يمكن تشغيل التعليمات البرمجية الخاصة بك على IBM® DB2®، أو MySQL، أو PostgreSQL، أو أي قاعدة بيانات أخرى تريد الاتصال بها دون الكثير من التغيير والتبديل.
هناك قيمة أخرى لاستخدام وحدة PEAR DB وطبقة التجريد PDO وهي أنه يمكنك استخدام عامل التشغيل ? في عبارات SQL. يؤدي القيام بذلك إلى تسهيل صيانة تطبيق SQL وحمايته من هجمات حقن SQL.
الكود البديل باستخدام PEAR DB موضح أدناه.
القائمة 2. Access/get_good.php
<?php
require_once("DB.php");
الوظيفة get_user_id( $name )
{
$dsn = 'mysql://root:password@localhost/users';
$db =& DB::Connect( $dsn, array() );
if (PEAR::isError($db)) { die($db->getMessage()); }
$res = $db->query( 'SELECT id FROM users WHERElogin=?',array( $name ) ) ;
$id = null;
while( $res->fetchInto( $row ) ) { $id = $row[0] }
return $id;
}
var_dump( get_user_id( 'jack' ));
>
لاحظ أنه تم إلغاء جميع الاستخدامات المباشرة لـ MySQL، باستثناء سلسلة اتصال قاعدة البيانات في $dsn. بالإضافة إلى ذلك، نستخدم المتغير name في SQL من خلال عامل التشغيل ? ثم يتم إرسال بيانات الاستعلام من خلال المصفوفة الموجودة في نهاية طريقة الاستعلام ().
المشكلة الثانية: عدم استخدام ميزة الزيادة التلقائية
مثل معظم قواعد البيانات الحديثة، تتمتع MySQL بالقدرة على إنشاء معرفات فريدة ذات زيادة تلقائية على أساس كل سجل. أبعد من ذلك، سنظل نرى الكود الذي يقوم أولاً بتشغيل عبارة SELECT للعثور على أكبر معرف، ثم يزيد هذا المعرف بمقدار 1، ويبحث عن سجل جديد. تعرض القائمة 3 مثالاً لنمط سيء.
قائمة 3. Badid.sql
DROP TABLE إذا كان هناك مستخدمين؛
إنشاء مستخدمي الجدول (
معرف متوسط,
نص تسجيل الدخول,
نص كلمة المرور
أدخل في قيم المستخدمين (1، 'jack'، 'pass')
؛
أدخل في قيم المستخدمين (2، 'joan'، 'pass')؛
INSERT INTO users VALUES (1, 'jane', 'pass' );
هنا يتم تحديد حقل المعرف ببساطة كعدد صحيح. لذا، على الرغم من أنها يجب أن تكون فريدة، إلا أنه يمكننا إضافة أي قيمة، كما هو موضح في عبارات INSERT المتعددة التي تلي عبارة CREATE. تعرض القائمة 4 كود PHP لإضافة مستخدمين إلى هذا النوع من المخططات.
القائمة 4. Add_user.php
<?php
require_once("DB.php");
الوظيفة add_user( $name, $pass )
{
$rows = array()
$dsn = 'mysql://root:password@localhost/bad_badid';
$db =& DB::Connect( $dsn, array() );
if (PEAR::isError($db)) { die($db->getMessage()) }
$res = $db->query( "SELECT max(id) FROM users");
$id = null;
while( $res->fetchInto( $row ) ) { $id = $row[0] }
$id += 1
; ")؛
$db->execute( $sth, array( $id, $name, $pass ));
return $id;
}
$id = add_user( 'jerry', 'pass' );
var_dump( $id );
?>
يقوم الكود الموجود في add_user.php أولاً بإجراء استعلام للعثور على الحد الأقصى لقيمة المعرف. يقوم الملف بعد ذلك بتشغيل عبارة INSERT مع زيادة قيمة المعرف بمقدار 1. سوف يفشل هذا الرمز في حالة السباق على خادم محمل بشكل كبير. بالإضافة إلى أنها غير فعالة أيضًا.
إذن ما هو البديل؟ استخدم ميزة الزيادة التلقائية في MySQL لإنشاء معرف فريد تلقائيًا لكل إدراج. يبدو المخطط المحدث هكذا.
القائمة 5. Goodid.php
DROP TABLE إذا كان هناك مستخدمين؛
إنشاء مستخدمي الجدول (
المعرف متوسط وليس فارغًا AUTO_INCREMENT،
نص تسجيل الدخول ليس فارغًا،
نص كلمة المرور غير فارغ،
المفتاح الأساسي (المعرف)
أدخل
في قيم المستخدمين (null, 'jack', 'pass');
أدخل في قيم المستخدمين (null, 'joan', 'pass' );
INSERT INTO users VALUES ( null, 'jane', 'pass' );
أضفنا علامة NOT NULL للإشارة إلى أن الحقل يجب ألا يكون فارغًا. أضفنا أيضًا علامة AUTO_INCREMENT للإشارة إلى أن الحقل يتزايد تلقائيًا، وعلامة PRIMARY KEY للإشارة إلى أن الحقل عبارة عن معرف. هذه التغييرات تسرع الأمور. تعرض القائمة 6 كود PHP المحدث لإدراج المستخدم في الجدول.
القائمة 6. Add_user_good.php
<?php
require_once("DB.php");
الوظيفة add_user( $name, $pass )
{
$dsn = 'mysql://root:password@localhost/good_genid';
$db =& DB::Connect( $dsn, array() );
if (PEAR::isError($db)) { die($db->getMessage()); }
$sth = $db->prepare( "INSERT INTO users VALUES(null,?,?)" );
$db->execute( $sth, array( $name, $pass ) );
$res = $db->query( "SELECT last_insert_id()");
$id = null;
while( $res->fetchInto( $row ) ) { $id = $row[0] }
return $id;
}
$id = add_user( 'jerry', 'pass' );
var_dump( $id );
?>
الآن بدلاً من الحصول على الحد الأقصى لقيمة المعرف، أستخدم عبارة INSERT مباشرة لإدراج البيانات، ثم استخدم عبارة SELECT لاسترداد معرف السجل المدرج الأخير. الكود أبسط بكثير وأكثر كفاءة من الإصدار الأصلي والأنماط المرتبطة به.
السؤال 3: استخدام قواعد بيانات متعددة
في بعض الأحيان، سنرى تطبيقًا حيث يكون كل جدول في قاعدة بيانات منفصلة. وهذا أمر معقول في قواعد البيانات الكبيرة جدًا، ولكن بالنسبة للتطبيقات العامة فإن هذا المستوى من التقسيم غير مطلوب. بالإضافة إلى ذلك، لا يمكن تنفيذ الاستعلامات العلائقية عبر قواعد البيانات، مما ينتقص من فكرة استخدام قاعدة بيانات علائقية بأكملها، ناهيك عن أنه سيكون من الصعب إدارة الجداول عبر قواعد بيانات متعددة. إذًا، كيف يجب أن تبدو قواعد البيانات المتعددة؟ أولا، أنت بحاجة إلى بعض البيانات. تعرض القائمة 7 هذه البيانات مقسمة إلى أربعة ملفات.
القائمة 7. ملف قاعدة البيانات
Files.sql:
إنشاء ملفات الجدول (
معرف متوسط,
معرف_المستخدم متوسط،
نص الاسم,
نص المسار
)
;
أدخل في قيم الملفات (1، 1، 'test1.jpg'، 'files/test1.jpg')؛
أدخل في قيم الملفات (2، 1، 'test2.jpg'، 'files/test2.jpg' )
؛
إسقاط الجدول إذا كان هناك مستخدمين؛
إنشاء مستخدمي الجدول (
معرف متوسط,
نص تسجيل الدخول,
نص كلمة المرور
)
;
أدخل في قيم المستخدمين (1، 'jack'، 'pass')؛
INSERT INTO users VALUES (2, 'jon', 'pass' );
في إصدار قاعدة البيانات المتعددة لهذه الملفات، يجب عليك تحميل عبارة SQL في قاعدة بيانات واحدة ثم تحميل عبارة SQL للمستخدمين في قاعدة بيانات أخرى. يظهر أدناه رمز PHP المستخدم للاستعلام عن قاعدة البيانات للملفات المرتبطة بمستخدم معين.
القائمة 8. Getfiles.php
<?php
require_once("DB.php");
الدالة get_user( $name )
{
$dsn = 'mysql://root:password@localhost/bad_multi1';
$db =& DB::Connect( $dsn, array() );
if (PEAR::isError($db)) { die($db->getMessage()); }
$res = $db->query( "حدد معرف من المستخدمين حيث تسجيل الدخول =؟",array( $name ) ) ;
$uid = null;
while( $res->fetchInto( $row ) ) { $uid = $row[0] }
return $uid;
}
وظيفة get_files( $name )
{
$uid = get_user( $name );
$rows = array()
$dsn = 'mysql://root:password@localhost/bad_multi2';
$db =& DB::Connect( $dsn, array() );
if (PEAR::isError($db)) { die($db->getMessage()); }
$res = $db->query( "SELECT * FROM files WHERE user_id=؟",array( $uid ) ) ;
while( $res->fetchInto( $row ) ) { $rows[] = $row }
إرجاع صفوف $؛
}
$files = get_files( 'jack' );
var_dump( $files);
?>
تتصل وظيفة get_user بقاعدة البيانات التي تحتوي على جدول المستخدمين وتقوم باسترداد معرف مستخدم معين. تتصل وظيفة get_files بجدول الملفات وتقوم باسترداد صفوف الملفات المرتبطة بمستخدم معين.
أفضل طريقة للقيام بكل هذه الأشياء هي تحميل البيانات في قاعدة بيانات ثم تنفيذ استعلام، مثل الاستعلام أدناه.
القائمة 9. Getfiles_good.php
<?php
require_once("DB.php");
الدالة get_files( $name )
{
$rows = array()
$dsn = 'mysql://root:password@localhost/good_multi';
$db =& DB::Connect( $dsn, array() );
if (PEAR::isError($db)) { die($db->getMessage()); }
$res = $db->query("SELECT files.* FROM users, files WHERE
users.login=? AND users.id=files.user_id"،
صفيف (اسم $) ؛
while( $res->fetchInto( $row ) ) { $rows[] = $row }
return $rows;
}
$files = get_files( 'jack' );
var_dump( $files);
?>
الكود ليس أقصر فحسب، بل إنه أسهل في الفهم وفعالية. بدلاً من تنفيذ استعلامين، نقوم بتنفيذ استعلام واحد.
على الرغم من أن هذا السؤال قد يبدو بعيد المنال، إلا أننا عادة ما نستنتج من الناحية العملية أن جميع الجداول يجب أن تكون في نفس قاعدة البيانات ما لم يكن هناك سبب مقنع للغاية.
السؤال 4: عدم استخدام العلاقات
تختلف قواعد البيانات العلائقية عن لغات البرمجة من حيث أنها لا تحتوي على أنواع مصفوفات. بدلاً من ذلك، يستخدمون العلاقات بين الجداول لإنشاء بنية رأس بأطراف بين الكائنات، والتي لها نفس تأثير المصفوفة. إحدى المشكلات التي رأيتها في التطبيقات هي أن المهندسين يحاولون استخدام قاعدة البيانات مثل لغة البرمجة، وذلك عن طريق إنشاء صفائف باستخدام سلاسل نصية بمعرفات مفصولة بفواصل. انظر النمط أدناه.
قائمة 10. Bad.sql
DROP TABLE إذا كانت هناك ملفات؛
إنشاء ملفات الجدول (
معرف متوسط,
نص الاسم,
نص المسار
);
DROP TABLE إذا كان هناك مستخدمين؛
إنشاء مستخدمي الجدول (
معرف متوسط,
نص تسجيل الدخول,
نص كلمة المرور,
نص الملفات
أدخل في قيم الملفات (1، 'test1.jpg'، 'media/test1.jpg')
؛
أدخل في قيم الملفات (2، 'test1.jpg'، 'media/test1.jpg' )؛
INSERT INTO users VALUES (1, 'jack', 'pass', '1,2' );
يمكن أن يكون لدى المستخدم في النظام ملفات متعددة. في لغات البرمجة، يجب استخدام المصفوفة لتمثيل الملفات المرتبطة بالمستخدم. في هذا المثال، يختار المبرمج إنشاء حقل ملفات يحتوي على قائمة مفصولة بفواصل لمعرفات الملفات. للحصول على قائمة بجميع الملفات لمستخدم معين، يجب على المبرمج أولاً قراءة الصفوف من جدول المستخدمين، ثم تحليل نص الملفات وتشغيل عبارة SELECT منفصلة لكل ملف. يظهر الرمز أدناه.
القائمة 11. Get.php
<?php
require_once("DB.php");
الدالة get_files( $name )
{
$dsn = 'mysql://root:password@localhost/bad_norel';
$db =& DB::Connect( $dsn, array() );
if (PEAR::isError($db)) { die($db->getMessage()); }
$res = $db->query( "اختر الملفات من المستخدمين حيث يتم تسجيل الدخول=؟",array( $name ) ) ;
ملفات $ = فارغة؛
$
files = $row[0] }
$rows = array()
;
{
$res = $db->query( "SELECT * FROM files WHERE id=؟",
صفيف(ملف $));
while( $res->fetchInto( $row ) ) { $rows[] = $row }
}
إرجاع الصفوف $؛
}
$files = get_files( 'jack' );
var_dump( $files);
?>
التكنولوجيا بطيئة، ومن الصعب صيانتها، ولا تستفيد بشكل جيد من قاعدة البيانات. الحل الوحيد هو إعادة هندسة المخطط لتحويله مرة أخرى إلى النموذج العلائقي التقليدي كما هو موضح أدناه.
قائمة 12. Good.sql
DROP TABLE إذا كانت هناك ملفات؛
إنشاء ملفات الجدول (
معرف متوسط,
معرف_المستخدم متوسط،
نص الاسم,
نص المسار
);
DROP TABLE إذا كان هناك مستخدمين؛
إنشاء مستخدمي الجدول (
معرف متوسط,
نص تسجيل الدخول,
نص كلمة المرور
أدخل في قيم المستخدمين (1، 'jack'، 'pass')
؛
أدخل في قيم الملفات (1، 1، 'test1.jpg'، 'media/test1.jpg')؛
INSERT INTO files VALUES (2, 1, 'test1.jpg', 'media/test1.jpg' );
هنا، يرتبط كل ملف بالمستخدم في جدول الملفات من خلال وظيفة user_id. قد يتعارض هذا مع رأي أي شخص يفكر في الملفات المتعددة كمصفوفات. بالطبع، لا تشير المصفوفات إلى الكائنات التي تحتوي عليها، بل في الواقع، العكس صحيح. ولكن في قاعدة البيانات العلائقية، هذه هي الطريقة التي تعمل بها، والاستعلامات أسرع وأبسط بكثير بسبب ذلك. تعرض القائمة 13 رمز PHP المقابل.
القائمة 13. Get_good.php
<?php
require_once("DB.php");
الدالة get_files( $name )
{
$dsn = 'mysql://root:password@localhost/good_rel';
$db =& DB::Connect( $dsn, array() );
if (PEAR::isError($db)) { die($db->getMessage()) }
$rows = array();
$res = $db->query("SELECT files.* FROM users,files WHERE users.login=?
AND users.id=files.user_id",array( $name ) );
while( $res->fetchInto( $row ) ) { $rows[] = $row }
إرجاع صفوف $؛
}
$files = get_files( 'jack' );
var_dump( $files);
?>
هنا نقوم بإجراء استعلام لقاعدة البيانات للحصول على كافة الصفوف. التعليمات البرمجية ليست معقدة، وتستخدم قاعدة البيانات كما كان المقصود منها.
السؤال 5: نمط n+1
لا أستطيع أن أخبرك بعدد المرات التي رأيت فيها تطبيقات كبيرة حيث يقوم الكود أولاً باسترداد عدد قليل من الكيانات (على سبيل المثال، العملاء) ثم يذهب ذهابًا وإيابًا لاستردادها واحدًا تلو الآخر للحصول على كل منها تفاصيل الكيان. نطلق على هذا الوضع n+1 لأنه يتم تنفيذ الاستعلام عدة مرات - يسترد استعلام واحد قائمة بجميع الكيانات، ثم يتم تنفيذ استعلام واحد لكل كيان من الكيانات n. هذه ليست مشكلة عندما يكون n=10، ولكن ماذا عن n=100 أو n=1000؟ ومن ثم لا بد أن تكون هناك أوجه قصور. تعرض القائمة 14 مثالاً على هذا النمط.
القائمة 14.
جدول إسقاط Schema.sql في حالة وجود مؤلفين؛
إنشاء مؤلفي الجدول (
المعرف متوسط وليس فارغًا AUTO_INCREMENT،
نص الاسم غير فارغ،
المفتاح الأساسي (المعرف)
);
قم بإسقاط الجدول إذا كان هناك كتب؛
إنشاء كتب الجدول (
المعرف متوسط وليس فارغًا AUTO_INCREMENT،
Author_id متوسط ليس فارغًا،
نص الاسم غير فارغ،
المفتاح الأساسي (المعرف)
أدخل
في قيم المؤلفين ( null, 'Jack Herrington' );
INSERT INTO Authors VALUES ( null, 'Dave Thomas' );
INSERT INTO books VALUES ( null, 1, 'Code Generation in Action' );
أدخل في قيم الكتب ( null, 1, 'Podcasting Hacks' );
أدخل في قيم الكتب ( null, 1, 'PHP Hacks' );
أدخل في قيم الكتب ( null, 2, 'Pragmatic Programmer' );
أدخل في قيم الكتب ( null, 2, 'Ruby on Rails' );
INSERT INTO books VALUES ( null, 2, 'Programming Ruby' );
النمط موثوق ولا توجد أخطاء فيه. تكمن المشكلة في الكود الذي يدخل إلى قاعدة البيانات للعثور على جميع الكتب لمؤلف معين، كما هو موضح أدناه.
القائمة 15. Get.php
<?php
require_once('DB.php');
$dsn = 'mysql://root:password@localhost/good_books';
$db =& DB::Connect( $dsn, array() );
if (PEAR::isError($db)) { die($db->getMessage() }
وظيفة get_author_id( $name )
{
global $db
$res = $db->query( "SELECT id FROM Authors WHERE name=؟",array( $name ) );
$id = null;
while( $res->fetchInto( $row ) ) { $id = $row[0] };
إرجاع معرف $؛
}
الدالة get_books($id)
{
global $db
$res = $db->query( "SELECT id from books WHERE Author_id=؟",array( $id ));
$ids = array();
while( $res->fetchInto( $row ) ) { $ids []= $row[0] };
إرجاع معرفات $؛
}
الدالة get_book($id)
{
global $db
$res = $db->query( "SELECT * FROM books WHERE id=؟", array( $id ) );
while( $res->fetchInto( $row ) ) { return $row }
عودة فارغة؛
}
$author_id = get_author_id( 'جاك هيرينجتون' );
كتب $ = get_books( $author_id );
foreach( كتب $ كـ $book_id ) {
$book = get_book( $book_id );
var_dump($book);
}
?>
إذا نظرت إلى الكود أدناه، فربما تفكر، "مرحبًا، هذا واضح وبسيط حقًا." أولاً، احصل على معرف المؤلف، ثم احصل على قائمة الكتب، ثم احصل على المعلومات حول كل كتاب. نعم، الأمر واضح وبسيط، لكن هل هو فعال؟ الجواب هو لا. تعرف على عدد الاستعلامات التي يتم تنفيذها فقط لاسترداد كتب جاك هيرينجتون. مرة واحدة للحصول على المعرف، ومرة أخرى للحصول على قائمة الكتب، ثم قم بإجراء استعلام لكل كتاب. ثلاثة كتب تتطلب خمسة استفسارات!
الحل هو استخدام دالة لتنفيذ عدد كبير من الاستعلامات كما هو موضح أدناه.
القائمة 16. Get_good.php
<?php
require_once('DB.php');
$dsn = 'mysql://root:password@localhost/good_books';
$db =& DB::Connect( $dsn, array() );
if (PEAR::isError($db)) { die($db->getMessage() }
وظيفة get_books( $name )
{
global $db;
$res = $db->query("حدد الكتب.* من المؤلفين والكتب أين books.author_id=authors.id AND Authors.name=؟",
صفيف (اسم $) ؛
$rows = array();
while( $res->fetchInto( $row ) ) { $rows []= $row }
إرجاع صفوف $؛
}
$books = get_books( 'جاك هيرينجتون' );
var_dump($books);
?>
يتطلب استرداد القائمة الآن استعلامًا واحدًا سريعًا. هذا يعني أنني على الأرجح سأضطر إلى استخدام عدة طرق من هذه الأنواع بمعلمات مختلفة، لكن لا يوجد خيار حقًا. إذا كنت ترغب في الحصول على تطبيق PHP قابل للتطوير، فيجب عليك استخدام قاعدة البيانات بكفاءة، مما يعني استعلامات أكثر ذكاءً.
المشكلة في هذا المثال هي أنه واضح بعض الشيء. بشكل عام، تعتبر هذه الأنواع من مسائل n+1 أو n*n أكثر دقة. وتظهر فقط عندما يقوم مسؤول قاعدة البيانات بتشغيل Query Profiler على النظام عندما يواجه النظام مشكلات في الأداء.
الاستنتاج:
قواعد البيانات هي أدوات قوية، ومثل جميع الأدوات القوية، يمكنك إساءة استخدامها إذا كنت لا تعرف كيفية استخدامها بشكل صحيح. إن الحيلة في تحديد هذه المشكلات وحلها هي فهم التكنولوجيا الأساسية بشكل أفضل. لقد سمعت منذ فترة طويلة أن كتاب منطق الأعمال يشكون من أنهم لا يريدون أن يفهموا قواعد البيانات أو كود SQL. إنهم يستخدمون قاعدة البيانات ككائن ويتساءلون عن سبب ضعف الأداء.
لقد فشلوا في إدراك مدى أهمية فهم SQL لتحويل قاعدة البيانات من ضرورة صعبة إلى تحالف قوي. إذا كنت تستخدم قواعد البيانات كل يوم ولكنك لست على دراية بـ SQL، فيرجى قراءة The Art of SQL. هذا كتاب مكتوب جيدًا وعملي يمكنه إرشادك إلى الفهم الأساسي لقواعد البيانات.