9.6 RESERVATIONSSYSTEM

 

 

EINFÜHRUNG

 

Die Reservation von Sitzplätzen in Flugzeugen und Konzertsälen erfolgt heute fast ausnahmslos über Online-Reservationssysteme. Die Informationen werden dabei mit Online-Datenbanken verwaltet. Für die Entwicklung sind Datenbank-Spezialisten mit guten Programmierkenntnissen verantwortlich.

PROGRAMMIERKONZEPTE: Mehrbenutzersystem, Zugriffskonflikt

 

 

RESERVATIONSSYSTEM TEIL 1: BENUTZERINTERFACE

 

Das Reservationssystem präsentiert sich dem Benutzer mit einer attraktiven Benutzeroberfläche. Diese kann mit einem eigenständigen Programm oder als Webseite erzeugt werden. Bei Sitzplatz-Reservationssystemen für  Konzertsäle und Flugzeuge wird üblicherweise der Grundriss des Raumes bzw. des Flugzeuges dargestellt, auf dem die Sitze eingezeichnet sind. Bereits reservierte Sitze werden mit einer besonderen Farbe gekennzeichnet, beispielsweise freie grün und bereits reservierte rot.

Der Benutzer wählt eine Sitzplatzoption durch einen Mausklick auf einen freien Sitz. Dieser wechselt dabei seine Farbe, er wird beispielsweise gelb. Diese Wahl wird noch nicht zum Datenbankserver übertragen,  da der Benutzer oft mehrere zusammengehörende Sitze gleichzeitig reservieren möchte.  Erst beim Klicken auf einen Confirm-Button, wird der Reservationsprozess durchgeführt.

Für die Implementierung des graphischen Benutzerinterfaces ist die Gamelibrary JGameGrid hervorragend geeignet, da sich die Sitze meist gitterartig anordnen lassen. Für die Speicherung des aktuellen Zustands  (frei, option, reserviert) können die Sprite-Identifier (0, 1, 2) verwendet werden.

In deinem Beispiel modellierst du einen kleinen Konzert- oder Theatersaal mit nur 30 Plätzen, die von 1 bis 30 nummeriert sind. Du bettest sie in das nebenstehend gezeigte Gitter ein.

Für die beiden Buttons verwendest du die Klasse GGButton. Um eine Notifikation des Buttonklicks zu erhalten, musst du eine eigene Buttonklasse MyButton definieren,  die sowohl von GGButton wie von GGButtonListener abgeleitet ist (Mehrfachvererbung). In der Methode buttonPressed(), die aufgerufen wird, wenn ein Button gedrückt wird, kannst du mit dem Parameter button herausfinden, um welchen Button es sind handelt.

 

Mit diesem kurzen Programm hast du bereits ein voll funktionsfähiges Benutzerinterface realisiert. Es fehlt natürlich noch die Anbindung an die Datenbank [mehr... Wenn du etwas nicht verstehst, so kannst du dich an den Beispielen im Kapitel Games & OOP orientieren] .

from gamegrid import *

def toLoc(seat):
    i = ((seat - 1) % 6) + 1
    k = ((seat - 1) // 6) + 2
    return Location(i, k)

def toSeatNb(loc):
    if loc.x < 1 or loc.x > 6 or loc.y < 2 or loc.y > 6:
       return None
    i = loc.x - 1
    k = loc.y - 2
    seatNb = k * 6 + i + 1
    return seatNb

class MyButton(GGButton, GGButtonListener):
    def __init__(self, imagePath):
        GGButton.__init__(self, imagePath)
        self.addButtonListener(self)
        
    def buttonClicked(self, button):
        if button == confirmBtn:
            confirm()
        if button == renewBtn:
            renew()
            
    def buttonPressed(self, button):
        pass

    def buttonReleased(self, button):
        pass
           
def renew():
    setStatusText("View refreshed")

def confirm():
    for seatNb in range(1, 31):
       if seats[seatNb - 1].getIdVisible() ==  1:
           seats[seatNb - 1].show(2)
           refresh()
    setStatusText("Reservation successful")

def pressCallback(e):
    loc = toLocation(e.getX(), e.getY())
    seatNb = toSeatNb(loc)
    if seatNb == None:
        return
    seatActor = seats[seatNb - 1]
    if seatActor.getIdVisible() == 0:  # free
        seatActor.show(1) # option
        refresh()
    elif seatActor.getIdVisible() == 1:  # option
       seatActor.show(0) # free
       refresh()

makeGameGrid(8, 8, 40, None, "sprites/stage.gif", False, 
             mousePressed = pressCallback)
addStatusBar(30)
setTitle("Seat Reservation")
setStatusText("Please select free seats and press 'Confirm'")
confirmBtn = MyButton("sprites/btn_confirm.gif")
renewBtn = MyButton("sprites/btn_renew.gif")
addActor(confirmBtn, Location(1, 7))
addActor(renewBtn, Location(6, 7))
seats = []
for seatNb in range(1, 31):
    seatLoc = toLoc(seatNb)
    seatActor = Actor("sprites/seat.gif", 3)
    seats.append(seatActor)
    addActor(seatActor, seatLoc)
    addActor(TextActor(str(seatNb)), seatLoc)
show()
Programmcode markieren (Ctrl+C kopieren, Ctrl+V einfügen)

 

 

MEMO

 

Die Umrechnung von Sitznummern auf Locations und umgekehrt wird in zwei Transformationsfunktionen erledigt. Solche Mappingfunktionen werden oft mit to.. eingeleitet.

 

 

RESERVATIONSSYSTEM TEIL 2: ANBINDUNG AN DIE DATENBANK

 

Online-Datenbanken sind Mehrbenutzer-Systeme. Weil mehrere Clients die Daten gleichzeitig manipulieren, können je nach Situation heikle Zugriffskonflikte auftreten. Es liegt in der Natur solcher Probleme, dass sie nur sporadisch auftreten und darum schwierig zu meistern sind. Das folgende Szenario beschreibt einen typischen Konfliktfall:
 
Auf einem Reservationssystem nehmen Kunde A und Kunde B gleichzeitig eine Reservation vor. A und B fragen zu Beginn kurz hintereinander den aktuellen Belegungsplan von der Datenbank ab. Beide erhalten also die gleichen Informationen über die Sitzbelegung (grün = freie, rot = reservierte Plätze)

A und B können mit einem Mausklick die gewählten Sitze als Option auswählen. Die optierten Sitze werden gelb gefärbt. Diese Optionen werden aber der Datenbank nicht übermittelt, da der Benutzer sie noch verändern oder mit weiteren Optionen ergänzen will. Erst nach einiger Zeit, die bei A und B unterschiedlich lange dauert, klickt der schneller entschlossene Benutzer A den Confirm-Button, um die Option definitiv zu machen. Die Datenbank setzt die Sitze in den Zustand reserviert (booked = 'Y').

Von dieser Veränderung der Datenbank merkt allerdings der Kunde B nichts, da die Datenbank von sich aus keine Rückmeldungen an B macht und A natürlich auch nicht im direkten Kontakt mit B ist. [mehr... In einer Client-Server-Umgebung kann der Server von sich aus keine Mitteilungen an den Client versenden]. Nach einer gewissen Zeit entscheidet sich auch Kunde B und drückt den Confirm-Button.

Wegen Murphy's Spruch "Wenn etwas schiefgehen kann, wird es auch schief gehen" haben A und B gleiche Sitzplätze ausgewählt. Was geschieht? Werden etwa die Sitze nun an B vergeben oder stürzt das Programm von B sogar ab?

Es gibt eine einfache Lösung dieses Zugriffskonflikts: Wenn ein Benutzer seine Option dem Datenbankserver mitteilt, muss kurz zuvor nochmals die Datenbank abgefragt werden,  ob die Sitze tatsächlich immer noch frei sind. Wenn nicht, bleibt nichts anderes übrig, als dem Benutzer höflich mitzuteilen, dass die Plätze leider in der Zwischenzeit bereits vergeben wurden [mehr... Damit wird allerdings das Problem nur aus dem Zeitbereich von Minuten auf Millisekunden verlagert,
denn es könnten sich immer noch innerhalb der Query und dem Update der Datenbank zwei Benutzer
in die Quere kommen. Um dies zu vermeiden, müssen die betroffenen Tabellen während den beiden
Operationen für die ausschliessliche Verwendung durch einen einzigen Client gesperrt werden
(table lock)
]

 

Achtung: Die Ausführung dieses Programms setzt voraus, dass du den Datenbankserver gestartet, die Tabelle reservation erzeugt und die Tabelle mit Initialisierungsdaten gefüllt hast (siehe vorhergehendes Kapitel).

from dbapi import *
from gamegrid import *

serverURL = "derbyserver:localhost"
#serverURL = "derbyserver:10.1.1.123"
dbname = "casino"
username = "admin"
password = "solar"
tbl = "reservation"

def toLoc(seat):
    i = ((seat - 1) % 6) + 1
    k = ((seat - 1) // 6) + 2
    return Location(i, k)

def toSeatNb(loc):
    if loc.x < 1 or loc.x > 6 or loc.y < 2 or loc.y > 6:
       return None
    i = loc.x - 1
    k = loc.y - 2
    seatNb = k * 6 + i + 1
    return seatNb

def pressCallback(e):
    loc = toLocation(e.getX(), e.getY())
    seatNb = toSeatNb(loc)
    if seatNb == None:
        return
    seatActor = seats[seatNb - 1]
    if seatActor.getIdVisible() == 0:  # free
        seatActor.show(1) # option
        refresh()
    elif seatActor.getIdVisible() == 1:  # option
       seatActor.show(0) # free
       refresh()

class MyButton(GGButton, GGButtonListener):
    def __init__(self, imagePath):
        GGButton.__init__(self, imagePath)
        self.addButtonListener(self)
        
    def buttonClicked(self, button):
        if button == confirmBtn:
            confirm()
        if button == renewBtn:
            renew()
 
    def buttonPressed(self, button):
        pass

    def buttonReleased(self, button):
        pass
 
def renew():
    with connect(serverURL, dbname, username, password) as con:    
        cursor = con.cursor()
        sql = "SELECT * FROM " + tbl
        cursor.execute(sql)
        con.commit()
        result = cursor.fetchall()
        for record in result:
            seatNb = record[0]
            isBooked = (record[1] != 'N')
            if isBooked:
                seats[seatNb - 1].show(2)
            else:
                seats[seatNb - 1].show(0)
        refresh()
        setStatusText("View refreshed")

def confirm():
    # check if seats is still available
    with connect(serverURL, dbname, username, password) as con:    
        cursor = con.cursor()
        for seatNb in range(1, 31):
           if seats[seatNb - 1].getIdVisible() == 1:
               sql = "SELECT * FROM "  + tbl + "  WHERE seat=" + str(seatNb)
               cursor.execute(sql)
               result = cursor.fetchall()
               for record in result:
                   if record[1] == 'Y':
                        setStatusText("One of the seats are already taken.")
                        return

        isReserved = False                    
        for seatNb in range(1, 31):
           if seats[seatNb - 1].getIdVisible() == 1:
               sql = "UPDATE "  + tbl + " SET booked='Y' WHERE seat=" + \
                     str(seatNb)
               cursor.execute(sql)
               isReserved = True
           con.commit()
        renew()
        if isReserved:
            setStatusText("Reservation successful")
        else:
            setStatusText("Nothing to do")
        
makeGameGrid(8, 8, 40, None, "sprites/stage.gif", False, 
             mousePressed = pressCallback)
addStatusBar(30)
setTitle("Seat Reservation - Loading...")
confirmBtn = MyButton("sprites/btn_confirm.gif")
renewBtn = MyButton("sprites/btn_renew.gif")
addActor(confirmBtn, Location(1, 7))
addActor(renewBtn, Location(6, 7))
seats = []
for seatNb in range(1, 31):
    seatLoc = toLoc(seatNb)
    seatActor = Actor("sprites/seat.gif", 3)
    seats.append(seatActor)
    addActor(seatActor, seatLoc)
    addActor(TextActor(str(seatNb)), seatLoc)
show()
renew()
setTitle("Seat Reservation - Ready")
setStatusText("Select free seats and press 'Confirm'")
while not isDisposed():
    delay(100)
Programmcode markieren (Ctrl+C kopieren, Ctrl+V einfügen)

 

 

MEMO

 

Du kannst das Reservationssystem mit mehreren Benutzerfenster testen, indem du auf deinem Computer die TigerJython IDE mehrmals startest und das gleiche Programm ausführst. Sind alle Plätze reserviert, so kannst du die Tabelle (wie im vorangehenden Kapitel gezeigt) löschen und wieder neu erstellen.

 

 

AUFGABEN

 

1.


Ergänze das Reservationssystem, dass beim Klicken auf Confirm die Kundennummer (als Integer) abgefragt und in der Datenbank gespeichert wird. Zum Einlesen kannst du die Funktion inputInt(prompt, False) verwenden, die beim Drücken der Abbrechen-Taste das Programm nicht beendet, sondern None zurück gibt.


2.


Schreibe ein Administrator-Tool, dass die aktuelle Belegung und eine Liste der reservierten Sitze sowie der Kundennummern anzeigt. Ausserdem soll es möglich sein, eine Reservation durch Klicken auf einen roten Sitzplatz wieder rückgängig zu machen.