Search code examples
phplaraveleloquenteager-loadinglaravel-query-builder

Laravel - Integrate big raw query in relationship eagerload


I've been using laravel for about 4 years now but now I'm facing my biggest challenge yet. I'm refactoring and extending an old PHP application used to map the offices where I work. There's this huge SQL query that I need to integrate somehow into Laravel's QueryBuilder to feed an eager loaded relationship.

The flow is something like this

Building => hasMany: Floor => hasMany: Seat => hasMany: BookedSeat => belongsTo: User

where Building, Floor, Seat and BookedSeat are Eloquent models.

My huge query selects from BookedSeat the Seat reservations for the current date based on many many other conditions like if the person that booked the seat is in home office, vacation, etc.(these are stored in some other tables) and sets a property on the BookedSeat instance called Status to know if the Seat is taken or not for the current day

Now I'm trying to integrate this raw query into building a JSON hierarchy that I later send to a Vue.js application running on the front-end.

The hierarchy is something like:

 {
   "buildings": [
     {
        // properties
        "floors" : [
           {
              //properties
              "seats": [
                  {
                     //properties
                     "booked": [
                        {
                           "user": "some user model",
                           "Status": "some booked status"
                        }
                      ]
                  },
                  // other seats
              ] 
           }, 
           // other floors
        ]
     },
     //other buildings
   ]
}

The huge query returns an array of objects that I can then use to hydrate a BookedSeat collection with but I have no idea how I can then use this collection or use the huge query directly in order to eager load the BookedSeat for each Seat for each Floor for each Building and let the framework do the heavy lifting for me.

What I tried is building a method like the following:

public static function bookedSeatsForFloor(Relation $seatQuery, Relation $bookedQuery, Carbon $forDate)
    {
        $format = $forDate->format('Y-m-d H:m:i');
        $bindings = $seatQuery->getBindings();

        /** @var AvailableSeatsQuerySimple $availableSeats */
        $availableSeats = new AvailableSeatsQuerySimple($bindings, $format); // bindings are my floor id's and I'm feeding them to my big query in order to have control over which floors to load depending on the user's rights 

        return DB::raw($availableSeats->getRawQuery());
    }

and call it like this:

Floor::where('id', $someId)->with(['seats' => static function ($seatQuery) use ($_that) {

    /**
     * Add to each seat the manager and the active booking
     */
    $seatQuery->with(['booked' => static function ($bookedQuery) use ($seatQuery, $_that) {
        return self::bookedSeatsForFloor($seatQuery, $bookedQuery, $_that->forDate);
    }, 'manager'])->orderBy('seat_cir');
}]);

But I'd need to somehow modify the $bookedQuery in the bookedSeatsForFloor method with a $bookedQuery->select('something') or $bookedQuery->setQuery('some query builder instance') but I have no idea how to convert the huge query to a Builder instance.

Thanks!

PS: I would preferably want to skip rewriting the huge query into eloquent syntax because of the complexity

ADDED DETAILS:

So, as requested, this is my raw query where I changed some of the database/table names per company policy

    SET NOCOUNT ON;
DECLARE
    @the_date DATETIME;
SET @the_date = (SELECT CONVERT(DATETIME, ?, 120));

SELECT seat_identifier,
       user_id,
       FromDate,
       ToDate,
       Status
FROM (
         SELECT d.seat_identifier,
                d.user_id,
                d.FromDate,
                d.ToDate,
                CASE
                    WHEN d.Status IS NULL
                        THEN 0
                    WHEN d.Status = 2
                        THEN 2
                    WHEN d.Status != 0
                        THEN CASE
                                 WHEN -- New, OnGoing Request in Main_DB_Name
                                             ho.status = 1 -- New StatusType in Main_DB_Name
                                         OR
                                             ho.status = 4 -- Pending StatusType in Main_DB_Name
                                         OR
                                             twl.status = 1 -- New StatusType in Main_DB_Name
                                         OR
                                             twl.status = 4 -- Pending StatusType in Main_DB_Name
                                         OR
                                             li.status = 1 -- New StatusType in Main_DB_Name
                                         OR
                                             li.status = 4 -- Pending StatusType in Main_DB_Name
                                         OR
                                             ctaf.status = 1 -- New StatusType2 in Main_DB_Name
                                         OR
                                             ctaf.status = 2 -- Ongoing StatusType2 in Main_DB_Name
                                     THEN
                                     2 --> Pending seat in MyApplication


                                 WHEN -- Approved Request in Main_DB_Name
                                             ho.status = 2
                                         OR
                                             twl.status = 2
                                         OR
                                             li.status = 1
                                         OR
                                             li.status = 2
                                         OR
                                             ctaf.status = 1
                                         OR
                                             ctaf.status = 2
                                     THEN 0 -- Free Seat MyApplication

                                 ELSE 1 -- Taken  Seat MyApplication
                        END
                    END as 'Status'
         FROM (
                  SELECT seats.seat_identifier as seat_identifier,
                         c.user_id,
                         c.FromDate,
                         c.ToDate,
                         c.Status
                  FROM (
                           SELECT fo_bs.seat_identifier,
                                  fo_bs.user_id,
                                  fo_bs.FromDate,
                                  fo_bs.ToDate,
                                  fo_bs.Status
                           FROM MyApplication.another_schema.BookedSeats fo_bs
                                    INNER JOIN MyApplication.another_schema.seats AS seats ON fo_bs.seat_identifier = seats.seat_identifier
                               WHERE fo_bs.FromDate <= @the_date
                                    AND fo_bs.ToDate >= @the_date
                                    AND fo_bs.Status IN (1, 2)
                                    AND seats.floor_id IN (###FLOOR_IDS###) -- will replace this from php with a list of "?,?,?" depending on how many floor_ids are in the query bindings
                       ) c
                           INNER JOIN MyApplication.another_schema.seats AS seats ON c.seat_identifier = seats.seat_identifier) d
                  LEFT JOIN (SELECT requester, status
                             from Main_DB_Name.schema.HOME_OFFICE
                                 WHERE Main_DB_Name.schema.HOME_OFFICE.from_date <= @the_date
                                      and Main_DB_Name.schema.HOME_OFFICE.to_date >= @the_date) ho ON d.user_id = ho.requester
                  LEFT JOIN (SELECT requester, status
                             from Main_DB_Name.schema.TEMPORARY_WORK_LOCATION
                                 WHERE Main_DB_Name.schema.TEMPORARY_WORK_LOCATION.from_date <= @the_date
                                      and Main_DB_Name.schema.TEMPORARY_WORK_LOCATION.to_date >= @the_date) twl
                            ON d.user_id = twl.requester
                  LEFT JOIN (SELECT employee, status
                             from Main_DB_Name.schema.LEAVE_INVOIRE
                                 WHERE Main_DB_Name.schema.LEAVE_INVOIRE.leave_date = @the_date) li ON d.user_id = li.employee
                  LEFT JOIN (SELECT requester, status
                             from Main_DB_Name.schema.TRAVEL
                                 WHERE Main_DB_Name.schema.TRAVEL.from_date <= @the_date
                                      and Main_DB_Name.schema.TRAVEL.until_date >= @the_date) ctaf
                            ON d.user_id = ctaf.requester
     ) y

And my models/relationships are as following:

class Building extends Model {
  /* Properties */

   public function floors()
    {
        return $this->hasMany(Floor::class);
    }
}

class Floor extends Model {
  /* Properties */
   public function building()
    {
        return $this->belongsTo(Building::class);
    }

   public function seats()
    {
        return $this->hasMany(Seat::class);
    }
}

class Seat extends Model {
  /* Properties */
   public function floor()
    {
        return $this->belongsTo(Floor::class);
    }

   public function booked()
    {
        return $this->hasMany(BookedSeat::class);
    }
}

class BookedSeat extends Model {
  /* Properties */
   public function user()
    {
        return $this->belongsTo(User::class);
    }

   public function seat()
    {
        return $this->belongsTo(Seat::class);
    }
}

Solution

  • The problem is quite a difficult one. I was stuck with it trying different things for more than a week in total but couldn't find any nice way of doing it.

    I ended up using @Jonas Staudenmeir's suggestion by manually mapping through all my nested relationships and then setting the corresponding booked relation on my Seat model instances from a collection obtained from using BookedSeat::hydrate() with the results from the raw query as an argument.

    $availableSeats = new AvailableSeatsQuerySimple($format);
    
            // Map through all the Buildings
            $this->template['buildings'] = $this->template['buildings']
                ->map(static function (Building $building) use ($availableSeats) {
    
                    // Map through all the Floors in a Building
                    $floors = $building->floors->map(static function (Floor $floor) use ($availableSeats) {
                        /** @var BookedSeat|Collection $booked */
                        $booked = $availableSeats->execute($floor->id); // execute the raw query and get the results
    
                        if(count($booked) > 0) {
    
                            // Map through all the Seats in a Floor
                            $seats = $floor->seats->map(static function (Seat $seat) use ($booked) {
    
                                // Select the BookedSeat for the corresponding Seat
                                /** @var BookedSeat $bookedSeatForRelation */
                                $bookedSeatForRelation = $booked->filter(static function (BookedSeat $bookedSeat) use ($seat) {
                                    return $bookedSeat->seat_identifier === $seat->id;
                                })->first();
    
                                // Attach the BookedSeat to the Seat only if the Status IS NOT 0
                                if($bookedSeatForRelation !== null && $bookedSeatForRelation->Status !== 0) {
                                    return $seat->setRelation('booked', $bookedSeatForRelation);
                                }
    
                                return $seat->setRelation('booked', null);
                            });
    
                            return $floor->setRelation('seats', $seats);
                        }
    
                        return $floor;
                    });
    
                    return $building->setRelation('floors', $floors);
                });