در این قسمت از مجموعه بهره وری قصد دارم در قالب چند بخش، قابلیت جست و جو ای که با تطبیق رشته ها توسط عملگر LIKE کار می کند پیاده سازی کنم. تصور کنید که برنامه ای دارید که صفحه کاربران آن به شکل زیر است.
هنگامی که در باکس سرچ کلمه ای نوشته می شود، می خواهیم نتیجه ها را به کاربرانی که نام، نام خوانوادگی یا نام شرکت آنها با کلمه نوشته شده مطابقت دارد محدود کنیم. این برنامه دو مدل کاربر و شرکت را دارد که در آن هر کاربر متعلق به یک شرکت است. کنترلر User هم اکنون دارای متد index است که کاربر ها را از دیتابیس می خواند و برای آن ها شرکتشان را eagerload می کند و در انتها نتیجه ها را صفحه بندی می کند.
public function index()
{
$users = User::query()
->with('company')
->paginate();
return view('users', ['users' => $users]);
}
در این پروژه seeder ای نوشته شده تا 10 هزار شرکت که هر کدام 10 کاربر دارند را در دیتابیس قرار دهد تا در مجموع 100 هزار کاربر بوجود بیاید. این حجم از داده برای مشخص شدن هرگونه مشکل در بهره وری کوئری های ما کافی است.
اولین کاری که می کنیم ویرایش کوئری موجود در کنترلر User است تا به آن scope جدیدی برای search اضافه کنیم.
public function index()
{
$users = User::query()
->with('company')
->search(request('search'))
->paginate();
return view('users', ['users' => $users]);
}
اکنون به مدل User می رویم تا این scope را اضافه کنیم.
public function scopeSearch($query, string $terms = null)
{
}
این scope دو ورودی دارد. اولین ورودی شی ای از کلاس QueryBuilder است که به طور پیش فرض باید روی همه scope هایی که می نویسیم باشد. دومین ورودی کلمه مورد جست و جو است.
اکنون باید به این فکر کنیم که می خواهیم scope ما چگونه کار کند. یکی از کار هایی که من دوست دارم انجام دهم این است که کلمه جست و جو شده را به کلمه های کلیدی تقسیم و هر کدام از بخش ها را در دیتابیس بررسی کنم. برای مثال اگر عبارت bill gates Microsoft جست و جو شده باشد ابتدا در دیتابیس به دنبال کاربری میگردیم که نام، نام خانوادگی یا شرکتی داشته باشد که با کلمه bill مطابقت داشته باشد. سپس همین کار را برای کلمه های gates و Microsoft هم انجام می دهیم. برای این که کاربری در نتیجه ها بیاید، حداقل یکی از عبارت ها باید با یکی از ستون های موجود در دیتابیس مطابق باشد. این به آن معنا است که هر چه تعداد عبارات بیشتر باشد نتیجه کمتری دریافت خواهیم کرد.
با تقسیم کردن عبارت جست و جو شده به کلمه های کلیدی آغاز می کنیم.
ابتدا عبارت جست و جو شده را با کمک تابع explode بر اساس فاصله به چند بخش تقسیم می کنیم و بخش ها را به یک کالکشن تبدیل می کنیم تا کار های بعدی رو آن ها راحت تر انجام شود.
سپس متد filter را بدون هیچ ورودی ای صدا می زنیم تا مقدار های خالی حذف شوند و با استفاده از متد each روی بخش ها پیمایش می کنیم تا کوئری لازم برای هر بخش را اضافه کنیم.
public function scopeSearch($query, string $terms = null)
{
collect(explode(' ', $terms))->filter()->each(function ($term) use ($query) {
});
}
همانطور که ابتدای مطلب گفته شد قرار است از عملگر LIKE برای جست و جو استفاده کنیم که کار آن تطبیق بر اساس الگو است. به صورت پیش فرض عملگر LIKE تطابق کامل را بررسی می کند. برای مثال اگر ما عبارت jon را جست و جو کرده باشیم، با کلمه Jonathan مطابق نمی شود. البته ما می توانیم % را به قبل و بعد از کلمه کلیدی مورد جست و جو اضافه کنیم تا عملگر LIKE نتایجی که شامل کلمه کلیدی داده شده هستند را هم پیدا کند و به جای تطبیق کامل، نتیجه هایی که به شکل جزئی مطابق هستند را هم در پاسخ بیاورد.
collect(explode(' ', $terms))->filter()->each(function ($term) use ($query) {
$term = $term . '%';
});
اکنون شرط های where را برای نام و نام خوانوادگی می نویسیم.
public function scopeSearch($query, string $terms = null)
{
collect(explode(' ', $terms))->filter()->each(function ($term) use ($query) {
$term = $term . '%';
$query->where(function ($query) use ($term) {
$query->where('first_name', 'like', $term)
->orWhere('last_name', 'like', $term);
});
});
}
باید شرط های هر کلمه کلیدی از دیگر کلمه ها جدا باشند تا نتایج درست تری بدست بیاوریم. به همین دلیل کوئری های جست و جوی یک کلمه کلیدی را داخل یک where نوشتیم تا با بقیه کلمه ها قاطی نشوند.
اکنون می خواهیم بین نام شرکت ها هم جست و جو کنیم. از آن جایی که ستونی که نام شرکت را نگه می دارد در جدول کاربر ها نیست نمی توانیم مانند نام و نام خوانوادگی where اضافه کنیم. یکی از راه های موجود برای انجام کاری که می خواهیم استفاده از متد orWhereHas است.
public function scopeSearch($query, string $terms = null)
{
collect(explode(' ', $terms))->filter()->each(function ($term) use ($query) {
$term = $term . '%';
$query->where(function ($query) use ($term) {
$query->where('first_name', 'like', $term)
->orWhere('last_name', 'like', $term)
->orWhereHas('company', function ($query) use ($term) {
$query->where('name', 'like', $term);
});
});
});
}
این متد نام رابطه را در ورودی اول و یک closure را به عنوان ورودی دوم دریافت می کند. داخل closure همان کاری که برای نام و نام خوانوادگی کردیم را انجام می دهیم. اکنون می توانیم جست و جو را امتحان کنیم.
برای عبارت bill gates Microsoft یک نتیجه پیدا می شود. نگاه دقیق تری به کوئری که زده شده می اندازیم.
SELECT
*
FROM
`users`
WHERE
(
`first_name` LIKE 'bill%' OR `last_name` LIKE 'bill%' OR EXISTS(
SELECT
*
FROM
`companies`
WHERE
`users`.`company_id` = `companies`.`id` AND `name` LIKE 'bill%'
)
) AND(
`first_name` LIKE 'gates%' OR `last_name` LIKE 'gates%' OR EXISTS(
SELECT
*
FROM
`companies`
WHERE
`users`.`company_id` = `companies`.`id` AND `name` LIKE 'gates%'
)
) AND(
`first_name` LIKE 'microsoft%' OR `last_name` LIKE 'microsoft%' OR EXISTS(
SELECT
*
FROM
`companies`
WHERE
`users`.`company_id` = `companies`.`id` AND `name` LIKE 'microsoft%'
)
)
LIMIT 15 OFFSET 0
ابتدا همه ستون های جدول کاربر ها را select کردیم و بعد از آن یک where بزرگ داریم که توسط scope ای که نوشتیم ساخته شده است. در این where ابتدا بررسی می کنیم که نام کاربر با کلمه کلیدی bill به صورت جزئی مطابق هست یا نه سپس همین کار را برای نام خانوادگی و نام شرکت ای که کاربر در آن است انجام می دهیم. سپس همین کار ها را برای کلمه gates و Microsoft انجام می دهیم و در پایان limit و offset مربوط به صفحه بندی را داریم.
در حال حاضر زمان کل درخواست ما 445 میلی ثانیه است. در قسمت بعدی تلاش می کنیم تا با استفاده از index ها این زمان را کاهش دهیم