رابطه پویا زمانی کاربرد داره که بخوایم بدون اضافه کردن Table و Constraint جدید به دیتابیس بین ریکورد ها رابطه ایجاد کنیم. برای مثال یک مدل ساده برای سیستم کتابداری رو در نظر بگیرید :
users id - integer name - string books id - integer title - string transactions id - integer created_at - timestamp book_id - integer loaned - boolean user_id - integer
هر کتاب میتونه تعدادی تراکنش داشته باشه یعنی میتونیم به کاربری قرض بدیم یا از کاربری تحویل بگیریم اما تنها آخرین تراکنش هست که وضعیت کتاب رو مشخص میکنه و بقیه به عنوان تاریخچه تبادلات استفاده میشن. برای گرفتن آخرین تراکنش هر کتاب راه های زیادی هست که هرکدوم مزایا و معایب خودشون رو دارن. میتونید در صفحه زیر توضیحات کامل تری رو بخونید:
تذکر: نسخه لاراول استفاده شده 6 هست.
اول از همه ریلیشن های مدل Transaction رو کامل میکنیم چون فرقی با حالت عادی نداره:
class Transaction extends Model { public function book() { return $this->belongsTo(Book::class); } public function user() { return $this->belongsTo(User::class); } }
سپس مدل Book رو کامل میکنیم:
class Book extends Model { public function transactions() { return $this->hasMany(Transaction::class); } public function lastTransaction() { return $this->belongsTo(Transaction::class); } }
همونطور که حدس زدید رابطه دوم باعث بروز خطا میشه چون برای پیدا کردن تراکنش مربوطه نیاز به کلید خارجی به اسم last_transaction_id در مدل Book هست که وجود نداره. ما این کلید رو به شکل پویا به مدل اضافه میکنیم. برا اینکار یک Scope به مدل Book اضافه میکنیم:
public function scopeWithLastTransaction(Builder $query) { $query->addSelect([ 'last_transaction_id' => Transaction::select('id') ->whereColumn('book_id', 'books.id') ->latest() //By default, result will be ordered by the ccreated_at column ->take(1) ])->with('lastTransaction'); }
برای اینکه Scope ایجاد شده به هر کوئری که روی کتاب ها زده میشه، اضافه بشه باید اون رو به صورت Global در مدل Book تعریف کنیم:
protected static function boot() { parent::boot(); static::addGlobalScope('with_last_transaction', function ($query) { $query->withLastTransaction(); }); }
تا به اینجا نیمی از کار رو رفتیم و میتونیم به این شکل استفاده کنیم:
$myBook = Book::find(1)->first(); $lastTransaction = $myBook->lastTransaction; // WRONG $loanedBooks = Book::whereHas('lastTransaction', function ($query) { $query->where('loaned', true); })->get();
ازونجایی که مقدار last_transaction_id به بخش SELECT کوئری اضافه شده ولی WHERE پیش از اون اجرا میشه نمیتونیم از اسکوپ های آماده لاروال مثل whereHas استفاده کنیم. برای اینکار یک اسکوپ جدید به مدل Book اضافه میکنیم:
public function scopeWhereHasLastTransaction(Builder $query, Closure $callback = null) { $query->whereHas('transactions', function ($query0) use ($callback) { $latestTransactions = Transaction::selectRaw('DISTINCT book_id, MAX(created_at) as last_transaction_created_at')->groupBy('book_id'); $query0->joinSub($latestTransactions, 'latest_transactions', function ($join) { $join->on('transactions.book_id', '=', 'latest_transactions.book_id') ->on('transactions.created_at', '=', 'latest_transactions.last_transaction_created_at'); }); if ($callback) { $callback($query0); } }); }
خب حالا میتونیم به شکل زیر ازش استفاده کنیم:
$loanedBooks = Book::whereHasLastTransaction(function (Builder $query) { $query->where('loaned', true); })` $inStockBooks = Book::whereHasLastTransaction(function (Builder $query) { $query->where('loaned', false); })
در مورد مدل Book هم میشه به دلخواه روابطی اضافه کرد مثلا کتاب های که در حال حاضر به یک کاربر قرض داده شده یا کتاب هایی که به تازگی پس داده شده و ... همه این ها به طور مشابه قابل پیاده سازی هست.
نکته نهایی این که سعی شد تمام خواسته ها در قالب یک کوئری با چند ساب کوئری گنجانده بشه و دیگه زمانی برای تبدیل داده ها به Eloquent Model هدر نره مگر در انتها که میخایم نتایج رو بگیریم.
حتما اگر مشکلی یا ابهامی بود در قسمت نظرات اطلاع بدید.