Im vorherigen Beitrag hat unsere erste WebStencils-Seite eine Liste von Objekten gerendert, die wir im Code erzeugt haben. Um die Syntax kennenzulernen, war das in Ordnung, aber so sieht keine echte Seite aus. Echte Seiten zeigen Daten aus einer Datenbank, und genau das machen wir heute.
Eine Anforderung habe ich mir selbst gestellt: Die Datenbank darf nichts sein, was Sie herunterladen, wiederherstellen oder von Hand einrichten müssen. Der Server legt sie an. Beim ersten Start erzeugt er die SQLite-Datei, die Tabelle und eine Handvoll Beispieldatensätze. Bei jedem weiteren Start findet er alles vor und lässt es in Ruhe. Ich habe aufgehört zu zählen, wie viele Beispielprojekte ich nicht starten konnte, weil eine Datenbankdatei fehlte oder ein Skript auf einen Pfad auf dem Rechner von jemand anderem verwies -- fügen wir dieser Liste kein weiteres hinzu.
Wir verwenden SQLite, das eine komplette Datenbank in einer einzigen Datei hält und keinen Server braucht, sowie FireDAC, die Datenbankbibliothek von Delphi. Beide werden mit Delphi ausgeliefert, der Stack bleibt also derselbe wie in der gesamten Serie: Über Delphi hinaus muss nichts installiert werden.
Was wir bauen
Bevor wir loslegen, schauen wir uns an, wann welcher Teil des Codes läuft, denn es gibt zwei klar getrennte Zeitpunkte. Das Programm legt die Datenbank genau einmal an, beim Start, bevor der HTTP-Server die erste Anfrage annimmt. Danach öffnet jede Anfrage an die Kundenseite eine Query über die Verbindung ihres Web-Moduls und übergibt sie an WebStencils.
Die gepunktete Linie vom Web-Modul zur Datei ist das entscheidende Detail: Jedes Web-Modul spricht über seine eigene Verbindung mit derselben Datenbankdatei. Erinnern Sie sich an den ersten Beitrag dieser Serie: WebBroker erzeugt ein Web-Modul pro gleichzeitiger Anfrage. Genau deshalb kann jedes Modul eine eigene Verbindung besitzen, ohne dass wir selbst irgendetwas sperren müssen -- und genau deshalb muss SQLite mehreren offenen Verbindungen erlauben, sich die Datei zum Lesen und Schreiben zu teilen, was die Standardeinstellung von FireDAC, wie wir gleich sehen werden, nicht tut.
Falls FireDAC neu für Sie ist: Mein früherer Beitrag zu Delphi und PostgreSQL von Grund auf baut Schritt für Schritt eine FireDAC-Anwendung auf und legt dabei auch eine Datenbank im Code an. Mit SQLite fällt dieser Teil heute deutlich kürzer aus.
Schritt 1: Die Datenbank im Code anlegen
Alles, was die Datenbank betrifft, kommt in eine Unit. Legen Sie CustomerData.pas an:
unit CustomerData;
interface
uses
FireDAC.Comp.Client;
function DatabaseFileName: string;
function CreateConnection: TFDConnection;
procedure CreateDatabase;
implementation
uses
System.SysUtils,
System.IOUtils,
FireDAC.Stan.Def,
FireDAC.Stan.Async,
FireDAC.DApt,
FireDAC.Phys.SQLite,
FireDAC.Phys.SQLiteWrapper.Stat,
FireDAC.ConsoleUI.Wait;
const
SEED: array[0..5, 0..1] of string = (
('Los Pollos Hermanos', 'Albuquerque'),
('Wayne Enterprises', 'Gotham City'),
('Dunder Mifflin', 'Scranton'),
('Cyberdyne Systems', 'Sunnyvale'),
('Oscorp', 'New York'),
('Stark Industries', 'New York'));
function DatabaseFileName: string;
begin
// the database file sits next to the executable
Result := TPath.Combine(ExtractFilePath(ParamStr(0)), 'customers.sqlite3');
end;
function CreateConnection: TFDConnection;
begin
Result := TFDConnection.Create(nil);
Result.LoginPrompt := False;
Result.Params.DriverID := 'SQLite';
Result.Params.Database := DatabaseFileName;
// (1) create the file if it does not exist yet
Result.Params.Values['OpenMode'] := 'CreateUTF8';
// (2) FireDAC opens SQLite exclusively by default; several web
// modules with their own connections need shared access
Result.Params.Values['LockingMode'] := 'Normal';
Result.Params.Values['SharedCache'] := 'False';
end;
procedure CreateDatabase;
var
Connection: TFDConnection;
I: Integer;
begin
Connection := CreateConnection;
try
Connection.Connected := True;
// (3) the schema -- safe to run on every start
Connection.ExecSQL(
'CREATE TABLE IF NOT EXISTS customers (' +
' id INTEGER PRIMARY KEY AUTOINCREMENT,' +
' name TEXT NOT NULL,' +
' city TEXT NOT NULL)');
// (4) sample data, but only into an empty table
if Connection.ExecSQLScalar('SELECT COUNT(*) FROM customers') = 0 then
begin
Connection.StartTransaction;
try
for I := Low(SEED) to High(SEED) do
Connection.ExecSQL(
'INSERT INTO customers (name, city) VALUES (:name, :city)',
[SEED[I, 0], SEED[I, 1]]);
Connection.Commit;
except
Connection.Rollback;
raise;
end;
end;
finally
Connection.Free;
end;
end;
end.Die nummerierten Kommentare der Reihe nach:
OpenMode=CreateUTF8weist den FireDAC-SQLite-Treiber an, die Datenbankdatei anzulegen, falls sie noch nicht existiert. Das ist zwar der Standard, aber in einer Unit, deren einziger Zweck das Anlegen einer Datenbank ist, schreibe ich es lieber ausdrücklich hin.LockingMode=Normalist die Zeile, die Ihnen einen Nachmittag erspart. FireDAC öffnet SQLite-Datenbanken standardmäßig im exklusiven Sperrmodus. Das ist die schnellste Variante, solange es nur eine einzige Verbindung gibt. Im exklusiven Modus behält eine Verbindung, die einmal aus der Datei gelesen hat, ihre Sperre, bis sie geschlossen wird. Unsere Seite, die nur liest, bemerkt davon nichts: Ich habe die Zeile entfernt, 20 parallele Anfragen an den Server geschickt, und jede lieferte alle sechs Zeilen. Die Falle schnappt beim ersten Schreibzugriff zu. Unsere Web-Module halten ihre Verbindungen offen, solange sie leben, und so scheitert jede andere Verbindung, die schreiben will -- ein weiteres Web-Modul oder ein Datenbankwerkzeug auf Ihrem Desktop --, mitdatabase is locked. Im normalen Modus werden die Sperren nach jeder Transaktion freigegeben, undSharedCache=Falsegibt jeder Verbindung ihren eigenen Cache. Die SQLite-Datenbank in Embarcaderos eigenen WebStencils-Demos ist mit genau diesen beiden Einstellungen konfiguriert.CREATE TABLE IF NOT EXISTSkann bei jedem Start gefahrlos ausgeführt werden. Beim ersten Start legt es die Tabelle an, bei jedem weiteren tut es nichts.- Die Beispieldaten werden nur eingefügt, wenn die Tabelle leer ist, damit ein Neustart des Servers sie nicht verdoppelt. Sie werden in einer einzigen Transaktion eingefügt, sodass nichts halb fertig zurückbleibt, wenn ein Insert fehlschlägt. Außerdem ist es schneller: Ohne explizite Transaktion ist jedes einzelne
INSERTeine eigene Transaktion, und genau das nennt die SQLite-FAQ als klassischen Grund für langsame Inserts.
Beachten Sie die Units im Implementation-Abschnitt. FireDAC.Phys.SQLite registriert den Treiber, und FireDAC.Phys.SQLiteWrapper.Stat linkt die SQLite-Engine statisch in die ausführbare Datei, sodass keine sqlite3.dll ausgeliefert werden muss. FireDAC.ConsoleUI.Wait stellt den Wartecursor von FireDAC für eine Konsolenanwendung bereit. In meinen Tests mit RAD Studio 13 lief der Server auch ohne diese Unit, denn FireDAC behandelt jeden Thread außer dem Hauptthread als "silent" und fragt dort nie nach einem Wartecursor. Jeder Codepfad, der doch danach fragt, bricht ohne eingebundene Wait-Unit allerdings mit einem "factory missing"-Fehler ab, und die Unit kostet nichts, also behalte ich sie. Das WebBroker-Projekt in den WebStencils-Demos verwendet für seine SQLite-Datenbank dieselben Units.
Beachten Sie außerdem, dass es sich bei den Beispieldaten um erfundene Firmen aus Film und Fernsehen handelt. Falls Sie Post von den Anwälten von Wayne Enterprises bekommen: Ich habe Ihnen nicht gesagt, dass Sie diese Zeile einfügen sollen.
Schritt 2: Die Datenbank beim Start anlegen
Das Programm ruft CreateDatabase auf, bevor es mit dem Lauschen beginnt. Fügen Sie CustomerData der uses-Klausel von HelloServer.dpr hinzu und erweitern Sie den Anfang des Hauptblocks:
// (0) create the database file, table, and sample data if needed
CreateDatabase;
WriteLn('Database: ', DatabaseFileName);
// (1) tell WebBroker which class answers the requests
WebRequestHandler.WebModuleClass := THelloModule;Schlägt das Anlegen der Datenbank fehl -- etwa weil der Ordner schreibgeschützt ist --, landet die Exception im vorhandenen except-Block, und der Server startet gar nicht erst. Das ist Absicht. Ein Webserver, der ohne seine Datenbank startet, verschiebt den Fehler nur auf die erste Anfrage, wo er deutlich schwerer zu entdecken ist.
Schritt 3: Die Datenbank im Web-Modul abfragen
Das Web-Modul bekommt eine eigene Verbindung. Fügen Sie FireDAC.Comp.Client der uses-Klausel im Interface-Abschnitt und CustomerData der im Implementation-Abschnitt hinzu, und ergänzen Sie die Klasse um ein Feld und einen Destruktor:
THelloModule = class(TWebModule)
private
FConnection: TFDConnection;
procedure AddRoute(const APathInfo: string; AMethod: TMethodType;
AHandler: THTTPMethodEvent; ADefault: Boolean = False);
procedure SendJson(Response: TWebResponse; AStatusCode: Integer;
AJson: TJSONObject);
procedure Cors(Sender: TObject; Request: TWebRequest;
Response: TWebResponse; var Handled: Boolean);
procedure HelloAction(Sender: TObject; Request: TWebRequest;
Response: TWebResponse; var Handled: Boolean);
procedure AddAction(Sender: TObject; Request: TWebRequest;
Response: TWebResponse; var Handled: Boolean);
procedure HomeAction(Sender: TObject; Request: TWebRequest;
Response: TWebResponse; var Handled: Boolean);
procedure CustomersAction(Sender: TObject; Request: TWebRequest;
Response: TWebResponse; var Handled: Boolean);
procedure NotFoundAction(Sender: TObject; Request: TWebRequest;
Response: TWebResponse; var Handled: Boolean);
public
constructor Create(AOwner: TComponent); override;
destructor Destroy; override;
end;Der Konstruktor erzeugt die Verbindung und registriert die neue Route; der Destruktor gibt die Verbindung wieder frei:
constructor THelloModule.Create(AOwner: TComponent);
begin
inherited;
// (10) every web module instance gets its own connection
FConnection := CreateConnection;
// (1) runs before any action -- adds the CORS headers
BeforeDispatch := Cors;
// (2) one action per URL
AddRoute('/hello/HelloService/Hello', mtGet, HelloAction);
AddRoute('/hello/HelloService/Add', mtGet, AddAction);
AddRoute('/hello', mtGet, HomeAction);
AddRoute('/hello/customers', mtGet, CustomersAction);
// (3) the default action answers everything else
AddRoute('', mtAny, NotFoundAction, True);
end;
destructor THelloModule.Destroy;
begin
FConnection.Free;
inherited;
end;Beachten Sie, dass wir die Verbindung hier nur erzeugen. Geöffnet wird sie erst bei der ersten Query, ein Web-Modul, das immer nur JSON beantwortet, berührt die Datenbank also nie. Der Handler für die neue Seite sieht so aus:
procedure THelloModule.CustomersAction(Sender: TObject; Request: TWebRequest;
Response: TWebResponse; var Handled: Boolean);
var
Query: TFDQuery;
Processor: TWebStencilsProcessor;
City: string;
begin
City := Request.QueryFields.Values['city'];
Query := TFDQuery.Create(nil);
try
Query.Connection := FConnection;
// (11) user input only ever travels as a parameter
if City = '' then
Query.Open('SELECT id, name, city FROM customers ORDER BY name')
else
Query.Open('SELECT id, name, city FROM customers ' +
'WHERE city = :city ORDER BY name', [City]);
Processor := TWebStencilsProcessor.Create(nil);
try
Processor.InputFileName := TemplateFileName('customers.html');
Processor.AddVar('info', TPageInfo.Create('Customers'), True);
// (12) the open query IS the data -- False: the processor
// must not free it, we do that ourselves
Processor.AddVar('customers', Query, False);
Response.ContentType := 'text/html; charset=utf-8';
Response.Content := Processor.Content;
finally
Processor.Free;
end;
finally
Query.Free;
end;
end;Die Nummerierung setzt die der vorherigen Beiträge fort:
- Jede Instanz des Web-Moduls erzeugt im Konstruktor ihre eigene
TFDConnectionund gibt sie im Destruktor frei. Da WebBroker nie zwei Anfragen gleichzeitig auf demselben Modul ausführt, teilen sich nie zwei Threads eine Verbindung. - Der optionale Query-Parameter
cityfiltert die Liste. Er gelangt ausschließlich als Parameter (:city) zur Datenbank, niemals als Teil des SQL-Strings. Was auch immer jemand in die URL schreibt, es kann die Anweisung nicht verändern. Ich weiß, dass Sie das wissen. Ich sehe aber immer noch oft genug per String-Verkettung zusammengebautes SQL in Produktivcode, um es jedes einzelne Mal hinzuschreiben. - Die geöffnete Query wird direkt an WebStencils übergeben. Es gibt keine Objektliste und keinen Mapping-Code: WebStencils iteriert über die Zeilen eines Datasets und liest die Felder über ihren Namen. Der dritte Parameter ist diesmal
False, denn die Query gehört uns und wird in unserem eigenenfinally-Block freigegeben. Übergeben Sie den Besitz an den Prozessor, geben Sie die Query zweimal frei.
Schritt 4: Das Template
Im Template wird aus dem Dataset eine Tabelle. Speichern Sie es als templates/customers.html:
@LayoutPage layout
<h1>@info.Title</h1>
<table>
<thead>
<tr><th>#</th><th>Name</th><th>City</th></tr>
</thead>
<tbody>
@ForEach (var customer in customers) {
<tr>
<td>@customer.id</td>
<td>@customer.name</td>
<td><a href="/hello/customers?city=@customer.city">@customer.city</a></td>
</tr>
}
</tbody>
</table>
<p><a href="/hello/customers">Show all customers</a> | <a href="/hello">Home</a></p>@ForEach (var customer in customers) läuft durch die Zeilen der Query, und @customer.name liest das Feld name der aktuellen Zeile -- dieselbe Syntax, die wir im vorherigen Beitrag für Objekteigenschaften verwendet haben. Die Feldnamen sind die Spaltennamen aus unserem SELECT. Das Template nutzt das Layout aus dem vorherigen Beitrag, die Seite erhält also denselben Rahmen und dieselbe Fußzeile, ohne eine einzige zusätzliche Zeile.
Jede Stadt ist ein Link, der die Liste nach dieser Stadt filtert. Die Kunden aus New York sind genau aus diesem Grund dabei: Ein Filter, der immer nur eine Zeile liefert, beweist nicht viel.
Schritt 5: Starten
Kopieren Sie den Ordner templates erneut neben HelloServer.exe -- er enthält eine neue Datei -- und starten Sie den Server. Die Konsole zeigt jetzt zuerst den Pfad der Datenbankdatei an.

Öffnen Sie http://localhost:8080/hello/customers, und Sie sollten alle sechs Kunden sehen, nach Namen sortiert.

Klicken Sie auf New York, und die Liste schrumpft auf Oscorp und Stark Industries. Schick.

?city=New%20York; die Stadt gelangt als Parameter zu SQLite.Beenden Sie nun den Server und schauen Sie neben die ausführbare Datei: Dort liegt customers.sqlite3. Starten Sie den Server erneut, und nichts wird doppelt eingefügt. Löschen Sie die Datei, und der nächste Start legt sie von Grund auf neu an. Genau darum geht es bei dieser Übung: Die Datenbank ist Teil des Codes und nichts, was Sie mitbringen müssen.
Der JSON-Service aus dem ersten Beitrag und die Startseite aus dem zweiten funktionieren natürlich weiterhin. Unser kleiner Server liefert jetzt JSON, HTML aus statischen Daten und datenbankgestütztes HTML aus einer einzigen ausführbaren Datei.

Wo dieses Beispiel endet
Für ein kleines internes Werkzeug trägt diese Struktur recht weit. Sie sollten aber wissen, wo sie aufhört. CREATE TABLE IF NOT EXISTS ist keine Migrationsstrategie: Sobald Sie zum ersten Mal einer bestehenden Datenbank eine Spalte hinzufügen müssen, wollen Sie versionierte Schemaänderungen. Die Seite liest nur Daten; Formulare zum Anlegen oder Bearbeiten von Kunden sind der naheliegende nächste Schritt und brauchen eine ordentliche Validierung. Und SQLite kommt mit vielen Lesern sehr gut zurecht, erlaubt aber immer nur einen Schreiber gleichzeitig, wie die SQLite-Dokumentation zu geeigneten Einsatzgebieten ausführlich erläutert. Für einen schreiblastigen Service mit vielen gleichzeitigen Benutzern ist ein Datenbankserver wie PostgreSQL die bessere Wahl, und mit FireDAC ist der Wechsel im Wesentlichen eine Frage der Verbindungsparameter.
Fazit
Wir haben eine WebStencils-Seite aus einer SQLite-Datenbank gespeist, die das Programm selbst anlegt: Datei, Tabelle und Beispieldaten beim ersten Start, und keinerlei Änderungen bei jedem weiteren Start. Drei Details tragen das Beispiel. LockingMode=Normal erlaubt jedem Web-Modul, seine eigene Verbindung offen zu halten, ohne jeden Schreibzugriff auszusperren. Query-Parameter halten Benutzereingaben aus dem SQL heraus. Und WebStencils iteriert direkt über eine FireDAC-Query und liest die Felder über ihren Namen.
Die beste Beispieldatenbank ist die, die niemand herunterladen muss.
Damit schließen wir die ersten Schritte unserer Serie ab. Wir haben mit einem JSON-Service begonnen, serverseitig gerendertes HTML hinzugefügt und eine Datenbank angebunden -- alles mit WebBroker, WebStencils und FireDAC. Vertraut wird man damit nur, indem man Dinge verändert. Fügen Sie also als Nächstes ein Formular hinzu, das einen Kunden anlegt, und stellen Sie sich vor, welche Möglichkeiten Sie jetzt haben...
Vollständiger Quellcode
Zum Nachschlagen finden Sie hier die vollständigen Dateien, die sich gegenüber dem vorherigen Beitrag geändert haben. Die unveränderten Dateien HelloPageModel.pas und HelloWebModule.dfm sowie die Templates aus den vorherigen Beiträgen vervollständigen das Projekt.
program HelloServer;
{$APPTYPE CONSOLE}
uses
System.SysUtils,
Web.WebReq,
IdHTTPWebBrokerBridge,
HelloPageModel in 'HelloPageModel.pas',
CustomerData in 'CustomerData.pas',
HelloWebModule in 'HelloWebModule.pas' {HelloModule: TWebModule};
const
PORT = 8080;
var
Server: TIdHTTPWebBrokerBridge;
begin
try
// (0) create the database file, table, and sample data if needed
CreateDatabase;
WriteLn('Database: ', DatabaseFileName);
// (1) tell WebBroker which class answers the requests
WebRequestHandler.WebModuleClass := THelloModule;
// (2) the HTTP server: Indy, bridged to WebBroker
Server := TIdHTTPWebBrokerBridge.Create(nil);
try
Server.DefaultPort := PORT;
Server.Active := True;
WriteLn('WebBroker server running on port ', PORT);
WriteLn('Try: http://localhost:8080/hello/customers');
WriteLn('Press Enter to stop.');
ReadLn;
Server.Active := False;
finally
Server.Free;
end;
except
on E: Exception do
begin
WriteLn(E.ClassName, ': ', E.Message);
ExitCode := 1;
end;
end;
end.unit HelloWebModule;
interface
uses
System.SysUtils,
System.Classes,
System.JSON,
Web.HTTPApp,
FireDAC.Comp.Client;
type
THelloModule = class(TWebModule)
private
FConnection: TFDConnection;
procedure AddRoute(const APathInfo: string; AMethod: TMethodType;
AHandler: THTTPMethodEvent; ADefault: Boolean = False);
procedure SendJson(Response: TWebResponse; AStatusCode: Integer;
AJson: TJSONObject);
procedure Cors(Sender: TObject; Request: TWebRequest;
Response: TWebResponse; var Handled: Boolean);
procedure HelloAction(Sender: TObject; Request: TWebRequest;
Response: TWebResponse; var Handled: Boolean);
procedure AddAction(Sender: TObject; Request: TWebRequest;
Response: TWebResponse; var Handled: Boolean);
procedure HomeAction(Sender: TObject; Request: TWebRequest;
Response: TWebResponse; var Handled: Boolean);
procedure CustomersAction(Sender: TObject; Request: TWebRequest;
Response: TWebResponse; var Handled: Boolean);
procedure NotFoundAction(Sender: TObject; Request: TWebRequest;
Response: TWebResponse; var Handled: Boolean);
public
constructor Create(AOwner: TComponent); override;
destructor Destroy; override;
end;
implementation
uses
System.IOUtils,
System.Generics.Collections,
Web.Stencils,
HelloPageModel,
CustomerData;
{$R *.dfm}
function TemplateFileName(const AName: string): string;
begin
// the templates folder sits next to the executable
Result := TPath.Combine(
TPath.Combine(ExtractFilePath(ParamStr(0)), 'templates'), AName);
end;
{ THelloModule }
constructor THelloModule.Create(AOwner: TComponent);
begin
inherited;
// (10) every web module instance gets its own connection
FConnection := CreateConnection;
// (1) runs before any action -- adds the CORS headers
BeforeDispatch := Cors;
// (2) one action per URL
AddRoute('/hello/HelloService/Hello', mtGet, HelloAction);
AddRoute('/hello/HelloService/Add', mtGet, AddAction);
AddRoute('/hello', mtGet, HomeAction);
AddRoute('/hello/customers', mtGet, CustomersAction);
// (3) the default action answers everything else
AddRoute('', mtAny, NotFoundAction, True);
end;
destructor THelloModule.Destroy;
begin
FConnection.Free;
inherited;
end;
procedure THelloModule.AddRoute(const APathInfo: string; AMethod: TMethodType;
AHandler: THTTPMethodEvent; ADefault: Boolean);
var
Item: TWebActionItem;
begin
Item := Actions.Add;
Item.PathInfo := APathInfo;
Item.MethodType := AMethod;
Item.Default := ADefault;
Item.OnAction := AHandler;
end;
procedure THelloModule.SendJson(Response: TWebResponse; AStatusCode: Integer;
AJson: TJSONObject);
begin
try
Response.StatusCode := AStatusCode;
Response.ContentType := 'application/json; charset=utf-8';
Response.Content := AJson.ToJSON;
finally
AJson.Free;
end;
end;
procedure THelloModule.Cors(Sender: TObject; Request: TWebRequest;
Response: TWebResponse; var Handled: Boolean);
begin
// (4) CORS: '*' allows EVERY origin, which effectively switches the
// browser's cross-origin protection off for this server. Fine for
// development; in production, replace '*' with the origin of your
// web application, e.g. 'https://app.example.com'.
Response.SetCustomHeader('Access-Control-Allow-Origin', '*');
// (5) answer the browser's preflight request right here
if SameText(Request.Method, 'OPTIONS') then
begin
Response.SetCustomHeader('Access-Control-Allow-Methods', 'GET, OPTIONS');
Response.SetCustomHeader('Access-Control-Allow-Headers', 'Content-Type');
Response.StatusCode := 204;
Handled := True;
end;
end;
procedure THelloModule.HelloAction(Sender: TObject; Request: TWebRequest;
Response: TWebResponse; var Handled: Boolean);
begin
SendJson(Response, 200,
TJSONObject.Create.AddPair('value', 'Hello from WebBroker!'));
end;
procedure THelloModule.AddAction(Sender: TObject; Request: TWebRequest;
Response: TWebResponse; var Handled: Boolean);
var
A, B: Integer;
begin
// (6) nobody converts the parameters for us -- we do it ourselves
if TryStrToInt(Request.QueryFields.Values['A'], A) and
TryStrToInt(Request.QueryFields.Values['B'], B) then
SendJson(Response, 200,
TJSONObject.Create.AddPair('value', TJSONNumber.Create(A + B)))
else
SendJson(Response, 400,
TJSONObject.Create.AddPair('error', 'A and B must be integers'));
end;
procedure THelloModule.HomeAction(Sender: TObject; Request: TWebRequest;
Response: TWebResponse; var Handled: Boolean);
var
Processor: TWebStencilsProcessor;
Endpoints: TObjectList<TEndpoint>;
begin
Processor := TWebStencilsProcessor.Create(nil);
try
// (7) the page to render; it names its own layout
Processor.InputFileName := TemplateFileName('home.html');
// (8) the data -- True hands ownership to the processor
Processor.AddVar('info', TPageInfo.Create('Hello from WebStencils!'), True);
Endpoints := TObjectList<TEndpoint>.Create;
Endpoints.Add(TEndpoint.Create('/hello/HelloService/Hello',
'returns a greeting', False));
Endpoints.Add(TEndpoint.Create('/hello/HelloService/Add?A=2&B=3',
'adds two integers', False));
Endpoints.Add(TEndpoint.Create('/hello', 'this page', True));
Endpoints.Add(TEndpoint.Create('/hello/customers',
'customers from SQLite', True));
Processor.AddVar('endpoints', Endpoints, True);
// (9) Content runs the template engine and returns the HTML
Response.ContentType := 'text/html; charset=utf-8';
Response.Content := Processor.Content;
finally
Processor.Free;
end;
end;
procedure THelloModule.CustomersAction(Sender: TObject; Request: TWebRequest;
Response: TWebResponse; var Handled: Boolean);
var
Query: TFDQuery;
Processor: TWebStencilsProcessor;
City: string;
begin
City := Request.QueryFields.Values['city'];
Query := TFDQuery.Create(nil);
try
Query.Connection := FConnection;
// (11) user input only ever travels as a parameter
if City = '' then
Query.Open('SELECT id, name, city FROM customers ORDER BY name')
else
Query.Open('SELECT id, name, city FROM customers ' +
'WHERE city = :city ORDER BY name', [City]);
Processor := TWebStencilsProcessor.Create(nil);
try
Processor.InputFileName := TemplateFileName('customers.html');
Processor.AddVar('info', TPageInfo.Create('Customers'), True);
// (12) the open query IS the data -- False: the processor
// must not free it, we do that ourselves
Processor.AddVar('customers', Query, False);
Response.ContentType := 'text/html; charset=utf-8';
Response.Content := Processor.Content;
finally
Processor.Free;
end;
finally
Query.Free;
end;
end;
procedure THelloModule.NotFoundAction(Sender: TObject; Request: TWebRequest;
Response: TWebResponse; var Handled: Boolean);
begin
SendJson(Response, 404,
TJSONObject.Create.AddPair('error', 'Not found: ' + Request.PathInfo));
end;
end.