فصل ۵- پایگاه داده ها و Eloquent
پایگاه داده ها و Eloquent
لاراول مجموعهای از ابزارها را برای تعامل با پایگاهدادههای برنامهتان فراهم میکند، اما مهمترین آنها Eloquent است، ORM (نگاشت شیء به رابطهای) فعال لاراول.
Eloquent یکی از محبوبترین و تأثیرگذارترین ویژگیهای لاراول است. این یک مثال عالی از تفاوت لاراول با اکثر فریمورکهای PHP است؛ در دنیای ORMهای DataMapper که قدرتمند اما پیچیده هستند، Eloquent به دلیل سادگی خود برجسته است. برای هر جدول یک کلاس وجود دارد که مسئول بازیابی، نمایش و ذخیرهسازی دادهها در آن جدول است.
با این حال، چه شما از Eloquent استفاده کنید یا نه، همچنان از دیگر ابزارهای پایگاهدادهای که لاراول فراهم میکند، بهره زیادی خواهید برد. بنابراین، قبل از اینکه به سراغ Eloquent برویم، ابتدا اصول عملکرد پایگاهداده لاراول را پوشش میدهیم: مهاجرتها، Seederها و سازندهی کوئری.
سپس به Eloquent میپردازیم: تعریف مدلها، درج، بهروزرسانی و حذف دادهها، سفارشیسازی پاسخها با استفاده از Accessors، Mutators و Type Casting، و در نهایت روابط. اینجا خیلی اتفاقات میافتد و ممکن است احساس سردرگمی کنید، اما اگر قدم به قدم پیش برویم، از پس آن برمیآییم.
پیکربندی
قبل از اینکه به استفاده از ابزارهای پایگاهداده لاراول بپردازیم، یک لحظه توقف کنیم و نحوه پیکربندی اطلاعات و ارتباطات پایگاهدادهتان را بررسی کنیم.
پیکربندی دسترسی به پایگاهداده در فایل config/database.php و .env قرار دارد. مانند بسیاری از دیگر بخشهای پیکربندی در لاراول، شما میتوانید چندین "اتصال" تعریف کنید و سپس تصمیم بگیرید که کدامیک به طور پیشفرض توسط کد استفاده شود.
اتصالات پایگاه داده
به طور پیشفرض، برای هر کدام از درایورها یک اتصال وجود دارد، همانطور که در مثال ۵-۱ میبینید.
مثال ۵-۱. فهرست پیشفرض کانکشنهای پایگاه داده
'connections' => [
'sqlite' => [
'driver' => 'sqlite',
'url' => env('DATABASE_URL'),
'database' => env('DB_DATABASE', database_path('database.sqlite')),
'prefix' => '',
'foreign_key_constraints' => env('DB_FOREIGN_KEYS', true),
],
'mysql' => [
'driver' => 'mysql',
'url' => env('DATABASE_URL'),
'host' => env('DB_HOST', '127.0.0.1'),
'port' => env('DB_PORT', '3306'),
'database' => env('DB_DATABASE', 'forge'),
'username' => env('DB_USERNAME', 'forge'),
'password' => env('DB_PASSWORD', ''),
'unix_socket' => env('DB_SOCKET', ''),
'charset' => 'utf8mb4',
'collation' => 'utf8mb4_unicode_ci',
'prefix' => '',
'prefix_indexes' => true,
'strict' => true,
'engine' => null,
'options' => extension_loaded('pdo_mysql') ? array_filter([
PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'),
]) : [],
],
'pgsql' => [
'driver' => 'pgsql',
'url' => env('DATABASE_URL'),
'host' => env('DB_HOST', '127.0.0.1'),
'port' => env('DB_PORT', '5432'),
'database' => env('DB_DATABASE', 'forge'),
'username' => env('DB_USERNAME', 'forge'),
'password' => env('DB_PASSWORD', ''),
'charset' => 'utf8',
'prefix' => '',
'prefix_indexes' => true,
'search_path' => 'public',
'sslmode' => 'prefer',
],
'sqlsrv' => [
'driver' => 'sqlsrv',
'url' => env('DATABASE_URL'),
'host' => env('DB_HOST', 'localhost'),
'port' => env('DB_PORT', '1433'),
'database' => env('DB_DATABASE', 'forge'),
'username' => env('DB_USERNAME', 'forge'),
'password' => env('DB_PASSWORD', ''),
'charset' => 'utf8',
'prefix' => '',
'prefix_indexes' => true,
// 'encrypt' => env('DB_ENCRYPT', 'yes'),
// 'trust_server_certificate' => env('DB_TRUST_SERVER_CERTIFICATE', 'false'),
],
]
هیچ چیزی شما را از حذف یا تغییر این اتصالات با نامهای مشخص یا ایجاد اتصالات خودتان باز نمیدارد. شما میتوانید اتصالات جدید با نامهای دلخواه ایجاد کنید و قادر خواهید بود درایورهای مختلف (مانند MySQL، Postgres و غیره) را در آنها تنظیم کنید. بنابراین، در حالی که به طور پیشفرض یک اتصال برای هر درایور وجود دارد، این یک محدودیت نیست؛ شما میتوانید پنج اتصال مختلف، همه با درایور MySQL، ایجاد کنید، اگر بخواهید.
هر اتصال به شما این امکان را میدهد که ویژگیهای لازم برای اتصال به هر نوع اتصال و سفارشیسازی آن را تعریف کنید.
چند دلیل برای استفاده از چندین درایور وجود دارد. ابتدا، بخش "اتصالات" که به طور پیشفرض ارائه میشود، یک الگوی ساده است که راهاندازی برنامههایی که از هر یک از انواع اتصالات پایگاهداده پشتیبانی شده استفاده میکنند را آسان میکند. در بسیاری از برنامهها، شما میتوانید اتصال پایگاهدادهای را که قصد دارید استفاده کنید انتخاب کرده و اطلاعات آن را پر کنید، و حتی اگر بخواهید، اتصالات دیگر را حذف کنید. من معمولاً همه آنها را نگه میدارم، به این دلیل که شاید در آینده بخواهم از آنها استفاده کنم.
اما مواردی هم وجود دارد که شما ممکن است نیاز به اتصالات مختلف در یک برنامه داشته باشید. برای مثال، ممکن است از اتصالات پایگاهداده مختلف برای دو نوع داده مختلف استفاده کنید، یا ممکن است از یک پایگاهداده برای خواندن دادهها و از دیگری برای نوشتن استفاده کنید. پشتیبانی از اتصالات متعدد این امکان را فراهم میکند.
تنظیمات URL
اغلب سرویسهایی مانند Heroku یک متغیر محیطی با یک URL فراهم میکنند که تمام اطلاعات لازم برای اتصال به پایگاهداده را در خود دارد. این URL به شکل زیر خواهد بود:
mysql://root:password@127.0.0.1/forge?charset=UTF-8
شما نیازی به نوشتن کدی برای تجزیه این متغیر ندارید؛ به جای آن، کافی است آن را به عنوان متغیر محیطی DATABASE_URL ارسال کنید (یا گزینه تنظیمات config(connections.mysql.url) را به یک متغیر محیطی دیگر تخصیص دهید) و لاراول این URL را برای شما تجزیه میکند.
سایر گزینه های پیکربندی پایگاه داده
بخش پیکربندی config/database.php چندین گزینه پیکربندی دیگر دارد. شما میتوانید دسترسی به Redis را پیکربندی کنید، نام جدول مورد استفاده برای مهاجرتها را سفارشیسازی کنید، اتصال پیشفرض را تعیین کنید و تنظیم کنید که آیا فراخوانیهای غیر Eloquent نمونههای stdClass یا آرایهها را باز میگردانند.
با هر سرویسی در لاراول که از منابع مختلف اتصال پشتیبانی میکند—برای مثال، ممکن است سشنها از پایگاهداده یا ذخیرهسازی فایل پشتیبانی کنند، کش میتواند از Redis یا Memcached استفاده کند، و پایگاهدادهها میتوانند از MySQL یا PostgreSQL استفاده کنند—شما میتوانید اتصالات مختلفی را تعریف کرده و همچنین انتخاب کنید که یک اتصال خاص به عنوان "پیشفرض" باشد، به این معنی که هرگاه شما به طور مشخص درخواست اتصال خاصی نکنید، از آن استفاده خواهد شد. در اینجا نحوه درخواست یک اتصال خاص آورده شده است، اگر بخواهید:
$users = DB::connection('secondary')->select('select * from users');
مهاجرت ها
تعریف مهاجرت ها
مهاجرت یک فایل واحد است که دو چیز را تعریف میکند: تغییراتی که هنگام اجرای مهاجرت با دستور up اعمال میشود و به طور اختیاری، تغییراتی که هنگام اجرای مهاجرت با دستور down اعمال میشود.
"Up" و "Down" در مهاجرتها مهاجرتها همیشه به ترتیب تاریخ اجرا میشوند. هر فایل مهاجرت نامی مشابه با این دارد: 2018_10_12_000000_create_users_table.php. زمانی که یک سیستم جدید مهاجرت میشود، سیستم هر مهاجرت را ازآخرین تاریخ گرفته و متد up() آن را اجرا میکند—در این مرحله شما سیستم را "بالا میبرید". اما سیستم مهاجرت همچنین به شما این امکان را میدهد که آخرین مجموعه مهاجرتهای خود را "بازگشت" دهید. سیستم هر کدام را گرفته و متد down() آن را اجرا میکند، که باید تغییراتی که متد up انجام داده را معکوس کند. بنابراین، متد up() یک مهاجرت باید "مهاجرت" را انجام دهد و متد down() باید آن را "برگرداند". |
مثال 5-2 نشان میدهد که مهاجرت پیشفرض "ایجاد جدول کاربران" که همراه با لاراول میآید، چگونه به نظر میرسد.
مثال ۵-۲. مایگریشن پیشفرض لاراول برای "ایجاد جدول کاربران"
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up(): void
{
Schema::create('users', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('email')->unique();
$table->timestamp('email_verified_at')->nullable();
$table->string('password');
$table->rememberToken();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down(): void
{
Schema::dropIfExists('users');
}
};
تایید ایمیل ستون email_verified_at یک timestamp ذخیره میکند که زمان تأیید ایمیل کاربر را نشان میدهد. |
همانطور که مشاهده میکنید، ما یک متد up() و یک متد down() داریم. متد up() به مهاجرت میگوید که یک جدول جدید به نام users با چند فیلد ایجاد کند و متد down() به آن میگوید که جدول users را حذف کند.
ایجاد یک مهاجرت
همانطور که در فصل 8 خواهید دید، لاراول مجموعهای از ابزارهای خط فرمان را فراهم میکند که میتوانید برای تعامل با برنامه خود و ایجاد فایلهای استاندارد استفاده کنید. یکی از این دستورات به شما این امکان را میدهد که یک فایل مهاجرت ایجاد کنید. شما میتوانید این دستور را با استفاده از php artisan make:migration اجرا کنید و یک پارامتر واحد که نام مهاجرت است را وارد کنید. به عنوان مثال، برای ایجاد جدولی که به تازگی پوشش دادیم، باید دستور php artisan make:migration create_users_table را اجرا کنید.
دو گزینه وجود دارد که میتوانید به صورت اختیاری به این دستور اضافه کنید. --create=table_name مهاجرت را با کدی که برای ایجاد جدولی به نام table_name طراحی شده است، پیشفرض میکند و --table=table_name مهاجرت را برای اصلاحات جدول موجود پر میکند.
php artisan make:migration create_users_table
php artisan make:migration add_votes_to_users_table --table=users
php artisan make:migration create_users_table --create=users
ایجاد جداول
ما قبلاً در مهاجرت پیشفرض create_users_table دیدیم که مهاجرتهای ما به فساد Schema و متدهای آن بستگی دارند. هر چیزی که در این مهاجرتها انجام میدهیم، به متدهای Schema وابسته است.
برای ایجاد یک جدول جدید در یک مهاجرت، از متد create() استفاده کنید—پارامتر اول نام جدول است و پارامتر دوم یک closure است که ستونهای آن را تعریف میکند:
Schema::create('users', function (Blueprint $table) {
// Create columns here
});
ایجاد ستونها
برای ایجاد ستونهای جدید در یک جدول، چه در فراخوانی create table و چه در فراخوانی modify table، از نمونهای از Blueprint که به closure شما ارسال میشود، استفاده کنید:
Schema::create('users', function (Blueprint $table) {
$table->string('name');
});
بیایید به متدهای مختلف موجود در نمونههای Blueprint برای ایجاد ستونها نگاه کنیم. من توضیح میدهم که چگونه در MySQL کار میکنند، اما اگر از دیتابیس دیگری استفاده میکنید، لاراول نزدیکترین معادل را استفاده خواهد کرد.
متدهای ساده فیلد در Blueprint به شرح زیر هستند:
id()
این یک معادل برای $table->bigIncrements(id) است.
integer(colName), tinyInteger(colName), smallInteger(colName), mediumInteger(colName), bigInteger(colName), unsignedTinyInteger(colName), unsignedSmallInteger(colName), unsignedMediumInteger(colName), unsignedBigInteger(colName)
یک ستون از نوع INTEGER یا یکی از تغییرات آن اضافه میکند.
string(colName, length)
یک ستون از نوع VARCHAR با طول اختیاری اضافه میکند.
binary(colName)
یک ستون از نوع BLOB اضافه میکند.
boolean(colName)
یک ستون از نوع BOOLEAN اضافه میکند (در MySQL یک TINYINT(1)).
char(colName, length)
یک ستون از نوع CHAR با طول اختیاری اضافه میکند.
date(colName), datetime(colName), dateTimeTz(colName)
یک ستون از نوع DATE یا DATETIME اضافه میکند؛ هنگامی که آگاهی از زمان منطقهای نیاز باشد، متد dateTimeTz() یک ستون DATETIME با منطقه زمانی ایجاد میکند.
decimal(colName, precision, scale), unsignedDecimal(colName, precision, scale)
یک ستون از نوع DECIMAL با دقت و مقیاس مشخص میکند—به عنوان مثال، decimal('amount', 5, 2) دقت 5 و مقیاس 2 را مشخص میکند؛ برای یک ستون بدون علامت، از متد unsignedDecimal استفاده کنید.
double(colName, total digits, digits after decimal)
یک ستون از نوع DOUBLE اضافه میکند—به عنوان مثال، double('tolerance', 12, 8) مشخص میکند که 12 رقم طول دارد که 8 رقم آن در سمت راست اعشار است، مانند 7204.05691739.
enum(colName, [choiceOne, choiceTwo])
یک ستون از نوع ENUM با انتخابهای دادهشده اضافه میکند.
float(colName, precision, scale)
یک ستون از نوع FLOAT اضافه میکند (معادل double در MySQL).
foreignId(colName), foreignUuid(colName)
یک ستون از نوع UNSIGNED BIGINT یا UUID با انتخابهای دادهشده اضافه میکند.
foreignIdFor(colName)
یک ستون از نوع UNSIGNED BIG INT با نام colName_id اضافه میکند.
geometry(colName), geometryCollection(colName)
یک ستون از نوع GEOMETRY یا GEOMETRYCOLLECTION اضافه میکند.
ipAddress(colName)
یک ستون از نوع VARCHAR اضافه میکند.
json(colName) and jsonb(colName)
یک ستون از نوع JSON یا JSONB اضافه میکند.
lineString(colName), multiLineString(colName)
یک ستون از نوع LINESTRING یا MULTILINESTRING با نام دادهشده اضافه میکند.
text(colName), tinyText(colName), mediumText(colName), longText(colName)
یک ستون از نوع TEXT (یا اندازههای مختلف آن) اضافه میکند.
macAddress(colName)
یک ستون از نوع MACADDRESS در دیتابیسهایی که از آن پشتیبانی میکنند (مانند PostgreSQL) اضافه میکند؛ در سایر سیستمهای دیتابیس، معادل رشتهای آن را ایجاد میکند.
multiPoint(colName), multiPolygon(colName), polygon(colName), point(colName)
این متدها به ترتیب ستونهایی از انواع MULTIPOINT، MULTIPOLYGON، POLYGON و POINT اضافه میکنند.
set(colName, membersArray)
یک ستون از نوع SET با نام colName و اعضای مشخصشده در membersArray ایجاد میکند.
time(colName, precision), timeTz(colName, precision)
یک ستون از نوع TIME با نام colName اضافه میکند؛ برای آگاهی از منطقه زمانی از متد timeTz() استفاده کنید.
timestamp(colName, precision), timestampTz(colName, precision)
یک ستون از نوع TIMESTAMP اضافه میکند؛ برای آگاهی از منطقه زمانی، از متد timestampTz() استفاده کنید.
uuid(colName)
یک ستون از نوع UUID (CHAR(36) در MySQL) اضافه میکند.
year()
یک ستون از نوع YEAR اضافه میکند.
و اینها متدهای ویژه (پیوسته) Blueprint هستند:
increments(colName)، tinyIncrements(colName)، smallIncrements(colName)، mediumIncrements(colName) و bigIncrements(colName)
یک کلید اصلی INTEGER افزایشی بدون علامت اضافه میکند، یا یکی از تغییرات آن
timestamps(precision)، nullableTimestamps(precision) و timestampsTz(precision)
ستونهای created_at و updated_at از نوع timestamp را با دقت اختیاری، نسخههای قابلقبول nullable و آگاه از منطقه زمانی اضافه میکند
rememberToken()
ستون remember_token (VARCHAR(100)) برای توکنهای "مرا به خاطر بسپار" کاربر اضافه میکند
softDeletes(colName, precision)، softDeletsTz(colName, precision)
یک timestamp به نام deleted_at برای استفاده در حذفهای نرم با دقت اختیاری، و نسخههای آگاه از منطقه زمانی اضافه میکند
morphs(colName)، nullableMorphs(colName)، uuidMorphs(relationshipName)، nullableUuidMorphs(relationshipName)
برای colName داده شده، یک colName_id از نوع عدد صحیح و یک colName_type از نوع رشته اضافه میکند (مثلاً morphs(tag) یک tag_id عدد صحیح و یک tag_type رشته اضافه میکند)؛ برای استفاده در روابط پلیمورفیک، با استفاده از idها یا uuidها، و میتواند طبق نام متد nullable باش
ساخت ویژگیهای اضافی به صورت زنجیرهای
بیشتر ویژگیهای تعریف یک فیلد—به عنوان مثال طول آن—به عنوان پارامتر دوم متد ساخت فیلد تنظیم میشوند، همانطور که در بخش قبلی دیدیم. اما چند ویژگی دیگر وجود دارد که آنها را با زنجیرهای از فراخوانی متدها پس از ایجاد ستون تنظیم خواهیم کرد. به عنوان مثال، این فیلد ایمیل nullable است و در MySQL درست بعد از فیلد last_name قرار خواهد گرفت:
Schema::table('users', function (Blueprint $table) {
$table->string('email')->nullable()->after('last_name');
});
متدهای زیر برخی از متدهایی هستند که برای تنظیم ویژگیهای اضافی یک فیلد استفاده میشوند؛ برای فهرستی کامل به مستندات مهاجرت ها مراجعه کنید.
nullable()
اجازه میدهد مقادیر NULL در این ستون وارد شوند.
default('default content')
محتوای پیشفرض این ستون را در صورتی که مقداری ارائه نشود، مشخص میکند.
unsigned()
ستونهای عددی را به عنوان unsigned علامتگذاری میکند (نه منفی و نه مثبت، فقط یک عدد صحیح).
first()
(فقط MySQL) ستون را در ابتدا در ترتیب ستونها قرار میدهد.
after(colName) (فقط MySQL)
ستون را بعد از یک ستون دیگر در ترتیب ستونها قرار میدهد.
charset(charset) (فقط MySQL)
کدگذاری کاراکتر برای یک ستون را تنظیم میکند.
collation(collation)
ترتیب مقایسهای برای یک ستون را تنظیم میکند.
invisible() (فقط MySQL)
ستون را برای پرس و جوهای SELECT مخفی میکند.
useCurrent()
برای ستونهای TIMESTAMP استفاده میشود تا CURRENT_TIMESTAMP به عنوان مقدار پیشفرض قرار گیرد.
isGeometry() (فقط PostgreSQL)
نوع ستون را به GEOMETRY تنظیم میکند (نوع پیشفرض GEOGRAPHY است).
unique()
یک ایندکس UNIQUE اضافه میکند.
primary()
یک ایندکس کلید اصلی اضافه میکند.
index()
یک ایندکس ساده اضافه میکند.
توجه داشته باشید که unique()، primary() و index() همچنین میتوانند خارج از زمینه ساخت ستون به صورت زنجیرهای استفاده شوند که در ادامه به آن پرداخته خواهد شد.
حذف جداول
اگر میخواهید یک جدول را حذف کنید، متد dropIfExists() در Schema وجود دارد که یک پارامتر، یعنی نام جدول را میگیرد:
Schema::dropIfExists('contacts');
تغییر ستونها
برای تغییر یک ستون، فقط کدی که برای ایجاد ستون بهطور جدید مینویسید را بنویسید و سپس یک فراخوانی به متد change() بعد از آن اضافه کنید.
وابستگی مورد نیاز قبل از تغییر ستونها اگر از پایگاه دادهای استفاده میکنید که بهطور بومی از تغییر نام و حذف ستونها پشتیبانی نمیکند (آخرین نسخههای رایجترین پایگاههای داده از این عملیات پشتیبانی میکنند)، قبل از اینکه بتوانید هر گونه تغییر در ستونها ایجاد کنید، باید دستور composer require doctrine/dbal را اجرا کنید. |
پس اگر ستونی از نوع رشته به نام name با طول ۲۵۵ داشته باشیم و بخواهیم طول آن را به ۱۰۰ تغییر دهیم، به این صورت مینویسیم:
Schema::table('users', function (Blueprint $table) {
$table->string('name', 100)->change();
});
همینطور اگر بخواهیم هر یک از ویژگیهای آن را که در نام متد تعریف نشدهاند تنظیم کنیم، به این صورت عمل میکنیم. برای nullable کردن یک فیلد، اینطور مینویسیم:
Schema::table('contacts', function (Blueprint $table) {
$table->string('deleted_at')->nullable()->change();
});
برای تغییر نام یک ستون، اینطور عمل میکنیم:
Schema::table('contacts', function (Blueprint $table)
{
$table->renameColumn('promoted', 'is_promoted');
});
و برای حذف یک ستون، اینطور عمل میکنیم:
Schema::table('contacts', function (Blueprint $table)
{
$table->dropColumn('votes');
});
تغییر چندین ستون به طور همزمان در SQLite اگر بخواهید چندین ستون را در یک بسته مهاجرتی (migration) حذف یا تغییر دهید و از SQLite استفاده میکنید، با خطاهایی مواجه خواهید شد.
|
ترکیب مهاجرتها
اگر مهاجرتهای زیادی دارید که مدیریت آنها دشوار است، میتوانید همه آنها را در یک فایل SQL ترکیب کنید که لاراول قبل از اجرای هر مهاجرت جدید، آن را اجرا خواهد کرد. این فرآیند به نام "ترکیب مهاجرتها" شناخته میشود.
// ترکیب طرح پایگاه داده اما نگه داشتن مهاجرتهای فعلی
php artisan schema:dump
// ذخیره طرح پایگاه داده فعلی و حذف همه مهاجرتهای موجود
php artisan schema:dump --prune
لاراول این dumpها را فقط زمانی اجرا میکند که تشخیص دهد هیچ مهاجرتی تاکنون اجرا نشده است. این یعنی میتوانید مایگریشنهای خود را فشردهسازی (squash) کنید بدون اینکه برنامههایی که قبلاً مستقر شدهاند (deployed) دچار مشکل شوند.
هشدار اگر از dump های طرح پایگاه داده استفاده میکنید، نمیتوانید از SQLite حافظهای (in-memory) استفاده کنید؛ این فقط روی MySQL، PostgreSQL، و SQLite فایل محلی کار میکند. |
ایندکسها و کلیدهای خارجی
ما نحوه ایجاد، اصلاح، و حذف ستونها را بررسی کردیم. حالا به ایندکسگذاری و ارتباط آنها میپردازیم.
اگر با ایندکسها آشنا نیستید، پایگاههای داده شما میتوانند بدون استفاده از آنها هم کار کنند، اما ایندکسها برای بهینهسازی عملکرد و برای برخی از کنترلهای یکپارچگی دادهها در ارتباط با جداول مرتبط بسیار مهم هستند. توصیه میکنم که درباره آنها مطالعه کنید، اما اگر به هر دلیلی می بایست این بخش را رد کنید، فعلاً میتوانید آن را نادیده بگیرید.
اضافه کردن ایندکسها
در مثال ۵-۳ نحوه اضافه کردن ایندکسها به ستونها آورده شده است.
مثال 5-3. افزودن ایندکس ستونها در مهاجرتها
// پس از ایجاد ستونها...
$table->primary('primary_id');// کلید اصلی؛ اگر از increments() استفاده میکنید، نیازی به آن نیست.
$table->primary(['first_name', 'last_name']); // کلیدهای مرکب
$table->unique('email'); // ایندکس یکتا
$table->unique('email', 'optional_custom_index_name'); // ایندکس یکتا با نام سفارشی
$table->index('amount'); // ایندکس پایه
$table->index('amount', 'optional_custom_index_name'); // ایندکس پایه با نام سفارشی
توجه داشته باشید که مثال اول، primary(), اگر از متدهای increments() یا bigIncrements() برای ایجاد ایندکس استفاده کنید، ضروری نیست؛ زیرا این متدها به طور خودکار یک ایندکس کلید اصلی برای شما اضافه میکنند.
حذف ایندکسها
ما میتوانیم ایندکسها را همانطور که در مثال ۵-۴ نشان داده شده است، حذف کنیم.
مثال 5-4. حذف ایندکس ستونها در مهاجرتها
$table->dropPrimary('contacts_id_primary');
$table->dropUnique('contacts_email_unique');
$table->dropIndex('optional_custom_index_name');
// اگر یک آرایه از نامهای ستونها را به dropIndex ارسال کنید، این متد
// نامهای ایندکسها را برای شما بر اساس قوانین تولید حدس میزند
$table->dropIndex(['email', 'amount']);
افزودن و حذف کلیدهای خارجی
برای افزودن یک کلید خارجی که مشخص میکند یک ستون خاص به ستونی در جدول دیگر ارجاع میدهد، سینتکس لاراول ساده و واضح است:
$table->foreign('user_id')->references('id')->on('users');
در اینجا ما یک ایندکس خارجی روی ستون user_id اضافه میکنیم که نشان میدهد این ستون به ستون id در جدول users ارجاع میدهد. سادهتر از این نمیشود.
اگر بخواهیم محدودیتهای کلید خارجی را مشخص کنیم، میتوانیم از cascadeOnUpdate()، restrictOnUpdate()، cascadeOnDelete()، restrictOnDelete() و nullOnDelete() استفاده کنیم. برای مثال:
$table->foreign('user_id')
->references('id')
->on('users')
->cascadeOnDelete();
همچنین یک نام مستعار برای ایجاد محدودیتهای کلید خارجی وجود دارد. با استفاده از آن، مثال بالا میتواند به شکل زیر نوشته شود:
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
برای حذف یک کلید خارجی، میتوانیم آن را با ارجاع به نام ایندکساش (که بهطور خودکار با ترکیب نامهای ستونها و جداول مرجع ایجاد میشود) حذف کنیم:
$table->dropForeign('contacts_user_id_foreign');
یا با ارسال آرایهای از فیلدهایی که در جدول محلی به آنها ارجاع داده میشود:
$table->dropForeign(['user_id']);
اجرای مهاجرت ها
بازبینی دیتابیس
اگر میخواهید وضعیت یا تعریف دیتابیس، جداول و مدلهای آن را بررسی کنید، چند دستور Artisan برای این منظور وجود دارد:
db:show
نمایی از دیتابیس شما را نشان میدهد، از جمله جزئیات اتصال، جداول، اندازه و اتصالات باز.
db:table
با دادن نام جدول، اندازه آن را نشان میدهد و ستونها را فهرست میکند.
db:monitor
تعداد اتصالات باز به دیتابیس را فهرست میکند.
داده گذاری (Seeding)
دادهگذاری با لاراول بسیار ساده است و به همین دلیل در روندهای معمول توسعه به طور گستردهای پذیرفته شده است، به طوری که در فریمورکهای PHP قبلی اینگونه نبوده است. یک پوشه _database/seeders همراه با یک کلاس DatabaseSeeder وجود دارد که متد run() را دارد و زمانی که شما دستور seeder را فراخوانی میکنید، این متد اجرا میشود.
دو روش اصلی برای اجرای دادهگذاریها وجود دارد: همراه با مایگریشن یا به صورت جداگانه.
برای اجرای یک seeder همراه با یک مایگریشن، کافی است --seed را به هر فراخوانی مایگریشن اضافه کنید:
php artisan migrate --seed
php artisan migrate:refresh --seed
و برای اجرای آن به صورت مستقل:
php artisan db:seed
php artisan db:seed VotesTableSeeder
این دستور به طور پیشفرض متد run() کلاس DatabaseSeeder را فراخوانی میکند، یا کلاس seeder که هنگام وارد کردن نام کلاس مشخص کردهاید.
ساخت یک Seeder
برای ساخت یک seeder، از دستور Artisan make:seeder استفاده کنید:
php artisan make:seeder ContactsTableSeeder
حالا کلاس ContactsTableSeeder در دایرکتوری database/seeders ظاهر میشود. قبل از ویرایش آن، بیایید آن را به کلاس DatabaseSeeder اضافه کنیم، همانطور که در مثال 5-5 نشان داده شده است، تا هنگام اجرای دادهگذاریها، این کلاس نیز اجرا شود.
مثال ۵-۵. فراخوانی یک سیدر سفارشی از فایل DatabaseSeeder.php
// database/seeders/DatabaseSeeder.php
...
public function run(): void
{
$this->call(ContactsTableSeeder::class);
}
حالا بیایید خود seeder را ویرایش کنیم. سادهترین کاری که میتوانیم در آن انجام دهیم، وارد کردن دستی یک رکورد با استفاده از facade DB است، همانطور که در مثال 5-6 نشان داده شده است.
مثال ۵-۶. وارد کردن رکوردهای پایگاه داده در یک سیدر سفارشی
<?php
namespace Database\Seeders;
use Illuminate\Database\Seeder;
class ContactsTableSeeder extends Seeder
{
public function run(): void
{
DB::table('contacts')->insert([
'name' => 'Lupita Smith',
'email' => 'lupita@gmail.com',
]);
}
}
این کار یک رکورد به ما میدهد که شروع خوبی است. اما برای seeds واقعی، احتمالاً میخواهید روی یک تولیدکننده تصادفی حلقه بزنید و این دستور insert() را چندین بار اجرا کنید، درست است؟ لاراول ویژگیای برای این کار دارد.
فکتوری های مدل
فکتوریهای مدل الگوهایی را برای ایجاد ورودیهای جعلی برای جداول پایگاه داده شما تعریف میکنند. بهطور پیشفرض، هر فکتوری به نام کلاس Eloquent است.
از نظر تئوری، شما میتوانید این فکتوریها را هرطور که میخواهید نامگذاری کنید، اما نامگذاری فکتوریها بهطور مشابه با نام کلاس Eloquent رویکردی رایج است. اگر از یک کنوانسیون متفاوت برای نامگذاری فکتوریهای خود استفاده میکنید، میتوانید نام کلاس فکتوری را در مدل مرتبط تنظیم کنید.
ایجاد یک فکتوری مدل
فکتوریهای مدل در پوشه database/factories قرار دارند. هر فکتوری در یک کلاس جداگانه تعریف میشود، با متدی به نام definition. در این متد شما ویژگیها و مقادیر آنها را که برای ایجاد مدل با فکتوری استفاده میشوند، تعریف میکنید.
برای تولید یک کلاس فکتوری جدید، از دستور Artisan make:factory استفاده کنید؛ باز هم معمول است که کلاسهای فکتوری را به نام مدلهای Eloquent که قرار است نمونههایی از آنها ایجاد کنند، نامگذاری کنید:
php artisan make:factory ContactFactory
این دستور یک فایل جدید در پوشه database/factories به نام ContactFactory.php ایجاد میکند. سادهترین فکتوری که میتوانیم برای یک تماس تعریف کنیم ممکن است چیزی شبیه به مثال 5-7 باشد:
مثال ۵-۷. سادهترین تعریف ممکن برای یک فکتوری
<?php
namespace Database\Factories;
use App\Models\Contact;
use Illuminate\Database\Eloquent\Factories\Factory;
class ContactFactory extends Factory
{
protected $model = Contact::class;
public function definition(): array
{
return [
'name' => 'Lupita Smith',
'email' => 'lupita@gmail.com',
];
}
}
حال شما نیاز دارید که از trait Illuminate\Database\Eloquent\Factories\HasFactory در مدل خود استفاده کنید.
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Contact extends Model
{
use HasFactory;
}
تریت HasFactory متد factory() را فراهم میکند، که از قوانین لاراول برای تعیین کارخانه (factory) مناسب برای مدل استفاده میکند. این متد به دنبال یک کارخانه در فضای نام Database\Factories میگردد که نام کلاس آن با نام مدل مطابقت داشته باشد و پسوند "Factory" را داشته باشد. اگر این قوانین را دنبال نکردید، میتوانید متد newFactory() را در مدل خود بازنویسی کنید تا کلاس فکتوری ای که باید استفاده شود را مشخص کنید:
// app/Models/Contact.php
...
* Create a new factory instance for the model.
*
* @return \Illuminate\Database\Eloquent\Factories\Factory
*/
protected static function newFactory()
{
return \Database\Factories\Base\ContactFactory::new();
}
حالا میتوانیم متد استاتیک factory() را روی مدل فراخوانی کنیم تا یک نمونه از مدل Contact را در فرایند بذرپاشی (seeding) و تستها ایجاد کنیم:
// ایجاد تکی
$contact = Contact::factory()->create();
// ایجاد چندتایی
Contact::factory()->count(20)->create();
اما اگر از آن کارخانه برای ایجاد ۲۰ Contact استفاده کنیم، همه ۲۰ Contact دارای اطلاعات یکسان خواهند بود. این کمتر مفید است.
ما از کارخانه های مدل حتی بیشتر بهره خواهیم برد زمانی که از Faker استفاده کنیم، که از طریق هِلپر fake() بهطور سراسری در لاراول در دسترس است؛ Faker این امکان را میدهد که ایجاد دادههای ساختگی ساختار یافته را بهراحتی تصادفی کنیم. مثال قبلی حالا به مثال ۵-۸ تبدیل میشود.
مثال ۵-۸. یک فکتوری ساده که برای استفاده از Faker تغییر یافته است
<?php
namespace Database\Factories;
use App\Models\Contact;
use Illuminate\Database\Eloquent\Factories\Factory;
class ContactFactory extends Factory
{
protected $model = Contact::class;
public function definition(): array
{
return [
'name' => fake()->name,
'email' => fake()->email,
];
}
}
اکنون، هر بار که یک Contact جعلی با استفاده از این فکتوری مدل ایجاد میکنیم، تمام ویژگیهای ما بهصورت تصادفی تولید خواهند شد.
کارخانه های مدل باید حداقل، فیلدهای موردنیاز پایگاه داده برای این جدول را بازگردانند.
ضمانت منحصر به فرد بودن دادههای تصادفی تولید شده اگر میخواهید اطمینان حاصل کنید که مقادیر تصادفی تولید شده برای هر ورودی نسبت به مقادیر تصادفی دیگر در آن فرآیند PHP منحصر به فرد هستند، میتوانید از متد unique() در Faker استفاده کنید:
|
استفاده از کارخانه مدل
دو زمینه اصلی وجود دارد که در آنها از کارخانههای مدل استفاده خواهیم کرد: تستها که در فصل ۱۲ پوشش داده میشوند و سیدر که در اینجا به آن میپردازیم. بیایید یک سیدر را با استفاده از کارخانه مدل بنویسیم؛ به مثال ۵-۹ نگاه کنید.
مثال ۵-۹. استفاده از کارخانههای مدل
$post = Post::factory()->create([
'title' => 'My greatest post ever',
]);
// کارخانه مدل پیشرفته؛ اما نگران نباشید!
User::factory()->count(20)->has(Address::factory()->count(2))->create()
ایجاد بیش از یک نمونه با کارخانه مدل
اگر بعد از متد factory() از متد count() استفاده کنید، میتوانید مشخص کنید که بیشتر از یک نمونه ایجاد میکنید. به جای بازگشت یک نمونه، یک مجموعه از نمونهها را برمیگرداند. این یعنی میتوانید نتیجه را مانند یک آرایه در نظر بگیرید و روی آنها تکرار کنید یا آنها را به هر متدی که بیش از یک شیء دریافت میکند ارسال کنید.
$posts = Post::factory()->count(6);
همچنین میتوانید بهصورت اختیاری یک "دنباله" از نحوه بازنویسی هرکدام را تعریف کنید:
$posts = Post::factory()
->count(6)
->state(new Sequence(
['is_published' => true],
['is_published' => false],
))
->create();
کارخانههای مدل پیشرفته
حال که رایجترین استفادهها و تنظیمات کارخانه مدلها را پوشش دادیم، بیایید وارد برخی از روشهای پیچیدهتر استفاده از آنها شویم.
اتصال روابط هنگام تعریف کارخانه مدلها
گاهی اوقات نیاز دارید که یک آیتم مرتبط را همراه با آیتمی که در حال ایجاد آن هستید بسازید. میتوانید متد factory را روی مدل مرتبط فراخوانی کنید تا شناسه آن را دریافت کنید، همانطور که در مثال 5-10 نشان داده شده است.
مثال ۵-۱۰. ایجاد یک آیتم مرتبط در یک فکتوری
<?php
namespace Database\Factories;
use App\Models\Contact;
use Illuminate\Database\Eloquent\Factories\Factory;
class ContactFactory extends Factory
{
protected $model = Contact::class;
public function definition(): array
{
return [
'name' => 'Lupita Smith',
'email' => 'lupita@gmail.com',
'company_id' => \App\Models\Company::factory(),
];
}
}
شما همچنین میتوانید یک closure ارسال کنید که یک پارامتر واحد دریافت میکند، که شامل آرایهای از آیتم تولیدشده تا آن نقطه است. این میتواند به روشهای دیگری نیز استفاده شود، همانطور که در مثال 5-11 نشان داده شده است.
مثال 5-11. استفاده از مقادیر سایر پارامترها در یک کارخانه
// ContactFactory.php
public function definition(): array
{
return [
'name' => 'Lupita Smith',
'email' => 'lupita@gmail.com',
'company_id' => Company::factory(),
'company_size' => function (array $attributes) {
// Uses the "company_id" property generated above
return Company::find($attributes['company_id'])->size;
},
];
}
اضافه کردن موارد مرتبط هنگام تولید نمونههای مدل کارخانه
در حالی که قبلاً نحوه تعریف یک رابطه در تعریف کارخانه را پوشش دادهایم، معمولاً وقتی که نمونههایمان را ایجاد میکنیم، موارد مرتبط آنها را هم تعریف میکنیم.
دو روش اصلی که برای این کار استفاده میکنیم عبارتند از: has() و for() . has() به ما اجازه میدهد که تعریف کنیم نمونهای که داریم ایجاد میکنیم "فرزند" دارد یا موارد دیگر در رابطه از نوع "hasMany" دارد، در حالی که for() به ما اجازه میدهد که تعریف کنیم نمونهای که داریم ایجاد میکنیم "مربوط به" یک مورد دیگر است. بیایید به چند مثال نگاه کنیم تا بهتر متوجه شویم که چگونه کار میکنند.
در مثال 5-12، فرض کنید یک Contact دارای چندین آدرس است.
مثال ۵-۱۲. استفاده از has() هنگام تولید مدلهای مرتبط
// Attach 3 addresses
Contact::factory()
->has(Address::factory()->count(3))
->create()
// Accessing information about each user in the child factory
$contact = Contact::factory()
->has(
Address::factory()
->count(3)
->state(function (array $attributes, User $user) {
return ['label' => $user->name . ' address'];
})
)
->create();
حال فرض کنید که ما در حال ایجاد نمونه فرزند به جای نمونه والد هستیم. بیایید یک آدرس ایجاد کنیم.
در اینگونه مواقع، معمولاً میتوانید فرض کنید که تعریف کارخانه فرزند مسئول ایجاد نمونه والد خواهد بود. پس استفاده از for() چه کاربردی دارد؟ این متد بیشتر زمانی مفید است که بخواهید چیزی را به طور خاص در مورد والد تعریف کنید؛ معمولاً یکی یا بیشتر از ویژگیهای آن یا عبور دادن یک نمونه خاص از مدل. به مثال 5-13 نگاه کنید تا ببینید چگونه معمولاً از آن استفاده میشود.
مثال ۵-۱۳. استفاده از for() هنگام تولید مدلهای مرتبط
// مشخص کردن جزئیات مربوط به والد ایجادشده
Address::factory()
->count(3)
->for(Contact::factory()->state([
'name' => 'Imani Carette',
]))
->create();
// استفاده از یک مدل والد موجود (با فرض اینکه از قبل آن را به صورت $contact داریم)
Address::factory()
->count(3)
->for($contact)
->create();
تعریف و دسترسی به چندین وضعیت برای کارخانه مدل
بیایید برای لحظهای به ContactFactory.php (از مثالهای 5-7 و 5-8) برگردیم. ما یک کارخانه مدل پایه برای Contact تعریف کردهایم:
class ContactFactory extends Factory
{
protected $model = Contact::class;
public function definition(): array
{
return [
'name' => 'Lupita Smith',
'email' => 'lupita@gmail.com',
];
}
}
اما گاهی اوقات شما به بیش از یک کارخانه مدل برای یک کلاس از اشیاء نیاز دارید. اگر بخواهیم بتوانیم برخی از مخاطبان را که افراد بسیار مهم (VIP) هستند اضافه کنیم چه؟ میتوانیم از متد state() برای تعریف یک وضعیت کارخانه مدل دوم برای این کار استفاده کنیم، همانطور که در مثال 5-14 مشاهده میکنید. متد state() یک آرایه از ویژگیهایی که میخواهید به طور خاص برای این وضعیت تنظیم کنید را دریافت میکند.
مثال 5-14. تعریف چندین وضعیت کارخانه مدل برای همان مدل
class ContactFactory extends Factory
{
protected $model = Contact::class;
public function definition(): array
{
return [
'name' => 'Lupita Smith',
'email' => 'lupita@gmail.com',
];
}
public function vip()
{
return $this->state(function (array $attributes) {
return [
'vip' => true,
// Uses the "company_id" property from the $attributes
'company_size' => function () use ($attributes) {
return Company::find($attributes['company_id'])->size;
},
];
});
}
}
حالا، بیایید یک نمونه از یک وضعیت خاص بسازیم:
$vip = Contact::factory()->vip()->create();
$vips = Contact::factory()->count(3)->vip()->create();
استفاده از همان مدل به عنوان رابطه در تنظیمات پیچیده کارخانهها
گاهی اوقات شما یک کارخانه دارید که آیتمهای مرتبط را از طریق کارخانههایشان ایجاد میکند و دو یا بیشتر از آنها رابطه مشابهی دارند. شاید وقتی که یک سفر را با کارخانه خود ایجاد میکنید، به طور خودکار یک رزرو و رسید ایجاد میشود و هر سه باید به همان کاربر متصل شوند. وقتی که شما سفر را ایجاد میکنید، کارخانهها به طور دستی برای هرکدام یک کاربر جدید ایجاد میکنند، مگر اینکه به آنها بگویید که خلاف این کار را انجام دهند.
با استفاده از متد recycle()، شما میتوانید دستور دهید که هر کارخانهای که در زنجیره فراخوانی میشود از همان نمونه از یک شیء استفاده کند. همانطور که در مثال ۵-۱۵ میبینید، این روش یک سینتاکس ساده برای اطمینان از اینکه همان مدل در هر نقطه از زنجیره کارخانه استفاده میشود، فراهم میکند.
مثال 5-15. استفاده از متد recycle() برای استفاده از همان نمونه در هر رابطه در زنجیره کارخانهها
$user = User::factory()->create();
$trip = Trip::factory()
->recycle($user)
-create();
اوه، این خیلی زیاد بود. نگران نباشید اگر پیگیری این مطالب سخت بود—بخش آخر واقعاً مفاهیم پیشرفتهتری بود. بیایید دوباره به مبانی برگردیم و درباره هسته ابزارهای پایگاه داده لاراول صحبت کنیم: سازنده پرسوجو.
سازنده پرس و جو (Query Builder)
استفاده پایه از فساد DB
SQL خام
همانطور که در مثال 5-16 دیدید، این امکان وجود دارد که هر دستور خامی را با استفاده از فساد DB و متد statement() اجرا کنید:
DB::statement('SQL statement here').
اما همچنین متدهای خاصی برای اعمال مختلف و رایج وجود دارد: select()، insert()، update()، و delete(). اینها هنوز دستورات خام هستند، اما تفاوتهایی وجود دارد. اولاً، استفاده از update() و delete() تعداد ردیفهای تغییر یافته را برمیگرداند، در حالی که statement() این کار را نمیکند؛ ثانیاً، با استفاده از این متدها برای توسعهدهندگان آینده مشخصتر است که شما چه نوع دستوری را اجرا میکنید.
انتخابهای خام
سادهترین متد خاص DB، select() است. شما میتوانید آن را بدون پارامتر اضافی اجرا کنید:
$users = DB::select('select * from users');
این دستور آرایهای از اشیاء stdClass را برمیگرداند.
اتصال پارامترها و اتصالهای نامگذاریشده
معماری پایگاهداده لاراول امکان استفاده از اتصال پارامترهای PDO را فراهم میکند که از حملات احتمالی SQL جلوگیری میکند. ارسال پارامتر به یک دستور به سادگی با جایگزینی مقدار در دستور با یک ? و سپس اضافه کردن مقدار به پارامتر دوم فراخوانی شما انجام میشود:
$usersOfType = DB::select(
'select * from users where type = ?',
[$type]
);
همچنین میتوانید این پارامترها را برای وضوح بیشتر نامگذاری کنید:
$usersOfType = DB::select(
'select * from users where type = :type',
['type' => $userType]
);
درجهای خام
از اینجا به بعد، تمام دستورات خام بهطور تقریبی مشابه هستند. درجهای خام به این شکل هستند:
DB::insert(
'insert into contacts (name, email) values (?, ?)',
['sally', 'sally@me.com']
);
بهروزرسانیهای خام
بهروزرسانیها به این شکل هستند:
$countUpdated = DB::update(
'update contacts set status = ? where id = ?',
['donor', $id]
);
حذفهای خام
و حذفها به این شکل هستند:
$countDeleted = DB::delete(
'delete from contacts where archived = ?',
[true]
);
زنجیره سازی با کوئری بیلدر
تراکنش ها
اگر با تراکنشهای پایگاه داده آشنا نیستید، این یک ابزار است که به شما اجازه میدهد مجموعهای از کوئریهای پایگاه داده را بهصورت یکجا اجرا کنید، که میتوانید در صورت نیاز آن را بازگردانی (rollback) کنید و کل مجموعه کوئریها را لغو کنید. تراکنشها معمولاً برای اطمینان از این استفاده میشوند که همه یا هیچکدام از یک سری کوئریهای مرتبط اجرا شوند—اگر یکی از آنها شکست بخورد، ORM کل مجموعه کوئریها را بازمیگرداند.
با قابلیت تراکنش در کوئری بیلدر لاراول، اگر در هر نقطهای از بلاک تراکنش استثنایی رخ دهد، تمام کوئریهای درون تراکنش بازگردانی خواهند شد. اگر اجرای بلاک تراکنش با موفقیت به پایان برسد، تمام کوئریها تأیید (commit) شده و بازگردانی نخواهند شد.
بیایید نمونهای از یک تراکنش ساده را در مثال 5-17 بررسی کنیم.
مثال 5-17. یک تراکنش ساده در پایگاه داده
DB::transaction(function () use ($userId, $numVotes) {
// احتمال اجرای ناموفق کوئری پایگاه داده
DB::table('users')
->where('id', $userId)
->update(['votes' => $numVotes]);
// کوئری کش که نمیخواهیم در صورت شکست کوئری بالا اجرا شود
DB::table('votes')
->where('user_id', $userId)
->delete();
});
در این مثال، میتوان فرض کرد که یک فرایند قبلی تعداد آرا را از جدول votes برای یک کاربر مشخص خلاصه کرده است. ما میخواهیم این مقدار را در جدول users کش کنیم و سپس آن آرا را از جدول votes حذف کنیم. اما طبیعتاً نمیخواهیم رأیها را حذف کنیم تا زمانی که بهروزرسانی جدول users با موفقیت انجام شده باشد. همچنین، نمیخواهیم تعداد بهروزرسانیشده رأیها را در جدول users نگه داریم اگر حذف از جدول votes شکست بخورد.
اگر در هر یک از این کوئریها مشکلی پیش بیاید، کوئری دیگر اعمال نخواهد شد. این همان جادوی تراکنشهای پایگاه داده است.
توجه داشته باشید که میتوانید تراکنشها را بهصورت دستی نیز شروع و پایان دهید—و این موضوع هم برای کوئریهای query builder و هم برای کوئریهای Eloquent صدق میکند. تراکنش را با DB::beginTransaction() شروع کنید، با DB::commit() پایان دهید، و در صورت نیاز با DB::rollBack() متوقف کنید.
DB::beginTransaction();
// Take database actions
if ($badThingsHappened) {
DB::rollBack();
}
// Take other database actions
DB::commit();
مقدمه ای بر Eloquent
ایجاد و تعریف مدل های Eloquent
دریافت داده ها با Eloquent
بیشتر اوقات که دادهها را از پایگاه داده با Eloquent میکشید، از فراخوانیهای ایستا روی مدل Eloquent خود استفاده میکنید.
بیایید با گرفتن همه چیز شروع کنیم:
$allContacts = Contact::all();
این که ساده بود. حالا کمی فیلتر کنیم:
$vipContacts = Contact::where('vip', true)->get();
میبینیم که فساد Eloquent به ما این امکان را میدهد که محدودیتها را زنجیرهای کنیم و از آنجا محدودیتها بسیار آشنا هستند:
$newestContacts = Contact::orderBy('created_at', 'desc')
->take(10)
->get();
مشخص میشود که پس از عبور از نام فساد اولیه، شما در واقع با ساختار کوئری لاراول کار میکنید. شما میتوانید کارهای بیشتری انجام دهید—که به زودی پوشش خواهیم داد—اما هر چیزی که میتوانید با ساختار کوئری روی فساد DB انجام دهید، میتوانید روی اشیای Eloquent خود انجام دهید.
دریافت یک رکورد
همانطور که قبلاً در این فصل توضیح دادیم، میتوانید از first() برای بازگرداندن تنها اولین رکورد از یک کوئری یا از find() برای کشیدن تنها رکورد با شناسه ارائهشده استفاده کنید. برای هر کدام، اگر “OrFail” را به نام متد اضافه کنید، در صورتی که نتیجهای مطابق با آن پیدا نشود، یک استثنا پرتاب خواهد شد. این باعث میشود که findOrFail() ابزار رایجی برای جستجوی یک موجودیت از طریق یک بخش URL باشد (یا پرتاب استثنا اگر موجودیتی مطابق وجود نداشته باشد)، مانند آنچه که در مثال 5-20 میبینید.
مثال ۵-۲۰. استفاده از متد OrFail() در مدل Eloquent داخل یک متد کنترلر
// ContactController
public function show($contactId)
{
return view('contacts.show')
->with('contact', Contact::findOrFail($contactId));
}
هر متدی که قرار است یک رکورد واحد را برگرداند (first()، firstOrFail()، find() یا findOrFail()) یک نمونه از کلاس Eloquent را باز میگرداند. بنابراین، Contact::first() یک نمونه از کلاس Contact را با دادههای ردیف اول جدول پر میکند.
شما همچنین میتوانید از متد firstWhere() استفاده کنید که یک میانبر ترکیب شده از where() و first() است:
// با where() و first()
Contact::where('name', 'Wilbur Powery')->first();
// با firstWhere()
Contact::firstWhere('name', 'Wilbur Powery');
استثناها همانطور که در مثال 5-20 مشاهده میکنید، نیازی به گرفتن استثنای مدل پیدا نشد Eloquent (Illuminate\Database\Eloquent\ModelNotFoundException) در کنترلرهای خود نداریم؛ سیستم مسیریابی لاراول آن را گرفته و برای ما یک 404 رخ می دهد. |
دریافت چند رکورد
متد get() در Eloquent همانطور که در فراخوانیهای معمولی ساختار کوئری کار میکند، عمل میکند—یک کوئری بسازید و در انتها از get() برای دریافت نتایج استفاده کنید:
$vipContacts = Contact::where('vip', true)->get();
اما یک متد خاص Eloquent وجود دارد به نام all() که معمولاً زمانی که میخواهید لیستی از تمام دادههای موجود در جدول بدون فیلتر دریافت کنید، از آن استفاده میشود:
$contacts = Contact::all();
استفاده از get() به جای all() هر زمان که میتوانید از all() استفاده کنید، میتوانید از get() نیز استفاده کنید. Contact::get() همان پاسخ را میدهد که Contact::all() میدهد. اما به محض اینکه شروع به تغییر کوئری خود کنید—مثلاً افزودن فیلتر where()—all() دیگر کار نخواهد کرد، در حالی که get() همچنان کار میکند. |
تقسیم خروجی ها با chunk()
اگر تاکنون نیاز به پردازش تعداد زیادی رکورد (هزاران یا بیشتر) به طور همزمان داشتهاید، ممکن است با مشکلات حافظه یا قفلگذاری مواجه شده باشید. لاراول این امکان را میدهد که درخواستهای خود را به بخشهای کوچکتر (چانکها) تقسیم کرده و آنها را به صورت دستهای پردازش کنید، که این کار بار حافظه درخواستهای بزرگ شما را کاهش میدهد. مثال 5-21 استفاده از chunk() برای تقسیم یک کوئری به "چانک"های 100 رکوردی را نشان میدهد.
مثال ۵-۲۱. تقسیمبندی (Chunking) یک کوئری Eloquent برای محدود کردن مصرف حافظه
Contact::chunk(100, function ($contacts) {
foreach ($contacts as $contact) {
// Do something with $contact
}
});
تجمیعات
تجمیعاتی که در ساختار کوئری موجود هستند، در کوئریهای Eloquent نیز در دسترس هستند. به عنوان مثال:
$countVips = Contact::where('vip', true)->count();
$sumVotes = Contact::sum('votes');
$averageSkill = User::avg('skill_level');
درج و به روزرسانی با Eloquent
حذف با Eloquent
حذف با Eloquent بسیار مشابه بهروزرسانی با Eloquent است، اما با حذفهای نرم (اختیاری)، میتوانید اقلام حذف شده را برای بررسی یا حتی بازیابی بعدی بایگانی کنید.
حذفهای عادی
سادهترین روش برای حذف یک رکورد مدل این است که متد delete() را روی خود نمونه فراخوانی کنید:
$contact = Contact::find(5);
$contact->delete();
با این حال، اگر فقط شناسه (ID) را داشته باشید، نیازی به جستجوی یک نمونه برای حذف آن نیست؛ میتوانید یک شناسه یا آرایهای از شناسهها را به متد destroy() مدل ارسال کرده و آنها را مستقیماً حذف کنید:
Contact::destroy(1);
// or
Contact::destroy([1, 5, 7]);
در نهایت، میتوانید تمام نتایج یک کوئری را حذف کنید:
Contact::where('updated_at', '<', now()->subYear())->delete();
حذفهای نرم
حذفهای نرم ردیفهای پایگاه داده را به عنوان حذف شده علامتگذاری میکنند بدون اینکه واقعاً آنها را از پایگاه داده حذف کنند. این امکان را به شما میدهد که آنها را بعداً بررسی کرده، رکوردهایی که بیشتر از "اطلاعاتی وجود ندارد، حذف شده" هنگام نمایش اطلاعات تاریخی نشان میدهند، و به کاربران (یا مدیران) این امکان را میدهد که برخی یا همه دادهها را بازیابی کنند.
قسمت سخت کدنویسی یک برنامه با حذفهای نرم این است که هر کوئری که بنویسید باید دادههای حذف شده بهصورت نرم را نادیده بگیرد. خوشبختانه، اگر از حذفهای نرم Eloquent استفاده کنید، هر کوئری که بنویسید بهطور پیشفرض برای نادیده گرفتن حذفهای نرم محدود میشود، مگر اینکه به صراحت از آنها بخواهید که دوباره برگردند.
عملکرد حذف نرم Eloquent نیاز به اضافه کردن یک ستون deleted_at به جدول دارد. پس از فعالسازی حذف نرم روی آن مدل Eloquent، هر کوئری که بنویسید (مگر اینکه رکوردهای حذفشده نرم را به صراحت شامل کنید) بهطور پیشفرض برای نادیده گرفتن ردیفهای حذفشده نرم محدود میشود.
کی باید از حذفهای نرم استفاده کنم؟ فقط به این دلیل که یک ویژگی وجود دارد، به این معنا نیست که همیشه باید از آن استفاده کنید. بسیاری از افراد در جامعه لاراول بهطور پیشفرض از حذفهای نرم در هر پروژهای استفاده میکنند فقط به این دلیل که این ویژگی وجود دارد. با این حال، حذفهای نرم هزینههای واقعی دارند. احتمالاً اگر پایگاه داده خود را مستقیماً در ابزاری مانند Sequel Pro مشاهده کنید، حداقل یک بار فراموش میکنید که ستون deleted_at را بررسی کنید. و اگر رکوردهای قدیمی حذفشده نرم را پاکسازی نکنید، پایگاه دادههای شما بزرگتر و بزرگتر خواهند شد. |
فعالسازی حذفهای نرم
برای فعالسازی حذفهای نرم باید دو کار انجام دهید: اضافه کردن ستون deleted_at در یک مایگریشن و وارد کردن ویژگی SoftDeletes در مدل. متدی به نام softDeletes() در سازنده اسکیمای لاراول برای اضافه کردن ستون deleted_at به یک جدول وجود دارد، همانطور که در مثال 5-28 مشاهده میکنید. و مثال 5-29 یک مدل Eloquent را نشان میدهد که حذفهای نرم در آن فعال شده است.
مثال ۵-۲۸. مایگریشن برای افزودن ستون حذف نرم (soft delete) به یک جدول
Schema::table('contacts', function (Blueprint $table) {
$table->softDeletes();
});
مثال ۵-۲۹. یک مدل Eloquent با فعالسازی حذف نرم (soft deletes)
<?php
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
class Contact extends Model
{
use SoftDeletes; // از این Trait استفاده کنید
}
پس از اعمال این تغییرات، هر بار که متدهای delete() و destroy() فراخوانی شوند، ستون deleted_at در ردیف شما به تاریخ و زمان جاری تنظیم میشود به جای اینکه آن ردیف حذف شود. و تمام درخواستهای بعدی آن ردیف را از نتایج خود حذف خواهند کرد.
کوئری با حذفهای نرم
پس، چطور میتوانیم آیتمهای حذفشده نرم را دریافت کنیم؟
اول، میتوانید آیتمهای حذفشده نرم را به یک کوئری اضافه کنید:
$allHistoricContacts = Contact::withTrashed()->get();
بازگرداندن موجودیتهای حذفشده نرم
اگر بخواهید یک آیتم حذفشده نرم را بازگردانید، میتوانید متد restore() را روی یک نمونه یا پرسوجو اجرا کنید:
$contact->restore();
// یا
Contact::onlyTrashed()->where('vip', true)->restore();
حذف اجباری موجودیتهای حذفشده نرم
میتوانید یک موجودیت حذفشده نرم را با فراخوانی forceDelete() روی یک موجودیت یا پرسوجو حذف کنید:
$contact->forceDelete();
// یا
Contact::onlyTrashed()->forceDelete();
دامنه ها (Scopes)
سفارشی سازی تعاملات فیلد با Accessors، Mutators و Attribute Casting
مجموعه های Eloquent
سریال سازی Eloquent
سریالسازی فرایندی است که در آن چیزی پیچیده—یک آرایه یا شیء—را به یک رشته تبدیل میکنید. در یک زمینه مبتنی بر وب، آن رشته معمولاً JSON است، اما میتواند فرمهای دیگری نیز داشته باشد.
سریالسازی رکوردهای پیچیده پایگاه داده میتواند، خوب، پیچیده باشد و این یکی از جاهایی است که بسیاری از ORMها شکست میخورند. خوشبختانه، با Eloquent دو متد قدرتمند به طور رایگان در اختیار دارید: toArray() و toJson(). مجموعهها نیز متدهای toArray() و toJson() دارند، بنابراین همه اینها معتبر هستند:
$contactArray = Contact::first()->toArray();
$contactJson = Contact::first()->toJson();
$contactsArray = Contact::all()->toArray();
$contactsJson = Contact::all()->toJson();
شما همچنین میتوانید یک نمونه یا مجموعه Eloquent را به رشته تبدیل کنید ($string = (string) $contact؛)، اما هم مدلها و هم مجموعهها فقط متد toJson() را اجرا کرده و نتیجه را باز میگردانند.
بازگرداندن مدلها مستقیماً از متدهای مسیریابی
روتر لاراول در نهایت هر آنچه را که مسیرها باز میگردانند به یک رشته تبدیل میکند، بنابراین یک ترفند هوشمندانه وجود دارد که میتوانید از آن استفاده کنید. اگر نتیجه یک فراخوانی Eloquent را در کنترلر بازگردانید، به طور خودکار به رشته تبدیل میشود و بنابراین به عنوان JSON باز میگردد. این به این معنی است که یک مسیر که JSON باز میگرداند میتواند به سادگی هرکدام از موارد زیر باشد.
مثال 5-42. بازگرداندن JSON مستقیماً از مسیرها
// routes/web.php
Route::get('api/contacts', function () {
return Contact::all();
});
Route::get('api/contacts/{id}', function ($id) {
return Contact::findOrFail($id);
});
مخفی کردن ویژگیها از JSON
استفاده از بازگشتهای JSON در APIها بسیار رایج است و مخفی کردن برخی ویژگیها در این زمینهها نیز بسیار رایج است، بنابراین Eloquent این امکان را فراهم میکند که هر ویژگی را هر بار که به JSON تبدیل میشود، مخفی کنید.
شما میتوانید ویژگیها را در لیست سیاه قرار دهید و آنهایی که فهرست کردهاید را مخفی کنید:
class Contact extends Model
{
public $hidden = ['password', 'remember_token'];
یا ویژگیها را در لیست سفید قرار دهید و فقط آنهایی که فهرست کردهاید را نشان دهید:
class Contact extends Model
{
public $visible = ['name', 'email', 'status'];
این همچنین برای روابط کار میکند:
class User extends Model
{
public $hidden = ['contacts'];
public function contacts()
{
return $this->hasMany(Contact::class);
}
بارگذاری محتویات یک رابطه
در صورتی که کنجکاو هستید، میتوانید یک User را با تمام مخاطبانش—مشروط بر اینکه رابطه به درستی تنظیم شده باشد—با فراخوانی زیر دریافت کنید:
|
ممکن است مواقعی باشد که بخواهید یک ویژگی را فقط برای یک فراخوانی خاص قابل مشاهده کنید. این ممکن است با استفاده از متد Eloquent makeVisible():
$array = $user->makeVisible('remember_token')->toArray();
اضافه کردن یک ستون تولیدشده به خروجی آرایه و JSON اگر شما یک دسترسی برای ستونی که وجود ندارد ایجاد کردهاید—برای مثال، ستون full_name در مثال 5-36—آن را به آرایه $appends در مدل اضافه کنید تا به خروجی آرایه و JSON اضافه شود:
|
روابط Eloquent
رویدادهای Eloquent
مدلهای Eloquent هر بار که برخی عملیات خاص انجام میشود، رویدادهایی را در فضای برنامه شما صادر میکنند، صرفنظر از اینکه شما در حال گوش دادن به این رویدادها هستید یا نه. اگر با الگوی pub/sub آشنا باشید، این همان مدل است (در فصل 16 بیشتر در مورد سیستم رویداد لاراول خواهید آموخت).
در اینجا یک مرور سریع برای اتصال یک شنونده به زمانی که یک Contact جدید ایجاد میشود آورده شده است. ما این کار را در متد boot() از AppServiceProvider انجام خواهیم داد، و تصور میکنیم که هر بار که یک Contact جدید ایجاد میکنیم، یک سرویس خارجی را مطلع میکنیم.
مثال 5-64. اتصال یک شنونده به یک رویداد Eloquent
class AppServiceProvider extends ServiceProvider
{
public function boot(): void
{
$thirdPartyService = new SomeThirdPartyService;
Contact::creating(function ($contact) use ($thirdPartyService) {
try {
$thirdPartyService->addContact($contact);
} catch (Exception $e) {
Log::error('Failed adding contact to ThirdPartyService; canceled.');
return false; // عملیات ایجاد را لغو میکند
}
});
}
در مثال 5-64 چند نکته قابل توجه است. اولاً، ما از Modelname::eventName() به عنوان متد استفاده میکنیم و یک closure به آن میدهیم. این closure به نمونه مدل که در حال انجام عملیات روی آن است، دسترسی پیدا میکند. ثانیاً، ما باید این شنونده را در یک سرویسپروایدر تعریف کنیم. و ثالثاً، اگر false برگردانیم، عملیات لغو خواهد شد و save() یا update() لغو میشود.
در اینجا رویدادهایی که هر مدل Eloquent ایجاد میکند آورده شده است:
- creating
- created
- updating
- updated
- saving
- saved
- deleting
- deleted
- restoring
- restored
- retrieved
بیشتر اینها باید کاملاً واضح باشند، مگر اینکه restoring و restored باشد که زمانی که یک ردیف حذفشده نرم بازیابی میشود، فعال میشوند. همچنین، saving برای هر دو عملیات ایجاد و بهروزرسانی فعال میشود و saved برای هر دو عملیات ایجاد و بهروزرسانی فعال میشود.
رویداد retrieved زمانی که یک مدل موجود از دیتابیس بازیابی میشود، فعال میشود.
تست نویسی
فریمورک تستنویسی کامل لاراول این امکان را میدهد که پایگاه داده خود را به راحتی تست کنید—نه با نوشتن تستهای واحد علیه Eloquent، بلکه با تست کل برنامهتان.
سناریو را در نظر بگیرید. شما میخواهید تست کنید که یک صفحه خاص یک تماس را نمایش دهد و تماس دیگر را نه. بخشی از این منطق به تعامل بین URL، کنترلر و پایگاه داده مربوط میشود، بنابراین بهترین روش برای تست آن، تست برنامه است. ممکن است به فکر استفاده از mock برای تماسهای Eloquent و تلاش برای جلوگیری از دسترسی سیستم به پایگاه داده باشید. این کار را انجام ندهید. به جای آن، از مثال ۵-۶۵ استفاده کنید.
مثال ۵-۶۵. تست تعاملات پایگاه داده با تستهای ساده برنامه
public function test_active_page_shows_active_and_not_inactive_contacts()
{
$activeContact = Contact::factory()->create();
$inactiveContact = Contact::factory()->inactive()->create();
$this->get('active-contacts')
->assertSee($activeContact->name)
->assertDontSee($inactiveContact->name);
}
همانطور که مشاهده میکنید، کارخانههای مدل و ویژگیهای تست برنامه لاراول برای تست تماسهای پایگاه داده عالی هستند.
بهطور جایگزین، میتوانید مستقیماً آن رکورد را در پایگاه داده جستجو کنید، مانند مثال ۵-۶۶.
مثال ۵-۶۶. استفاده از assertDatabaseHas() برای بررسی رکوردهای خاص در پایگاه داده
public function test_contact_creation_works()
{
$this->post('contacts', [
'email' => 'jim@bo.com'
]);
$this->assertDatabaseHas('contacts', [
'email' => 'jim@bo.com'
]);
}
Eloquent و فریمورک پایگاه داده لاراول بهطور گستردهای تست شدهاند. نیازی به تست آنها ندارید. نیازی به شبیهسازی آنها ندارید. اگر واقعاً میخواهید از دسترسی به پایگاه داده جلوگیری کنید، میتوانید از یک مخزن (repository) استفاده کنید و سپس نمونههای نادرست مدلهای Eloquent خود را بازگردانید. اما مهمترین پیام این است که نحوه استفاده برنامه شما از منطق پایگاه داده خود را تست کنید.
اگر دسترسیها، تغییرات، اسکوپها یا هر چیز دیگری دارید، میتوانید آنها را بهطور مستقیم تست کنید، مانند مثال ۵-۶۷.
مثال ۵-۶۷. تست دسترسیها، تغییرات و اسکوپها
public function test_full_name_accessor_works()
{
$contact = Contact::factory()->make([
'first_name' => 'Alphonse',
'last_name' => 'Cumberbund'
]);
$this->assertEquals('Alphonse Cumberbund', $contact->fullName);
}
public function test_vip_scope_filters_out_non_vips()
{
$vip = Contact::factory()->vip()->create();
$nonVip = Contact::factory()->create();
$vips = Contact::vips()->get();
$this->assertTrue($vips->contains('id', $vip->id));
$this->assertFalse($vips->contains('id', $nonVip->id));
}
فقط از نوشتن تستهایی که شما را مجبور به ایجاد زنجیرههای پیچیده "Demeter chains" میکند تا اطمینان حاصل کنید که یک استک خاص روی یک شبیهساز پایگاه داده فراخوانی شده است، اجتناب کنید. اگر تستهای شما شروع به پیچیده شدن و غرق شدن در لایه پایگاه داده میکنند، به این دلیل است که شما اجازه دادهاید که مفروضات قبلی شما را مجبور به استفاده از سیستمهای غیرضروری پیچیده کند. آن را ساده نگه دارید.
خلاصه
لاراول همراه با مجموعهای از ابزارهای قدرتمند پایگاه داده ارائه میشود که شامل مایگریشنها، دادهگذاری، سازنده کوئری زیبا و Eloquent، یک ORM قدرتمند از نوع ActiveRecord است. ابزارهای پایگاه داده لاراول نیازی به استفاده از Eloquent ندارند—شما میتوانید به پایگاه داده دسترسی پیدا کنید و آن را با لایهای نازک از راحتی دستکاری کنید بدون نیاز به نوشتن مستقیم SQL. اما افزودن یک ORM، چه Eloquent باشد یا Doctrine یا هر چیز دیگری، آسان است و میتواند بهخوبی با ابزارهای پایگاه داده اصلی لاراول کار کند.
Eloquent از الگوی Active Record پیروی میکند که این امکان را میدهد تا یک کلاس از اشیاء پشتیبانی شده توسط پایگاه داده تعریف کنید، از جمله اینکه آنها در کدام جدول ذخیره شدهاند و شکل ستونها، دسترسیها و تغییرات آنها چگونه است. Eloquent میتواند تمام انواع عملیات SQL معمولی و همچنین روابط پیچیده، از جمله روابط چند به چند پلیمورفیک را مدیریت کند.
لاراول همچنین سیستم قدرتمندی برای تست پایگاه دادهها دارد که شامل کارخانههای مدل است.