web-dev-qa-db-fra.com

Laravel Fluent Query Builder Join avec une sous-requête

OK après des heures de recherche et toujours en utilisant DB :: select, je dois poser cette question. Parce que je suis sur le point de traverser mon ordinateur;).

Je veux obtenir la dernière entrée d'un utilisateur (base sur l'horodatage). Je peux le faire avec le sql brut 

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

J'ai eu cette requête d'un autre post ici sur stackoverflow.

J'ai tout essayé pour le faire avec le constructeur de requêtes couramment à Laravel mais sans succès.

Je sais que le manuel dit que vous pouvez faire ceci:

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

Mais cela n’aide pas beaucoup parce que je ne vois pas comment utiliser une sous-requête ici.

59
driechel

Ok pour vous tous qui êtes arrivés ici désespérés à la recherche du même problème. J'espère que vous trouverez cela plus vite que moi;.

Voici comment cela est résolu. JoostK m'a dit à github que "le premier argument à rejoindre est la table (ou les données) que vous rejoignez". Et il avait raison.

Voici le code. Table et noms différents, mais vous aurez une bonne idée? Il t

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();
126
driechel

Je cherchais une solution à un problème connexe: trouver les derniers enregistrements par groupe, qui est une spécialisation d’un type plus-n-par-groupe avec N = 1. 

La solution implique le problème que vous traitez ici (c’est-à-dire comment construire la requête dans Eloquent), donc je l’affiche car cela pourrait être utile pour d’autres. Il montre une méthode plus simple de construction de sous-requêtes en utilisant une interface puissante et fluide Eloquent avec plusieurs colonnes de jointure et une condition where à l’intérieur de la sous-sélection jointe.

Dans mon exemple, je souhaite extraire les derniers résultats d'analyse DNS (table scan_dns) par groupe identifié par watch_id. Je construis la sous-requête séparément.

Le code SQL que je souhaite générer par 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`

Je l'ai fait de la manière suivante:

// 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();

METTRE À JOUR:

Depuis Laravel 5.6.17 la sous-requête les jointures ont été ajoutées, il existe donc un moyen natif de construire la requête.

$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();
14
ph4r05

Je pense que ce que vous cherchez, c'est "joinSub". Il est soutenu par laravel ^ 5.6. Si vous utilisez la version de laravel inférieure à 5.6, vous pouvez également l'enregistrer en tant que macro dans votre fichier de fournisseur de service d'application. comme ceci 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.*']);
2
shalonteoh

Vous pouvez utiliser le complément suivant pour gérer toutes les fonctions liées aux sous-requêtes à partir de 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
Maksim Martianov

Requête avec sous requête à 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
Amit Meena

Je ne peux pas commenter parce que ma réputation n'est pas assez élevée. @ Franklin Rivero si vous utilisez Laravel 5.2, vous pouvez définir les liaisons sur la requête principale au lieu de la jointure à l'aide de la méthode setBindings.

Donc, la requête principale dans la réponse de @ ph4r05 ressemblerait à ceci:

$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());
0
cby016