-1

I am building a hotel style reservation system and I am loosing the way. I've got stuck at the exactly reservation moment when to add the reservation and I am concerned about come situations.

My flow is really easy, it lets multiple user to go through the reservation process for the same room until one of them press the reservation button before the others. The reservation information is kept in the session and is never stored in the db until somebody reach last step and press "book". In every step, the system check if the room/s is/are available if not it gives the error page.

Now the problem is preventing race condition combined to a last one mandatory check if still there are enough rooms left:

When user is in last step, he/she select the payment method and press book button. After checking if the selected payment option is ok (form data hasn't been altered) it redirects to the reservation controller where:

reservation controller

<?php
ReservationController extends JControllerLegacy{

public function placeReservation(){

    //Do some checks to prevent direct access, booking existence and user completed all steps

    $reservatioModel = $this ->getModel('Reservation')
    if(!$reservationModel->placeReservation()){
         //set errors and redirect to error page
         return false;
    }

    //booking had been placed, passes the data to payment plugin
}

?>

and

Reservation model:

 <?php
 ReservationModel extends JModelLegacy{

public function placeReservation(){

    //Get all variables and objects

    //Lock the tables to prevent any insert while the last check
    $db ->lockTable(#__bookings);
    $db ->lockTable(#__booking_room);

    //Perform the last mandatory check with tables locked to prevent read the table right one moment before another transaction insert the booking and allowing overbooking. THIS CHECK MUST BE 100% SURE NO ONE ELSE IS MANIPULATING/READING THE TABLE
    if(!$this ->checkRoomsAvailability($data))
        return false;

    try{
        $db ->transactionStart();

        //Add the reservation in the booking table
        if(!$this ->_saveBooking()){
           $db ->rollBack();
           return false;
        }

        //Add the room/s to the middle table #__booking_room
        if(!$this -> _saveRooms())
           $db ->rollBack();
           return false;
        }

        $db ->transactionCommit();

    }catch(Exception $e){

        $db ->rollBack();
        $this ->setError('We were not able to complete your reservation.');
        return false;
    }

    $db ->unlockTables();

}
 ?>

The above mentioned tables are InnoDB, #__bookings hold the bookings and #__booking_room hold the information for the selected rooms (like avg_price, number of rooms, etc) and it has an foreign key to #__rooms and one to #__bookings

What am I concerned about?

1 - I am using other tables during the last availability check than the two locked and is giving me error.

2 - When the second user's execution arrives at the table locking point, does it wait until they get free or does it raise an error/exception? I could catch the exception and redirect user to the error page but locket table doesn't means reservation placed, it can occurs any issue while saving the information.

I hope my workflow is ok, please let me know if I am good to go or I should improve my code.

Thanks

EDIT:

To be mentioned, the business idea is like i.e. Expedia, booking, etc where you are allowed to select more rooms types and units per reservation. Indeed, my room table is not for specific/real room but is meant to be for room type with a fixed number of units available everyday if not already booked.

Marco C
  • 45
  • 1
  • 8

2 Answers2

1

My approach would be simpler. In the table that stores the actual reservation, I would have a foreign key for the specific room being reserved, and have a UNIQUE index for the fields roomID, date. This will ensure that the same roomID cannot be recorded twice for the same date.

Next, when the customer confirms booking, I would run everything in a transaction as you're currently doing. When the code gets to the last place and tries to insert a new reservation, if a moment before another customer reserved that room, the DB will not allow you to insert another reservation for that room for that date. That's when I would either:

  • rollback and throw the error, or
  • try again with another room (with its own roomID) if there is another room of the same type still available
BeetleJuice
  • 39,516
  • 19
  • 105
  • 165
  • I can accept your answer if every room would have a row in the #__rooms table and the user is booking on room per reservation but, as you know, that won't be a good choice. I want let the user to book multiple rooms is the same reservation, even because I'm managing type of rooms with an amount of units not just the specific room. – Marco C Aug 13 '17 at 18:36
  • @MarcoC If user books multiple rooms, then have each room booking in its own record. It would be in a separate table (not `reservations`), and all those records would have the same `reservationID` -- a foreign key. In this instance, the `UNIQUE` index would still be `roomID, date`. Since you have other accomodations (eg: flats), maybe `roomID` is not the best field name, but the concept is the same: a unique identifier for the specific space being reserved – BeetleJuice Aug 13 '17 at 19:13
  • `a unique identifier for the specific space being reserved` - the shortest but yet most accurate answer – Nils Rückmann Aug 13 '17 at 19:16
  • @Nils let's see an example. I have a flat that has 3 rooms: 2 king size and 1 full size. Rooms are saved per type not per room unit. I just have two records: - ID: 0 TITLE: room king size UNITS: 2 - ID: 1 TITLE: room full size UNITS: 1 If 2 couples want to book the two kings size bed rooms the same day, following your comment, it wouldn't be possible because of the constrain 'RoomID, Date'. As my logic is based on room units, I need to check one last time that I have enough units left for all the dates the user wants to book. Any suggestion about unique identifier? – Marco C Aug 14 '17 at 05:51
  • It's all about definition of units. If unit = room you can map the constraint to to room and date. But if room != unit you will need another table for units and map the constraint to unit and date. – Nils Rückmann Aug 14 '17 at 06:47
0

I can not say if your workflow aka business domain is the right choice as it depends on your business. But technically you want to have a booking transaction which also provides a check against double booking. The easiest way could be a room-booking-dates table with unique constraint on room is and date. So during your booking transaction you also insert each day with the room id into the table and the unique constraint will ensure that the second attempt will fail.

PS: If a table is locked, PHP will just wait for the unlock. No exception will be thrown (at least if the connection does not close after x seconds)

Nils Rückmann
  • 593
  • 3
  • 16
  • hi @nils, thanks for youe answer. I edited the post specifying the type of business and added that the room selection is per type as I am also renting flats, so with different amount of units and types (king size, twin bed, etc) – Marco C Aug 13 '17 at 18:56
  • Still the same answer. You have to ensure that the data is never in an 'impossible' state. Constraints and transactions are your friends. If you just lock tables you will ensure that the bookings will happen one after another but that will not prevent you from inserting duplicate data. – Nils Rückmann Aug 13 '17 at 19:13
  • is there any way we can chat? I know has been more than a year but I'm still interested in this topic – Marco C Dec 27 '18 at 23:34