پایگاه داده ها
اتصال به موتور پایگاه داده ها
F3 طوری طراحی شده که کار با پایگاههای داده SQL رو به ملایمت یه نسیم کنه. اگه دوست ندارین درگیر جزئیات SQL بشین و تمایل دارین از داده ها به شکل شئ گرا استفاده کنید، بخشهای اولیه این آموزش رو نادیده بگیرین. هرچند اگه نیاز به انجام کارهای پیچیده روی داده ها داشته باشین و بهینه بودن کارآیی دیتابیس براتون مهمه، SQL مسیری هست که باید طی کنید.
ارسال دستورات به موتورهای SQL مثل MySQL و SQLite و SQL Server و Sybase و Oracle با دستور آشنای ()f3->set$ انجام میشه. اما قبلش باید به دیتابیس وصل بشیم. برای این کار میتونیم از روش زیر استفاده کنیم:
$db = new DB\SQL('sqlite:/absolute/path/to/your/database.sqlite'));
یه مثال دیگه برای MySQL :
$db = new DB\SQL(
'mysql:host=localhost;port=3306;dbname=blog',
'root',
'ncis'
);
ارسال کوئری به دیتابیس
خوب. وصل شدن که راحت بود، نه؟ خیلی شبیه کاری بود که توی PHP معمولی و خام انجام میدادیم. فقط کافیه ساختار DSN دیتابیسی که میخواین بهش وصل بشین رو بدونین. برای توضیحات بیشتر،
مستندات PDO سایت PHP رو بخونید.
خوب اجازه بدین کدمون رو ادامه بدیم:
$f3->set('result', $db->exec('SELECT * from categories'));
echo Template::instance()->render('abc.htm');
چی شد؟ نباید چیزی رو مثل PDO و اشاره گرها و fetch کردن و... انجام بدیم؟ جواب ساده است: مجبور نیستین. F3 همه چیز رو با انجام این کارها در پشت پرده، ساده کرده.
ایندفعه میخوایم قالب HTML موردنظر (یعنی abc.htm) رو بسازیم که به همین کوتاهیه:
<repeat group="{{ @result }}" value="{{ @item }}">
<div>{{ @item.name }}</div>
</repeat>
در اکثر مواقع دستور SQL که با SET به متغیر دادین، برای تولید یک آرایه آماده برای استفاده توی HTML کافیه و شما میتونید از متغیر آرایه result توی قالبتون مستقیماً استفاده کنید. اما F3 مانع از این نمیشه که خروجی رو توی کنترلر کننده های داخلی SQL که توی F3 وجود دارن قرار بدین. درحقیقت کلاس DB\SQL مستقیماً از کلاس PDO خود PHP مشتق شده و درنتیجه اگه بخواین کنترل کاملی داشته باشین، به تمام اجزا و عناصر داخلی PDO توی هر فرآیند دسترسی خواهید داشت.
تراکنش ها (Transaction)
خوب یه مثال دیگه. بجای اینکه یه دستور رو بعنوان پارامتر به ()db->exec$ بدیم، میتونین یه آرایه از دستورات SQL بفرستین:
$db->exec(array(
"DELETE FROM diet WHERE food='cola'",
"INSERT INTO diet (food) VALUES ('carrot')"
'SELECT * FROM diet',
));
F3 اینقدر هوشمند هست که بفهمه وقتی یه آرایه از دستورات SQL میفرستین، به معنای یه ترنزکشن (تراکنش) دسته جمعی SQL هست. نیاز نیست شما نگران rollback و commit توی SQL باشین چون فریمورک خودش اگه مشکلی توی هرکدوم از دستورات پیش بیاد، وضعیت دیتابیس رو به قبل از اجرای اولین دستور برمیگردونه و اگه خطایی رخ نداد، F3 خودش تمام تغییرات رو توی دیتابیس ثبت میکنه.
البته یه راه دیگه هم برای شروع و خاتمه Transaction بصورت دستی وجود داره:
$db->begin();
$db->exec("DELETE FROM diet WHERE food='cola'");
$db->exec("INSERT INTO diet (food) VALUES ('carrot')");
$db->exec('SELECT * FROM diet');
$db->commit();
اگه خطایی توی دستورات رخ بده، rollback بطور خودکار صدا زده میشه و شما لازم نیست کاری انجام بدین.
برای بدست آوردن فهرست تمام دستوراتی که برای دیتابیس ارسال شدن، این کد رو بنویسید:
echo $db->log();
کوئری های پارامتری
ارسال پارامترهای متنی به دستورات SQL خطرناکه. این دستور رو درنظر بگیرین:
$db->exec("SELECT * FROM users WHERE username='" . $f3->get('POST.userID') . "'");
اگه متغیر userID که به روش POST ارسال شده، از هیچ فرآیند خنثی سازی کدهای مخرب رد نشه، یک کاربر بدخواه ممکنه این متن رو بفرسته و باعث بشه دیتابیس شما به شکل غیر قابل بازگشتی خراب بشه (اگه پشتیبان نداشته باشین) :
admin"; DELETE FROM users; SELECT "1
خوشبختانه کوئری های پارامتری به شما کمک میکنن این ریسکها رو از بین ببرین:
$db->exec('SELECT * FROM users WHERE userID=?', $f3->get('POST.userID'));
اگه F3 متوجه بشه که مقدار پارامتر/توکن کوئری یه رشته است، لایه اتصال داده پشت صحنه رشته رو خنثی سازی (Escape) میکنه و اگه لازم بود، کوتیشن هم بهش اضافه میکنه.
مثال قسمت قبلی ما اگه اینطوری نوشته بشه، درمقابل SQL Injection خیلی امن تر خواهد بود:
$db->exec(
array(
'DELETE FROM diet WHERE food=:name',
'INSERT INTO diet (food) VALUES (?)',
'SELECT * FROM diet',
),
array(
array(':name' => 'cola'),
array(1 => 'carrot'),
NULL,
)
);
CRUD (با کلی حالت مختلف)
F3 یه نگاشت کننده ارتباط اشیاء (ORM) داره که بین برنامه شما و داده ها قرار میگیره و نوشتن برنامه هایی که عملیات رایج روی داده ها انجام میدن (مثل ساخت، استخراج، بروزرسانی و حذف - CRUD) رو روی دیتابیسهای SQL و NoSQL خیلی راحت میکنه. نگاشت کننده های داده ها بیشتر کارها رو با نگاشت کردن عملیات روی اشیاء PHP به کوئریهای متناظرشون انجام میدن.
فرض کنید یه دیتابیس MySQL دارین که شامل جدولی از کاربران برنامه شماست (SQLite و PostgreSQL و SQL Server و Sybase هم فرقی ندارن). ممکنه این جدول با کوئری مثل این درست شده باشه:
CREATE TABLE users (
userID VARCHAR(255),
password CHAR(32),
visits INT,
PRIMARY KEY(userID)
);
نکته: MongoDB و NoSQL ذاتاً بدون طرح و الگو (Schema) هستن. F3 خودش یه پیاده سازی داخلی سبک و سریع از NoSQL داره به اسم Jig که از فایلهای معمولی PHP که داده ها رو بصورت Serialize شده یا JSON ذخیره میکنن. این لایه ها نیاز به ساختار داده پیچیده ای ندارن. فیلدها میتونن توی رکوردهای مختلف با هم فرق کنن. حتی میشه هر زمان خواستین، فیلد جدید تعریف کنین یا فیلدی رو حذف کنید.
حالا به SQL برگردیم. اول باید به دیتابیس وصل بشیم:
$db = new DB\SQL(
'mysql:host=localhost;port=3306;dbname=news',
'root',
'ncis'
);
برای استخراج یک رکورد از جدولمون:
$user = new DB\SQL\Mapper($db, 'users');
$user->load(array('userID=?', 'tarzan'));
خط اول یه شئ نگاشت کننده داده میسازه که با جدول users توی دیتابیس ارتباط داره. پشت صحنه، F3 ساختار جدول users رو استخراج میکنه و تشخیص میده که چه فیلد یا فیلدهایی بعنوان کلید اصلی جدول انتخاب شده. در این مرحله، شئ نگاشت کننده (Mapper) هنوز شامل هیچ داد های نیست (بهش میگن وضعیت خشک یا Dry State) و شئ user$ هیچی بجز یه شئ ساختاربندی شده نیست. البته متدهایی داره که برای انجام عملیات اولیه CRUD بعلاوه یکسری امتیازات جانبی که بعداً میبینید، بدرد میخورن. حالا، برای استخراج یک رکورد از جدول users که توی فیلد userID مقدار tarzan رو داره، ما از متد load استفاده میکنیم. به این کار، مرطوب سازی خودکار (Auto Hydrating) شئ نگاشت کننده میگن که اون رو از وضعیت خشک خارج میکنه.
ساده بود، نه؟ F3 میفهمه که یک جدول SQL ساختاری داره که توی موتور پایگاه داده ها ذخیره شده. برخلاف بقیه فریمورکها، F3 نیاز به هیچ تعریف کلاس اضافه ای نداره (مگه اینکه بخواین نگاشت کننده های داده ها رو توسعه بدین تا اشیاء پیچیده تری بسازین). هیچ نگاشتی از فیلدهای شئ به فیلدهای دیتابیس لازم نیست انجام بدین. نیاز به تولیدکننده های کد (Code Generator) نیست و درنتیجه اگه دیتابیس تغییر کرد، نیاز نیست دوباره کد رو تولید کنید. نیاز به فایلهای مضحک XML/YAML برای تنظیم کردن مدلتون ندارین. دستورات پیچیده لازم نیست تا یه رکورد رو استخراج کنید. با F3، تغییر یه فیلد ساده varchar توی جدول SQL شما نیاز به کوچکترین تغییری توی کد برنامه نداره. برای وفاداری به جنبه «جداسازی مفاهیم» توی MVC، مدیران پایگاه داده ها همونقدر روی تغییر ساختار دیتابیس کنترل دارن که یه طراح قالب روی قالبهای HTML/XML کنترل داره.
اگه میخواین با دیتابیسهای NoSQL کار کنین، شباهت دستورات، خیره کننده است. برای مثال به کد نگاشت کننده داده MongoDB دقت کنید:
$db = new DB\Mongo('mongodb://localhost:27017', 'testdb');
$user = new DB\Mongo\Mapper($db, 'users');
$user->load(array('userID' => 'tarzan'));
همینطور به کد Jig نگاه کنید که شبیه موتور قالب F3 هست:
$db = new DB\Jig('db/data/', DB\Jig::FORMAT_JSON);
$user = new DB\Jig\Mapper($db, 'users');
$user->load(array('@userID=?', 'tarzan'));
ORM هوشمند SQL
فریمورک بطور خودکار فیلدهای توی جدول رو به فیلدهای متناظر توی شئ نگاشت کننده نسبت میده. برای مثال، وقتی میگیم:
$user = new DB\SQL\Mapper($db, 'users');
فیلدهای user->userID$ و user->password$ به ترتیب به فیلدهای userID و password توی جدول ما در دیتابیس نگاشت میشن.
شما نمیتونید یک فیلد نگاشت شده رو اضافه یا کم کنید یا ساختار جدول رو با کمک ORM تغییر بدین. برای این کارها باید از خود موتور دیتابیس استفاده کنید. بعد از اینکه تغییرات رو توی دیتابیس انجام دادین، F3 بطور خودکار وقتی برنامه رو اجرا میکنید، ساختار جدید جدول رو با شئ نگاشت داده ها مطابقت میده.
F3 ساختار نگاشت دهنده داده خودش رو از ساختار دیتابیس مشتق میکنه. هیچ کار حدسی وجود نداره. F3 تفاوتهای بین MySQL و SQLite و MSSQL و Sybase و PostgreSQL رو تشخیص میده.
نقل قول:یادآوری: شناسه های SQL (اسامی جدولها، فیلدها و...) نباید کلمات کلیدی انتخاب بشن و باید محدود به کارکترهای حروف الفبای لاتین، اعداد و کارکتر خط زیر باشن. اسامی فیلدهایی که شامل فاصله (Space) یا کارکترهای خاص هستن و توی تعریف ساختار جداول داخل کوتیشن میان، با ORM سازگار نیستن چون نمیتونن به شکل مناسب بعنوان فیلدهای کلاس توی PHP استفاده بشن.
خوب حالا ما میخواین تعداد بازدید کاربر رو افزایش بدیم و رکورد متناظر با اون رو در جدول users تغییر بدیم. برای این کار باید این شکلی کار کنیم:
$user->visits++;
$user->save();
اگه بخوایم یه رکورد جدید درج کنیم، این شکلی کار میکنیم:
$user = new DB\SQL\Mapper($db, 'users');
// or $user = new DB\Mongo\Mapper($db, 'users');
// or $user = new DB\Jig\Mapper($db, 'users');
$user->userID = 'jane';
$user->password = md5('secret');
$user->visits = 0;
$user->save();
همونطور که میبینید، ما از همون متد save استفاده کردیم. اما F3 از کجا میفهمه چه موقع باید یه رکورد جدید درج کنه و چه موقع باید رکورد رو بروزرسانی کنه؟ در زمانی که یک رکورد رو استخراج میکنید، فریمورک وضعیت کلیدهای اصلی رو بررسی میکنه (یا id_ اگه از MongoDB و Jig استفاده کنید) و درنتیجه میفهمه که چه رکوردی باید بروزرسانی یا حذف بشه (حتی اگه کلیدهای اصلی رو تغییر بدین). اما رکوردی که بطور دستی پر شده و از دیتابیس خونده نشده، هیچ مقداری برای کلید اصلی خودش نداره و درنتیجه وقتی ما شئ user$ رو ایجاد کردیم و فیلدهاش رو یکی یکی با مقادیر خودمون پر کردیم (بدون خوندن از دیتابیس)، F3 میفهمه که این یه رکورد جدیده و باید اون رو درج کنه نه بروزرسانی.
یک شئ نگاشت بعد از فراخوانی ()save خالی نمیشه. اگه میخواین یه رکورد جدید ثبت کنید، باید اول شئ نگاشت رو با فراخوانی ()reset تخلیه کنید:
$user->reset();
$user->userID = 'cheetah';
$user->password = md5('unknown');
$user->save();
صدا زدن ()save برای دفعه دوم بدون صدا زدن ()reset باعث میشه رکوردی که در حال حاضر توسط شئ نگاشت بهش اشاره شده، بروزرسانی بشه.
پیش بینی های احتیاطی برای جداول SQL
درسته که تعریف کلید اصلی توی همه جداول دیتابیس یک موضوع منطقیه، اما F3 جلوی استفاده شما از اشیاء نگاشت داده که با جداول فاقد کلید اصلی کار میکنن رو نمیگیره. تنها مشکلی که پیش میاد اینه که شما نمیتونید یک رکورد نگاشت شده رو حذف یا بروزرسانی کنین چون واقعاً برای F3 راهی وجود نداره که بفهمه کدوم رکورد رو دارین بهش اشاره میکنن و ضمناً موقعیتهای مکان یابی (اینکه الان روی چندمین رکورد هستین) قابل اطمینان نیستن. ردیف سطرها رو نمیشه بین موتورهای مختلف SQL انتقال داد و به راه انداز دیتابیس PHP هم ممکنه ارسال نشن.
برای حذف یه رکورد از جدولمون، از متد ()erase استفاده میکنیم. مثال:
$user = new DB\SQL\Mapper($db, 'users');
$user->load(array('userID=? AND password=?', 'cheetah', 'chimp'));
$user->erase();
دستور زبان کوئری Jig هم خیلی شبیهه:
$user = new BD\Jig\Mapper($db, 'users');
$user->load(array('@userID=? AND @password=?', 'cheetah', 'chimp'));
$user->erase();
و همینطور MongoDB :
$user = new DB\Mongo\Mapper($db, 'users');
$user->load(array('userID' => 'cheetah', 'password' => 'chimp'));
$$user->erase();
وضعیت داده های شئ نگاشت کننده
برای اینکه بفهمیم یه نگاشت کننده با داده های معتبری از یک رکورد جدول پر شده یا نه، از متد ()dry استفاده میکنیم:
if($user->dry()) {
echo 'No record matching criteria';
}
فراتر از CRUD
درمورد CRUD صحبت کردیم. یکسری متدهای بیشتر توی نگاشت کننده وجود داره که میتونه مفید باشه:
$user = new DB\SQL\Mapper($db, 'users');
$user->copyFrom('POST');
$user->save();
متد ()copyFrom شئ نگاشت کننده رو با عناصر یه متغیر فریمورک از نوع آرایه پر میکنه. اندیسهای آرایه ای که همنام با فیلدهای شئ (و درنتیجه جدول دیتابیس) باشن، توی فیلدهای متناظرشون قرار میگیرن. درنتیجه وقتی فرم ارسال میشه، اگه عنصری داخلش باشه که اسمش userID باشه، درصورتی که متد با روش POST ارسال بشه، توی متغیر ['POST['userID_$ در PHP میشه که معادل متغیر POST.userID توی F3 هست و درنتیجه توی فیلد user->userID$ میشه و بعد از ذخیره شدن، توی فیلد userID رکورد جدید در دیتابیس ثبت میشه.
نقل قول:خطر: بطور پیشفرض copyFrom تمام آرایه ارائه شده رو تحت پوشش قرار میده. درنتیجه ممکنه درصورتی که کاربر یکسری فیلدهای اضافه که توی دیتابیس شما هست ولی شما توی فرم نگذاشته بودین رو با ابزارهایی مثل FireBug یا Developer Tools (یا حتی با ساخت یه فرم دیگه) ارسال کنه، یه حفره امنیتی توی برنامه شما پیش بیاد. از پارامتر دوم متد copyFrom برای ساخت یه فیلتر برای خلاص شدن از فیلدهای ناخواسته استفاده کنید. برای توضیحات بیشتر مستندات متد copyFrom رو ببینید.
ازطرف دیگه ممکنه یه جاهایی بخوایم فیلدهای دیتابیس رو توی یه متغیر F3 از نوع آرایه قرار بدیم تا بعداً (فرضاً توی قالب) بخوایم ازش استفاده کنیم:
$user = new DB\SQL\Mapper($db, 'users');
$user->load(array('userID=?', 'jane'));
$user->copyTo('user');
حالا میتونیم توی قالبمون از توکن {{user.userID@ }} استفاده کنیم. مثال:
<input type="text" name="userID" value="{{ @user.userID }}" />
متدهای save و update و copyFrom و نسخه های پارامتری load و erase درمقابل SQL Injection امن هستن.
پیمایش و صفحه بندی
بطور پیشفرض متد load فقط اولین رکوردی رو که با شرایط موردنظر مطابقت داشته باشه میخونه. اگه بیشتر از یک رکورد لازم دارین، میتونین از متد skip استفاده کنید تا توی رکوردهایی که با شرایط تعیین شده مطابقت دارن، حرکت کنین:
$user = new DB\SQL\Mapper($db, 'users');
$user->load(array('visits > ?', 3));
// For MongoDB users:
// $user = new DB\Mongo\Mapper($db, 'users');
// $user->load(array('visits' => array('$gt' => 3)));
// If you prefer Jig:
// $user = new DB\Jig\Mapper($db, 'users');
// $user->load('@visits>?', 3);
// Display the userID of the first record that matches the criteria
echo $user->userID;
// Go to the next record that matches the same criteria
$user->skip(); // Same as $user->skip(1);
// Back to the previous record
$user->skip(-1);
// Move three records forward
$user->skip(3);
ممکنه بخواین از ()user->next$ بعنوان جایگزین ()user->skip$ و از متد ()user->prev$ بجای (1-)user->skip$ استفاده کنین.
میتونید از متد dry برای بررسی اینکه از محدوده خارج نشدین استفاده کنین. مثال برای نمایش تمام userID ها:
$user->load(array('visits>?', 3));
while(!$user->dry()) {
echo '<p>' . $user->userID . '</p>' . PHP_EOL;
$user->next();
}
متد load یه پارامتر دوم هم داره که اختیاریه: آرایه از انتخابها که بصورت زوج کلید/مقدار هست:
$user->load(
array('visits>?', 3),
array(
'order' => 'userID DESC',
'offset' => 5,
'limit' => 3,
)
);
این دستور، کوئری زیر رو اجرا میکنه:
SELECT * FROM users
WHERE visits>3
ORDER BY userID DESC
LIMIT 3 OFFSET 5;
این یکی از راههای نمایش اطلاعات در قسمتهای کوچک محسوب میشه. یکی دیگه از راهها صفحه بندی نتایجه:
$page = $user->paginate(2, 5, array('visits>?', 3));
توی سناریوی بالا، F3 رکوردهایی رو که با شرط visits>3 تطبیق دارن پیدا میکنه. بعد نتایج رو به 5 رکورد (در صفحه) تقسیم میکنه و رکوردهای مربوط به صفحه 2 (با شروع صفحات از 0) رو برمیگردونه. خروجی F3 که توی page$ قرار میگیره، یه آرایه شامل عناصر زیره:
[subset] آرایه ای از اشیاء نگاشت کننده که با شرایط مطابقت داشتن
[total] تعداد کل رکوردهای تمام صفحات
[limit] اندازه صفحه که در اینجا 5 هست
[count] تعداد صفحات
[pos] موقعیت واقعی زیرمجموعه
موقعیت واقعی زیرمجموعه اگه پارامتر اول paginate منفی باشه یا از تعداد کل صفحات بیشتر باشه، NULL خواهد بود.
فیلدهای مجازی
گاهی وقتا میخواین نتیجه محاسبه شده یه فیلد یا یه مقدار استفاده شده از یه جدول دیگه رو برگردونین. اینجور وقتها فیلدهای مجازی به کار میان. ORM کوچک SQL به شما این اجازه رو میده که روی داده های دریافتی از فیلدهای موجود کار کنید. فرض کنید جدول زیر رو داریم:
CREATE TABLE products (
productID VARCHAR(255),
description TEXT,
supplierID VARCHAR(255),
unitprice DECIMAL(10, 2),
quantity INT,
PRIMARY KEY (productID)
);
دقت کنید که فیلد totalPrice وجود نداره ولی ما میتونیم به فریمورک بگیم تا از موتور دیتابیس بخواد اون رو با کمک فیلدهای موجود حساب کنه و به ما تحویل بده:
$item = new DB\SQL\Mapper($db, 'products');
$item->totalPrice = 'unitprice*quantity';
$item->load(array('productID=:pid', ':pid' => 'apple'));
echo $item->totalPrice;
کد بالا یه فیلد مجازی به اسم totalPrice تعریف میکنه که با ضرب unitprice در quantity محاسبه میشه. نگاشت کننده SQL فرمول/قانون رو ذخیره میکنه و درنتیجه وقتی میخوایم اطلاعات رو از دیتابیس بگیریم، میتونیم از فیلدمجازی خودمون مثل یه فیلد نگاشت شده معمولی استفاده کنیم.
میتونید فیلدهای مجازی پیچیده تری هم داشته باشین:
$item->mostNumber = 'MAX(quantity)';
$item->load(();
echo $item->mostNumber;
ایندفعه، F3 محصولی که بیشترین تعداد رو داره میخونه. دقت کنید که متد ()load هیچ شرطی تعریف نکرده، درنتیجه تمام رکوردهای دیتابیس پردازش میشن. البته فیلد مجازی mostNumber در کنار شرایطی که برای load مشخص میکنید، به شما اجازه میده که بیشترین تعداد محصولاتی که شرایط خاصی دارن و توی گروه مشخصی قرار میگیرن رو هم پیدا کنید.
حتی میتونید یک مقدار رو از جدول دیگه بخونید:
$item->supplierName = 'SELECT name FROM suppliers WHERE products.supplierID = suppliers.supplierID';
$item->load();
echo $item->supplierName;
هربار که یک رکورد رو از جدول proucts بارگذاری کنید، ORM فیلد supplierID رو از این جدول با فیلد supplierID از جدول suppliers مطابقت میده و فیلد name اون رو استخراج میکنه و توی فیلد مجازی supplierName میگذاره.
برای حذف یه فیلد مجازی از دستور unset خود PHP استفاده کنید:
unset($item->totalPrice);
دستور isset رو هم میتونید برای چک کردن اینکه یه فیلد مجازی تعریف شده یا نه، استفاده کنید:
if(isset($item->totalPrice)) {
echo $item->totalPrice;
}
دقت کنید که فیلدهای مجازی باید قبل از استخراج اطلاعات تعریف بشن. ORM محاسبه اصلی و استخراج از جداول دیگه رو انجام نمیده و این کار، وظیفه موتور پایگاه داده است که تمام این کارهای سخت رو انجام بده.
جوینده، یابنده است
اگه نیاز به حرکت رکورد به رکورد بین نتایج ندارین، میتونید کل رکوردها رو با یه شلیک بدست بیارین:
$frequentUsers = $user->find(array('visits>?', 3), array('order' => 'userID'));
معادل Jig کد بالا:
$frequentUsers = $user->find(array('@visits>?', 3), array('order' => 'userID'));
و نسخه MongoDB :
$frequentUsers = $user->find(array('visits' => array('$gt' => 3)), array('userID' => 1));
متد ()find جدول users رو جستجو میکنه تا رکوردهایی که با شرایط مطابقت دارن رو پیدا کنه. نتایج رو براساس فیلد userID مرتب میکنه و نتیجه رو بصورت یه آایه از اشیاء نگاشت کننده برمیگردونه. پس دقت کنید که ('find('visits>3 با دستور ('load('visits>3 فرق میکنه. اولی یه آرایه از تمام رکوردهای پیدا شده رو برمیگردونه و دومی، یک ارجاع به اولین رکوردی هست که با شرایط تطبیق داره. متد find تأثیری روی skip نداره.
نقل قول:مهم: تعریف یک شرط خالی، NULL یا رشته با طول صفر بعنوان پارامتر اول متدهای find و load باعث استخراج تمام رکوردها میشه. مطمئن باشین که میدونین چیکار میکنین وگرنه ممکنه از محدودیت حافظه (memory_limit) تعیین شده توسط PHP توی جداول بزرگ تجاوز کنین.
دستور زبان متد find این شکلیه:
find(
$criteria,
array(
'group' => 'foo',
'order' => 'foo,bar',
'limit' => 5,
'offset' => 0,
)
);
متد find یه آرایه از اشیاء برمیگردونه. هر شئ، یه نگاشت کننده به رکوردی هست که با شرط موردنظر مطابقت داشته:
$place = new DB\SQL\Mapper($db, 'places');
$list = $place->find("state='New York'");
foreach($list as $obj) {
echo $obj->city . ', ' . $obj->country;
}
اگه میخواین شئ نگاشت کننده رو به یه آرایه معمولی تبدیل کنین، از متد cast استفاده کنید:
$array = $place->cast();
echo $array['city'] . ', ' . $array['country'];
برای اینکه بدونید چند رکورد توی جدول با شرایط شما مطابقت داره، از متد count استفاده کنید:
if(!$user->count(array('visits>?', 10)) {
echo 'We need a better ad plan!';
}
یه متد دیگه هم هست به اسم select که شبیه find عمل میکنه (ازنظر خروجی) ولی کنترل بیشتری روی فیلدهای بازگشتی به شما میده و ساختارش شبیه SQL هست:
select(
'foo, bar, MIN(baz) AS lowest',
array('foo > ?', 3),
array(
'group' => 'foo, bar',
'order' => 'baz',
'limit' => 5,
'offset' => 3,
)
);
متدهای find و select محتوای شئ نگاشت رو تغییر نمیدن. پس صدا زدن متد dry برای اینکه بفهمیم چیزی توسط این متدها پیدا شده یا نه، معنا نداره. اگه رکوردی پیدا نشه، این متدها یه آرایه خالی برمیگردونن.
نقل قول:به خاطر بسپارین:
متد load شئ نگاشت کننده جاری رو پر میکنه درحالی که find آرایه ای از اشیاء نگاشت کننده پر شده، برمیگردونه.
گزارش گیری
اگه خواستین بفهمین چه دستورات SQL خاصی مستقیماً از برنامه شما (یا غیر مستقیم ازطریق اشیاء نگاشت کننده) اجرا شده تا فرضاً گلوگاههای برنامه و جاهایی که کارآیی کم میشه رو پیدا کنین، میتونید از دستور ساده زیر استفاده کنید:
echo $db->log();
F3 جریان تمام دستوراتی که به پایگاه داده SQL ارسال شده رو همراه با زمانی که اجرای هرکدوم طول کشیده رو نگهداری میکنه. این دقیقاً همون اطلاعاتیه که شما برای دستکاری کارآیی برنامه بهش نیاز دارین.
یه وقتهایی اینها کافی نیست
در اکثر مواقع، میتونید با امکانات راحتی که متدهای نگاشت کننده داده ها در اختیارتون گذاشته، زندگی راحتی داشته باشین. اگه خواستین فریمورک یکسری کارهای سنگینتر انجام بده، میتونید کلاس نگاشت کننده رو با مشتق کردن کلاسهای خودتون از اون و اضافه کردن متدهای بیشتر، گسترش بدین. البته این کار مستلزم اینه که دستهاتون رو به یکسری کدهای SQL سطح پایین آلوده کنید:
class Vendor extends DB\SQL\Mapper
{
// Instantiate mapper
function __cunstruct(DB\SQL $db)
{
// This is where the mapper and DB structure synchronization occurs
parent::__construct($db, 'vendors');
}
// Specialized query
function listByCity()
{
return $this->select('vendorID,name,city', null, array('order' => 'city DESC'));
/*
We could have done thesame thing with plain SQL:
return $this->db->exec('SELECT vendorID,name,city FROM vendors ORDER BY city DESC;');
*/
}
}
$vendor = new Vendor;
$vendor->listByCity();
توسعه نگاشت کننده های داده به این شکل، راه ساده ای برای ساخت مدلهای دیتابیس اختصاصی برنامه خودتونه.
مزایا و معایب
اگه با SQL آشنایی دارین، شاید بگین هر چیزی توی ORM رو میشه با کوئریهای سنتی SQL انجام داد. قطعاً همینطوره. میتونیم بدون شنونده های رویداد (Event Listeners) اضافه و با کمک ماشه های دیتابیس (DB Triggers) و روال های ذخیره شده (Stored Procedures) کارهامون رو انجام بدیم. ORM فقط یه سربار غیر ضروریه. اما نکته اینجاست که نگاشت کننده های داده به شما قابلیتهای اضافه تری بخاطر استفاده از اشیاء برای معادلسازی موجودیتهای دیستابیس میدن. بعنوان یه برنامه نویس، میتونید کدتون رو سریعتر بنویسید و خلاقانه کار کنید. برنامه نهایی اگه کوتاهتر نباشه، قطعاً تمیزتر خواهد بود. اما باید یه تعادل بین مزایای ORM و سرعت برقرار کنید. بخصوص وقتی که پای مدیریت پایگاه داده های پیچیده و بزرگ میاد وسط. به خاطر بسپارین که همه ORMها - فرقی نمیکنه که چقدر ساده یا پیچیده باشن - چیزی بیشتر از یه لایه دسترسی دیگه نیستن. اونها کماکان باید کار رو به لایه زیرین موتور SQL پاس بدن.
توی طراحی ORM فریمورک F3 متدهایی برای اتصال مستقیم اشیاء به هم (یعنی Join های SQL) ارائه نشده، چون این کار مثل بازکردن قوطی کرمهاست! این کار باعث میشه برنامه شما پیچیده تر از اون چیزی بشه که باید باشه و مواردی مثل بارگذاری اشیاء به روش کند (Lazy - بارگذاری اطلاعات جدول وابسته در لحظه نیاز) یا حریصانه (Eager - بارگذاری اطلاعات جدول وابسته در لحظه استخراج اطلاعات جدول اصلی) هم وجود داره که گاهی اوقات به بن بست منجر میشه و یا حتی ممکنه هماهنگی و تطابق اطلاعات توی اشیاء نگاشت کننده با اطلاعات اصلی دیتابیس بهم بریزه. راههای غیر مستقیم برای رسیدن به این موارد ازطریق نگاشت کننده های SQL وجود داره (استفاده از فیلدهای مجازی)، ولی شما باید خودتون با قبول مسئولیت ریسکش، براش برنامه نویسی کنید.
اگه میخواین مفاهیم «خالص» شئ گرایی رو توی برنامه خودتون پیاده سازی کنین تا همه داده ها رو در بر بگیرن (چون هر چیزی یه شئ محسوب میشه)، فراموش نکنید که داده ها تقریباً همیشه بیشتر از خود برنامه عمر میکنن. برنامه شما ممکنه خیلی قبل از اینکه اطلاعات شما از حافظه پاک بشه، تمام و از حافظه خارج شده باشه. بی دلیل یه لایه دیگه از پیچیدگی به برنامه خودتون با کمک اشیاء در هم پیچیده شده و کلاسهایی که خیلی از Schema و ساختار فیزیکی دیتابیس فاصله گردن، اضافه نکنید.
قبل از اینکه اشیاء مختلف رو در هم ادغام کنید تا جداول پشت صحنه دیتابیس رو دستکاری کنید، درمورد این موضوع فکر کنید: ساخت صفحات نمایش برای نشون دادن ارتباطها و رویدادها برای تعریف رفتار اشیاء توی موتور پایگاه داده ها کارآمدتره. دیتابیسهای رابطه ای برای مدیریت کردن جداول موقت و نمایشی، جداول ادغام شده و رویدادها طراحی شدن. اونها مخازن داده کودن و ابلهی نیستن. جداولی که توی یک جدول موقت ادغام میشن، ازنظر برنامه مثل یک جدول مجزا دیده میشن و F3 میتونه بطور خودکار یک جدول موقت رو مثل یک جدول معمولی نگاشت کنه. معادلسازی Joinها بعنوان اشیاء رابطه ای توی PHP کندتر از کد ماشین، جبر رابطه ای و منطق بهینه سازی موتور پایگاه داده هاست. بعلاوه، ادغام جداول بصورت تکراری توی برنامه شما (نه توی دیتابیس) میتونه علامت این باشه که طراحی پایگاه داده های شما نیازمند بازبینی و اصلاحه و جداول موقت جزء اساسی استخراج داده ها محسوب میشن. اگه یه جدول اطلاعاتی رو از یه جدول دیگه مرتب بخونه، بهتره با کمک یه جدول موقت ساختار دیتابیس رو نرمالسازی کنید. بعد یه شئ نگاشت کننده بسازین که از روی اون جدول موقت درست شده. این کار هم سریعتره و هم زحمت کمتری لازم داره.
این دستور رو ببینید که یه جدول موقت توی موتور دیتابیس شما میسازه:
CREATE VIEW combined AS
SELECT
projects.project_id AS project,
users.name AS name
FROM projects
LEFT OUTER JOIN USERS ON
projects.project_id=users.project_id AND
projects.user_id=users.user_id;
کد برنامه شما خیلی ساده میشه چون نیاز به مدیریت دو شئ نگاشت (یکی برای پروژه ها و یکی دیگه برای کاربران) اونهم فقط برای بدست آوردن اطلاعات از دو جدول ادغام شده، نخواهد داشت:
$combined = new DB\SQL\Mapper($db, 'combined');
$combined->load(array('project=?', 123));
echo $combined->name;
ترفند: از ابزارها به همون شکلی که براش طراحی شدن استفاده کنید. F3 همین الان هم یه کلاس کمکی SQL داره. اگه نیاز به یه چکش بزرگتر داشتین، از اون استفاده کنید
سعی کنید یه تعادل بین راحتی و کارآیی پیدا کنید. SQL همیشه نقطه بازگشت شما در مواقعی هست که روی ساختارهای پیچیده و قدیمی داده ها کار میکنید.