Laravel Fluent Query Builder Приєднайтеся до підзапиту


77

Гаразд, після багатогодинних досліджень та використання DB :: select Я повинен поставити це питання. Тому що я ось-ось віддам свій комп’ютер;).

Я хочу отримати останній вхід користувача (база на позначці часу). Я можу це зробити за допомогою сирого sql

SELECT  c.*, p.*
FROM    users c INNER JOIN
(
  SELECT  user_id,
          MAX(created_at) MaxDate
  FROM    `catch-text`
  GROUP BY user_id
 ) MaxDates ON c.id = MaxDates.user_id INNER JOIN
    `catch-text` p ON   MaxDates.user_id = p.user_id
     AND MaxDates.MaxDate = p.created_at

Я отримав цей запит з іншого повідомлення тут, на stackoverflow.

Я спробував все, щоб зробити це за допомогою вільного конструктора запитів у Laravel, однак безуспішно.

Я знаю, що в керівництві сказано, що ви можете зробити це:

DB::table('users')
    ->join('contacts', function($join)
    {
        $join->on('users.id', '=', 'contacts.user_id')->orOn(...);
    })
    ->get();

Але це не дуже допомагає, бо я не розумію, як я міг би там використовувати підзапит? Той, хто може освітлити мій день?

Відповіді:


143

Гаразд для всіх вас, хто прибув сюди у відчаї, шукаючи ту саму проблему. Сподіваюся, ви знайдете це швидше, ніж я; О.

Ось як це вирішується. JoostK сказав мені на github, що "першим аргументом, що приєднується, є таблиця (або дані), до яких ви приєднуєтеся". І він мав рацію.

Ось код. Різні таблиці та назви, але ви зрозумієте ідею правильно? Це т

DB::table('users')
        ->select('first_name', 'TotalCatches.*')

        ->join(DB::raw('(SELECT user_id, COUNT(user_id) TotalCatch,
               DATEDIFF(NOW(), MIN(created_at)) Days,
               COUNT(user_id)/DATEDIFF(NOW(), MIN(created_at))
               CatchesPerDay FROM `catch-text` GROUP BY user_id)
               TotalCatches'), 
        function($join)
        {
           $join->on('users.id', '=', 'TotalCatches.user_id');
        })
        ->orderBy('TotalCatches.CatchesPerDay', 'DESC')
        ->get();

5
Цей пакет повинен передбачати набір інструментів PHP для побудови запитів, але нам потрібно записати запит і ввести його за допомогою selectRawоператора! Вам пощастило передати об'єкту вільного запиту запит, а не рядок запиту?
hpaknia

Це стає неможливим, якщо у вас є параметр у підвиборі - наприклад, див. Відповідь @ ph4r05, але я не впевнений, чи можна це спростити, щоб уникнути цих негарних toSql та addBinding
JustAMartin

28

Я шукав вирішення досить супутньої проблеми: пошук найновіших записів на групу, що є спеціалізацією типового найбільшого n-на-групу з N = 1.

Рішення передбачає проблему, з якою ви тут стикаєтесь (тобто як створити запит у Eloquent), тому я публікую його, оскільки це може бути корисним для інших. Він демонструє більш чистий спосіб побудови підзапитів за допомогою потужного вільного інтерфейсу Eloquent з кількома стовпцями об’єднання та whereумовою всередині об’єднаного підвибору.

У своєму прикладі я хочу отримати найновіші результати сканування DNS (таблиця scan_dns) на групу, визначену watch_id. Я будую підзапит окремо.

SQL, який я хочу, щоб Eloquent генерував:

SELECT * FROM `scan_dns` AS `s`
INNER JOIN (
  SELECT x.watch_id, MAX(x.last_scan_at) as last_scan
  FROM `scan_dns` AS `x`
  WHERE `x`.`watch_id` IN (1,2,3,4,5,42)
  GROUP BY `x`.`watch_id`) AS ss
ON `s`.`watch_id` = `ss`.`watch_id` AND `s`.`last_scan_at` = `ss`.`last_scan`

Я зробив це наступним чином:

// table name of the model
$dnsTable = (new DnsResult())->getTable();

// groups to select in sub-query
$ids = collect([1,2,3,4,5,42]);

// sub-select to be joined on
$subq = DnsResult::query()
    ->select('x.watch_id')
    ->selectRaw('MAX(x.last_scan_at) as last_scan')
    ->from($dnsTable . ' AS x')
    ->whereIn('x.watch_id', $ids)
    ->groupBy('x.watch_id');
$qqSql = $subq->toSql();  // compiles to SQL

// the main query
$q = DnsResult::query()
    ->from($dnsTable . ' AS s')
    ->join(
        DB::raw('(' . $qqSql. ') AS ss'),
        function(JoinClause $join) use ($subq) {
            $join->on('s.watch_id', '=', 'ss.watch_id')
                 ->on('s.last_scan_at', '=', 'ss.last_scan')
                 ->addBinding($subq->getBindings());  
                 // bindings for sub-query WHERE added
        });

$results = $q->get();

ОНОВЛЕННЯ:

Оскільки Laravel 5.6.17 підзапит приєднується були додані так є власний спосіб для побудови запиту.

$latestPosts = DB::table('posts')
                   ->select('user_id', DB::raw('MAX(created_at) as last_post_created_at'))
                   ->where('is_published', true)
                   ->groupBy('user_id');

$users = DB::table('users')
        ->joinSub($latestPosts, 'latest_posts', function ($join) {
            $join->on('users.id', '=', 'latest_posts.user_id');
        })->get();

JoinCluse не має методу addBinding для мене, Laravel 5.2, будь-яка інша пропозиція?
Франклін Ріверо,

Я знайшов новий 5.6 спосіб зробити дуже приємним у використанні (з joinSub, leftJoinSub тощо ...), але у мене виникла проблема, мій підзапит не враховує умову ON для отримання результатів приєднання. У мене є щось на зразок $userInfoSubquery = DB::table('user_info') ->select() ->orderBy('info_date', 'DESC') ->limit(1);мого підзапиту. Але якщо найновіша інформація не належить користувачеві, вона не поверне щось ... Тож мені потрібно вміти робити щось на зразок: ->where('user_id', '=', userid)якось у підзапиті ...
Олексій

15

Я думаю, що ви шукаєте "joinSub". Він підтримується з laravel ^ 5.6. Якщо ви використовуєте версію laravel нижче 5.6, ви також можете зареєструвати її як макрос у файлі постачальника послуг програми. як це https://github.com/teamtnt/laravel-scout-tntsearch-driver/issues/171#issuecomment-413062522

$subquery = DB::table('catch-text')
            ->select(DB::raw("user_id,MAX(created_at) as MaxDate"))
            ->groupBy('user_id');

$query = User::joinSub($subquery,'MaxDates',function($join){
          $join->on('users.id','=','MaxDates.user_id');
})->select(['users.*','MaxDates.*']);

4

Запит із підзапитом у Laravel

$resortData = DB::table('resort')
        ->leftJoin('country', 'resort.country', '=', 'country.id')
        ->leftJoin('states', 'resort.state', '=', 'states.id')
        ->leftJoin('city', 'resort.city', '=', 'city.id')
        ->select('resort.*', 'country.name as country_name', 'states.name as state_name','city.name as city_name', DB::raw("(SELECT GROUP_CONCAT(amenities.name) from resort_amenities LEFT JOIN amenities on amenities.id= resort_amenities.amenities_id WHERE resort_amenities.resort_id=resort.id) as amenities_name"))->groupBy('resort.id')
        ->orderBy('resort.id', 'DESC')
       ->get();

1

Я не можу коментувати, оскільки моя репутація недостатньо висока. @Franklin Rivero, якщо ви використовуєте Laravel 5.2, ви можете встановити прив'язки для основного запиту замість об'єднання за допомогою методу setBindings.

Отже, основний запит у відповіді @ ph4r05 буде виглядати приблизно так:

$q = DnsResult::query()
    ->from($dnsTable . ' AS s')
    ->join(
        DB::raw('(' . $qqSql. ') AS ss'),
        function(JoinClause $join) {
            $join->on('s.watch_id', '=', 'ss.watch_id')
                 ->on('s.last_scan_at', '=', 'ss.last_scan');
        })
    ->setBindings($subq->getBindings());

1

Ви можете використовувати наступний аддон для обробки всіх функцій, пов’язаних із підзапитами від laravel 5.5+

https://github.com/maksimru/eloquent-subquery-magic

User::selectRaw('user_id,comments_by_user.total_count')->leftJoinSubquery(
  //subquery
  Comment::selectRaw('user_id,count(*) total_count')
      ->groupBy('user_id'),
  //alias
  'comments_by_user', 
  //closure for "on" statement
  function ($join) {
      $join->on('users.id', '=', 'comments_by_user.user_id');
  }
)->get();

1

Я на Laravel 7.25, і я не знаю, чи підтримує він попередні версії чи ні, але це досить добре.

Синтаксис функції:

public function joinSub($query, $as, $first, $operator = null, $second = null, $type = 'inner', $where = false)

Приклад:

Показ / отримання ідентифікатора користувача та загальної кількості дописів, які вони залишили, приєднуючись до двох таблиць користувачів та дописів.

        return DB::table('users')
            ->joinSub('select user_id,count(id) noOfPosts from posts group by user_id', 'totalPosts', 'users.id', '=', 'totalPosts.user_id', 'left')
            ->select('users.name', 'totalPosts.noOfPosts')
            ->get();

Альтернатива:

Якщо ви не хочете згадувати "left" для leftjoin, тоді ви можете використовувати іншу попередньо вбудовану функцію

    public function leftJoinSub($query, $as, $first, $operator = null, $second = null)
    {
        return $this->joinSub($query, $as, $first, $operator, $second, 'left');
    }

Так, він насправді викликає ту саму функцію, але передає сам тип об'єднання. Ви можете застосувати ту саму логіку для інших об'єднань, тобто righJoinSub (...) тощо.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.