جلب حقل واحد من علاقة has-Many بإستخدام SubQuery في laravel eloquent

جلب حقل واحد من علاقة has-Many بإستخدام SubQuery في laravel eloquent

2024-12-21 وقت القراءه : 5 دقائق

لفرض أن لدينا جدول المستخدمين وفي جدول أخر logins يتم تسجيل الوقت والتاريخ لكل عملية دخول يقوم بها المستخدم وأريد عرض جميع المستخدمين مع أخر عملية دخول.

 

أبسط طريقة للقيام بذلك 

In eloquent

public function index(){
$users=User::query()
     ->orderBy('name')
     ->paginate();
return view('books',compact('users'));
}

 

In blade

@foreach($users as $user)
    <tr>
        <td>{{ $user->name }}</td>
        <td>{{ $user->email }}</td>
        <td>{{ $user->logins()->latest()->first()->created_at->diffForHumans() }}</td>
    </tr>
@endforeach

 

لكن لو ألقينا نظره على debug bar


نجد أن هناك مشكلة، حيث تم تنفيذ ١٢ جملة إستعلام، ولو ألقينا نظرة على جمل الإستعلام نجد أن منها ١٠ جمل متشابهه، لأنه لكل مستخدم يتم عرضه يتم تنفيذ إستعلام أخر حتى يتم جلب أخر دخول، وهذه يطلق عليها n1 plus issue

 

ما الذي يمكننا فعله لتحسين الأداء

الطريقة الأولى

إضافة حقل last_login_id في جدول users وفي كل عملية دخول يتم تحديث هذا الحقل، وتعتبر هذه الطريقة فعالة، لكن ليس هي الحل الأمثل، بالطبع نحن لسنا بحاجة للقيام بذلك.

 

الطريقة الثانية

إستخدام eager loading

public function index(){
    $users=User::query()
        ->with('logins')
        ->orderBy('name')
        ->paginate();
    return view('books',compact('users'));
}

وفي ملف blade

<tr>
    <td>{{ $user->name }}</td>
    <td>{{ $user->email }}</td>
    <td>{{ $user->logins()->sortBy('created_at')->first()->created_at->diffForHumans() }}</td>
</tr>

 

عند إلقاء نظره على debug bar نجد أن جمل الإستعلام أصبحت ٣ لكن تم تحميل الموديل ٨٦ مرة، ومعدل إستهلاك الذاكرة هو 20MB.

 

 

الطريقة الثالثة 

وهو الحل الذي أفضلة هو إستخدام subquery  

حيث تسمح لنا SubQuery بتنفيذ إستعلامات داخل قاعدة بيانات أخرى، وتعتبر هذه طريقة فاله دون أن يتم تنفيذ إستعلامات إضافية.

public function index(){
     $users=User::query()
         ->addSelect(['last_login_at'=>Login::select('created_at')
             ->whereColumn('user_id','users.id')
             ->latest()
             ->take(1)
        ])
         ->orderBy('name')
         ->paginate();
    return view('books',compact('users'));
}

 

In blade

@foreach($users as $user)
    <tr>
        <td>{{ $user->name }}</td>
        <td>{{ $user->email }}</td>
        <td>{{ $user->last_login_at }}</td>
    </tr>
@endforeach

 

نلاحظ من شريط debug Bar أن عدد الموديل التي تم تحميلها فقط ٩ بدلا من ٨٦ ، وتم تقليل إستهلاك الذاكرة إلى 4.06MB، وتم تنفيذ إستعلامين فقط.

 

لو قمنا بأخذ جملة الإستعلام التي تننفيذها الى phpMyAdmin او tablePlus او أي برنامج أخر

 

 

نرى إنه تم إضافة جدول جديد باسم last_login_at مع العلم أنه غير موجود مسبقاً كحقل، بل تم إنشاءه مؤقتا أثناء تنفيذ الجمله in the fly.

 

ولو قمنا بإزالة LIMIT 1 فإننا سوف نحصل على الخطأ 

Query 1 ERROR: Subquery returns more than 1 row

  

ماذا لو أردنا أن يتم عرض التاريخ كـ diffForHumans

<td>{{ $user->last_login_at->diffForHumans() }}</td>

لكن هذا سوف يعطي خطأ 

ErrorException 
Trying to get property 'diffForHumans' of non-object (View: /Library/WebServer/Documents/test/resources/views/books.blade.php) 

 وذلك لأن last_login_at هي string وليس من تنسيقات Carbon ولروعة لارافيل أنها وفرت لنا عمل cast 

حيث يمكن تحويل last_login_at إلى time instance

public function index(){
     $users=User::query()
         ->addSelect(['last_login_at'=>Login::select('created_at')
             ->whereColumn('user_id','users.id')
             ->latest()
             ->take(1)
        ])
         ->withCasts(['last_login_at' => 'datetime'])
         ->orderBy('name')
         ->paginate();
    return view('books',compact('users'));
}

  

المشكله الأن أن الكود في الكونترولر أصبح كبير، ويفضل أن يتم نقل subquery إلى user model 

In users model

public function scopeWithLastLoginAt($query){
     $query->addSelect(['last_login_at'=>Login::select('created_at')
        ->whereColumn('user_id','users.id')
        ->latest()
        ->take(1)
    ])
        ->withCasts(['last_login_at' => 'datetime']);
}

 

In eloquent(controller)

public function index(){
     $users=User::query()
         ->withLastLoginAt()
         ->orderBy('name')
         ->paginate();
    return view('books',compact('users'));
}

التعليقات
زائر
منذ 3 سنوات

يعطيك الف عافيه

إضافة تعليق
Loading...