Laravel Fluent Query Builder Mit Unterabfrage verbinden

75

Okay, nach stundenlanger Recherche und immer noch mit DB :: select muss ich diese Frage stellen. Weil ich gerade dabei bin, meinen Computer wegzuwerfen;).

Ich möchte die letzte Eingabe eines Benutzers erhalten (basierend auf dem Zeitstempel). Ich kann das mit dem rohen SQL machen

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

Ich habe diese Abfrage von einem anderen Beitrag hier auf stackoverflow erhalten.

Ich habe alles versucht, um dies mit dem fließenden Abfrage-Builder in Laravel zu tun, jedoch ohne Erfolg.

Ich weiß, dass das Handbuch besagt, dass Sie dies tun können:

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

Aber das hilft nicht viel, weil ich nicht sehe, wie ich dort eine Unterabfrage verwenden könnte? Wer kann meinen Tag erhellen?

Driechel
quelle

Antworten:

141

Ok für alle da draußen, die verzweifelt hier angekommen sind und nach dem gleichen Problem gesucht haben. Ich hoffe, Sie finden das schneller als ich; O.

So wird es gelöst. JoostK sagte mir bei github, dass "das erste Argument, dem Sie beitreten möchten, die Tabelle (oder Daten) ist, der Sie beitreten." Und er hatte recht.

Hier ist der Code. Unterschiedliche Tabelle und Namen, aber Sie werden die Idee richtig verstehen? Es 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();
Driechel
quelle
5
Dieses Paket soll ein PHP-Tool-Set zum Erstellen von Abfragen bereitstellen. Wir müssen die Abfrage jedoch aufschreiben und per selectRawAnweisung einfügen ! Hatten Sie Glück, ein fließendes Abfrageobjekt anstelle einer Abfragezeichenfolge an die Abfrage zu übergeben?
Hpaknia
Dies wird unmöglich, wenn Sie einen Parameter in der Unterauswahl haben - siehe zum Beispiel @ ph4r05 Antwort, aber ich bin nicht sicher, ob es vereinfacht werden könnte, um diese hässlichen toSql und addBinding zu vermeiden
JustAMartin
28

Ich suchte nach einer Lösung für ein ziemlich verwandtes Problem: die neuesten Datensätze pro Gruppe zu finden, was eine Spezialisierung eines typischen größten n pro Gruppe mit N = 1 ist.

Die Lösung betrifft das Problem, mit dem Sie sich hier befassen (dh wie die Abfrage in Eloquent erstellt wird), sodass ich sie veröffentliche, da sie für andere hilfreich sein könnte. Es zeigt eine sauberere Art der Erstellung von Unterabfragen unter Verwendung einer leistungsstarken fließenden Eloquent-Schnittstelle mit mehreren Verknüpfungsspalten und whereBedingungen innerhalb der verknüpften Unterauswahl.

In meinem Beispiel möchte ich die neuesten DNS-Scanergebnisse (Tabelle scan_dns) pro Gruppe abrufen, die durch gekennzeichnet sind watch_id. Ich erstelle die Unterabfrage separat.

Das SQL, das Eloquent generieren soll:

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`

Ich habe es folgendermaßen gemacht:

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

AKTUALISIEREN:

Seit Laravel 5.6.17 wurden die Unterabfrage -Joins hinzugefügt, sodass es eine native Möglichkeit gibt, die Abfrage zu erstellen.

$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();
ph4r05
quelle
JoinCluse hat keine addBinding-Methode für mich, Laravel 5.2, einen anderen Vorschlag?
Franklin Rivero
Ich fand die neue 5.6-Methode sehr nützlich (mit joinSub, leftJoinSub usw.), aber ich habe ein Problem, meine Unterabfrage berücksichtigt nicht die ON-Bedingung, um dem Join Ergebnisse zu geben. Ich habe so etwas wie $userInfoSubquery = DB::table('user_info') ->select() ->orderBy('info_date', 'DESC') ->limit(1);meine Unterabfrage. Aber wenn die neuesten Informationen nicht dem Benutzer gehören, werden sie nichts zurückgeben ... Also muss ich in der Lage sein, etwas zu tun wie: ->where('user_id', '=', userid)irgendwie in der Unterabfrage ...
Alex
15

Ich denke, was Sie suchen, ist "joinSub". Es wird von Laravel ^ 5.6 unterstützt. Wenn Sie eine Laravel-Version unter 5.6 verwenden, können Sie diese auch als Makro in Ihrer App Service Provider-Datei registrieren. wie folgt 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.*']);
Shalonteoh
quelle
4

Abfrage mit Unterabfrage in 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();
Amit Meena
quelle
1

Ich kann nichts dazu sagen, weil mein Ruf nicht hoch genug ist. @Franklin Rivero Wenn Sie Laravel 5.2 verwenden, können Sie die Bindungen für die Hauptabfrage anstelle des Joins mithilfe der setBindings-Methode festlegen.

Die Hauptabfrage in der Antwort von @ ph4r05 würde also ungefähr so ​​aussehen:

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

Sie können das folgende Addon verwenden, um alle Funktionen für Unterabfragen ab Laravel 5.5+ zu verwalten

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();
Maksim Martianov
quelle
1

Ich bin auf Laravel 7.25 und ich weiß nicht, ob es in früheren Versionen unterstützt wird oder nicht, aber es ist ziemlich gut.

Syntax für die Funktion:

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

Beispiel:

Anzeigen / Abrufen der Benutzer-ID und der Gesamtzahl der von ihnen verbleibenden Beiträge, die zwei Tabellenbenutzer und Beiträge verbinden.

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

Alternative:

Wenn Sie 'left' für leftjoin nicht erwähnen möchten, können Sie eine andere vorgefertigte Funktion verwenden

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

Und ja, es ruft tatsächlich dieselbe Funktion auf, aber es übergibt den Join-Typ selbst. Sie können dieselbe Logik für andere Joins anwenden, z. B. righJoinSub (...) usw.

Tayyab Mughal
quelle