Wow! This recipe is very long! However, it summarizes all the concepts already seen in the previous recipes, so it's worth it.
The application is organized into three layers:
- Controller (TPeopleController):
- Takes care of all the machinery needed to deserialize the JSON data into Delphi objects
- Coordinates the job with the Table module
- Table Data Gateway (TPeopleModule):
- Handles all the persistence needs
- Gets objects and persists them
- Retrieves datasets and converts them into objects
- Business Objects (TPerson):
- Implements all the business logic required by the domain problem. In this sample, we don't have business logic, but if present, it should be inside the TPerson class.
When an HTTP request arrives at the server, the DMVCFramework router starts to find a suitable controller using the MVCPath attributes defined on all its controllers. When a matching controller and action is found, the request and response objects are packed in a TWebContext object and passed to the selected action. Here, we can read information from the request and build the response accordingly.
All the action methods look like the following:
- Read information from the HTTP request
- Invoke some methods on the TPersonModule instance
- Build the response for the client
Let's take a look at the following action used to create a new person:
[MVCPath]
[MVCHTTPMethod([httpPOST])]
[MVCConsumes('application/json')]
procedure CreatePerson;
. . .
procedure TPeopleController.CreatePerson;
var
Person: TPerson;
begin
//read information from the request
Person := Context.Request.BodyAs<TPerson>;
try
//invoke some methods on the TPeopleModule instance
FPeopleModule.CreatePerson(Person);
//build the response for the client
Context.Response.Location := '/people/' + Person.ID.ToString;
Render(201, 'Person created');
finally
Person.Free;
end;
end;
What's that Context.Response.Location line for? One of the RESTful features is the use of hypermedia controls. The point of hypermedia controls is that they tell us what we can do next, and the URI of the resource we need to manipulate to do it. Instead of having to know where to GET our newly created person, the hypermedia controls in the response tell us where to get the new person.
Another interesting action is mapped to the POST /people/searches. Here's the code:
[MVCPath('/searches')]
[MVCHTTPMethod([httpPOST])]
[MVCConsumes('application/json')]
procedure SearchPeople;
. . .
procedure TPeopleController.SearchPeople;
var
Filters: TJSONObject;
SearchText, PageParam: string;
CurrPage: Integer;
begin
//read informations from the requests
Filters := TSystemJSON.StringAsJSONObject(Context.Request.Body);
if not Assigned(Filters) then
raise Exception.Create('Invalid search parameters');
SearchText := TSystemJSON.GetStringDef(Filters, 'TEXT');
if (not TryStrToInt(CTX.Request.Params['page'], CurrPage))
or (CurrPage < 1) then
CurrPage := 1;
//call some method on the TPeopleModule
Render<TPerson>(FPeopleModule.FindPeople(SearchText, CurrPage));
//prepare the response (also if render has been already called)
Context.Response.CustomHeaders.Values['dmvc-next-people-page'] :=
Format('/people/searches?page=%d', [CurrPage + 1]);
if CurrPage > 1 then
Context.Response.CustomHeaders.Values['dmvc-prev-people-page'] :=
Format('/people/searches?page=%d', [CurrPage - 1]);
end;
This action is a bit longer, but the three steps are still clearly defined. This action executes a search on the people table using a pagination mechanism. The URL to get the next and the previous page are returned, along with the response in the headers dmvc-next-people-page and dmvc-prev-people-page. So, the client doesn't have to know which kind of call to do to get the second page, but can simply navigate through the returned information.
One last note about the TPersonModule is that it heavily uses the DataSet helpers introduced in the Serializing a dataset to JSON and back recipe. Look at the following code that's used to get a person by ID:
function TPeopleModule.GetPersonByID(AID: Integer): TPerson;
begin
qryPeople.Open('SELECT * FROM PEOPLE WHERE ID = :ID', [AID]);
//uses the dataset helper to convert a record to an object
Result := qryPeople.AsObject<TPerson>;
end;
It could not be simpler! Also, the method to create a new person is made really simple by using some of the TFireDACUtils methods:
procedure TPeopleModule.CreatePerson(APerson: TPerson);
var
InsCommand: TFDCustomCommand;
begin
//gets the Insert statement contained in the TFDUpdateSQL
InsCommand := updPeople.Commands[arInsert];
//Maps the object properties to the command parameters
TFireDACUtils.ObjectToParameters(InsCommand.Params, APerson, 'NEW_');
//execute the statement
InsCommand.Execute;
//retrieve the last assigned ID
APerson.ID := Conn.GetLastAutoGenValue('gen_people_id');
end;