We are now ready to make our work more practical. We have seen different approaches in using ORM frameworks, and we have a good understanding of the fundamentals of Aurelius. In this chapter, we work on setting up the building blocks that will allow us to see Aurelius in practice.
The Application
For the purpose of this book, we are going to develop the back end and the code of the database layer of an application that manages data from a call center. The data set and the dashboard come from Trump Excel (Bansal, 2019). Sumit Bansal has created a great site on Excel and offers data from a call center free to use. I have modified the original data file slightly by adding some columns that are more suitable for our examples.
The call center provides support to customers of a business that has five departments (air conditioner, fridge, television, toaster, washing machine), and the center employs eight agents who handle the calls (Becky, Dan, Diane, Greg, Jim, Joe, Martha, Stewart).
- 1.
A customer calls the center. The IT system assigns a Call ID to the call and records the date (Date) and the time the call reaches the center (Queue Enter Time).
- 2.
The call stays in a waiting queue until an agent is available. When an agent is released, the call is transferred to the Agent and the system records the time (Queue Exit Time) of transfer. At the same time, the call is flagged as Answered.
- 3.
The agent deals with the call for as long as it is necessary (Call Duration) and records the Department that the call is relevant to.
- 4.
At this stage, the call can be either Resolved or not.
- 5.
In the end, the caller rates the agent in a satisfaction scale from 1 to 5 (Satisfaction Rating).
- 6.
Sometimes, a call may be transferred to an agent, but for some reasons (technical or others) the call drops. In this case, the call is logged as both not Answered and not Resolved.
The application has three panels: the first one holds the Dashboard, the second one the Department management, and the last one the Agent management.
Dashboard
A panel with the overall satisfaction score, the total calls for the period of one month, the average answer speed (sec), the abandon rate (%), and the calls per minute
A list with the statistics about the agents: total calls, answered calls, average speed of answer(sec), call resolution (%), and call resolution trend
A chart with the call abandon rate by department
A chart with the satisfaction score per agent
A panel with the service-level agreement limits regarding the number of calls answered in less than 180 seconds and the number of calls with satisfaction score of less than 3
Departments
There is another grid that fills in the rest of the form. When a department is selected in the top grid, the user can see the calls filtered by the selected department.
Agents
The Interface
It’s time we build our application. We start with the user interface by creating the forms we need, as in the preceding figures. The forms include a series of grids, layouts, tabs, and buttons. I am not going to go through the details of how to build the GUI because it will take unnecessary space and will distract us from the main purpose of this book. You can find a skeleton project that we will use in this and the next chapters in the code that accompanies this book. The project is named CallCentreSkeleton.
The dashboard has a sidebar that filters the data per week. We will update the data in updateDashboard, which is called in the OnChange event of the TabControl.
We will add update methods for the agents and the departments in later chapters.
There is a setupGUI method that initializes the GUI the first time the application runs.
The skeleton project is runnable. Open the project and just run it. In the main form (TFormMain in MainForm.pas), you can click the buttons in the sidebar, change the tabs, and click the “Add Agent” or “Add Department” buttons in the corresponding tabs. You should be able to see a separate form (TFormEntity in EntityForm.pas) in the project. We will start adding up new code shortly to the project, so you may wish to save it under a new name if you would like to keep the skeleton project intact.
Note
In our code, we will not adhere to any specific design pattern such as MVP/MVC/MVVM or any other similar approaches. Our priority is to write code that demonstrates Aurelius features rather than respect the principles of any design styles.
Entities
ID: A property that holds a GUID as primary key
Description: A property that holds the name of the agent or the name of the department
- 1.
Open the CallCentreSkeleton project or create a copy.
- 2.
Add a new unit and save it under the name Entities.pas. This unit will hold all the attribute mapping of our classes.
- 3.In Entities.pas , add the following two classes:usesSysUtils,Aurelius.Mapping.Attributes,Aurelius.Types.Nullable,Aurelius.Types.Proxy;type[Entity][Table('Agent')][Id('FID', TIdGenerator.Guid)]TAgent = classprivate[Column('ID', [TColumnProp.Required])]FID: TGuid;[Column('Description', [TColumnProp.Required], 255)]FDescription: string;[Column('Photo', [TColumnProp.Lazy])]FPhoto: TBlob;publicproperty ID: TGuid read FID write FID;property Description: string read FDescription write FDescription;property Photo: TBlob read FPhoto write FPhoto;end;[Entity][Table('Department')][Id('FID', TIdGenerator.Guid)]TDepartment = classprivate[Column('ID', [TColumnProp.Required])]FID: TGuid;[Column('Description', [TColumnProp.Required], 255)]FDescription: string;publicproperty ID: TGuid read FID write FID;property Description: string read FDescription write FDescription;end;
I decorated the two classes with the very basic attributes to let Aurelius know that I treat TDepartment and TAgent as entities. I have also indicated that FID private field works as the primary key at the database level, uses Smart GUID engine, and is linked to column ID. Similarly, I have introduced the Description property to be of string type. Then, I linked it back to the relevant field, which I made compulsory at database level using the TColumnProp.Required parameter and set the length to an arbitrary value. The mapping of these two classes is straightforward and in line with what we discussed in Chapter 2.
- 4.The TCall class is more complicated as it holds more fields both compulsory and nullable; however, the principles to provide information to Aurelius are as before. In the same unit, add the following class:...type...[Entity][Table('Call')][Id('FID', TIdGenerator.SmartGuid)]TCall = classprivate[Column('ID', [TColumnProp.Required])]FID: TGuid;[Column('Date', [TColumnProp.Required])]FDate: TDateTime;[Column('QueueEntryTime', [TColumnProp.Required])]FQueueEntryTime: TDateTime;[Column('QueueExitTime', [])]FQueueExitTime: Nullable<TDateTime>;[Column('ServiceStartTime', [])]FServiceStartTime: Nullable<TDateTime>;[Column('ServiceEndTime', [])]FServiceEndTime: Nullable<TDateTime>;[Column('Answered', [TColumnProp.Required])]FAnswered: Integer;[Column('Resolved', [TColumnProp.Required])]FResolved: Integer;[Column('SatisfactionRate', [])]FSatisfactionRate: Nullable<Integer>;[Column('CallID', [TColumnProp.Required], 50)]FCallID: string;publicproperty ID: TGuid read FID write FID;property Date: TDateTime read FDate write FDate;property QueueEntryTime: TDateTime read FQueueEntryTime write FQueueEntryTime;property QueueExitTime: Nullable<TDateTime> read FQueueExitTime write FQueueExitTime;property ServiceStartTime: Nullable<TDateTime> read FServiceStartTime write FServiceStartTime;property ServiceEndTime: Nullable<TDateTime> read FServiceEndTime write FServiceEndTime;property Answered: Integer read FAnswered write FAnswered;property Resolved: Integer read FResolved write FResolved;property SatisfactionRate: Nullable<Integer> read FSatisfactionRate write FSatisfactionRate ;property CallID: string read FCallID write FCallID;end;The initial design of our application requires the ability to indicate that a specific call is assigned to a particular agent and refers to a specific department. At class level, these two requirements are resolved by introducing two properties in TCall holding a TAgent and TDepartment classes, respectively.type...TCall = classprivate...[Association([TAssociationProp.Lazy], CascadeTypeAll - [TCascadeType.Remove])][JoinColumn('AgentID', [], 'ID')]FAgentID: Proxy<TAgent>;[Association([TAssociationProp.Lazy], CascadeTypeAll - [TCascadeType.Remove])][JoinColumn('DepartmentID', [], 'ID')]FDepartmentID: Proxy<TDepartment>;function GetAgentID: TAgent;procedure SetAgentID(const Value: TAgent);function GetDepartmentID: TDepartment;procedure SetDepartmentID(const Value: TDepartment);public...property AgentID: TAgent read GetAgentID write SetAgentID;property DepartmentID: TDepartment read GetDepartmentID write SetDepartmentID;end;...function TCall.GetAgentID: TAgent;beginresult := FAgentID.Value;end;procedure TCall.SetAgentID(const Value: TAgent);beginFAgentID.Value := Value;end;function TCall.GetDepartmentID: TDepartment;beginresult := FDepartmentID.Value;end;procedure TCall.SetDepartmentID(const Value: TDepartment);beginFDepartmentID.Value := Value;end;
The corresponding private fields are where we define how Aurelius should manage them at database level. The two properties AgentID and DepartmentID are lazy loaded as the TAssociationProp.Lazy parameter is used.
The code, also, dictates what happens to the associated agent or department when a call is updated or deleted. The CascadeTypeAll - [TCascadeType.Remove] (or the equivalent CascadeTypeAllButRemove) parameter tells Aurelius that, in any other actions than removal (deletion) of a call, the department or the agent is updated. In other words, if the user deletes a call from the database, the linked agent and department records will be intact.
Lastly, the getter and setter methods for AgentID and DepartmentID are straightforward. As discussed in the previous chapter, we have to use this approach, as the corresponding fields are declared as proxies.
The preceding modifications allow us to link calls to agents and departments. It would be very convenient if we achieve the opposite as well. Given an agent (or a department), we would like to be able to drill down to all the calls they are associated with. Since Aurelius is aware of all the entities we need, we can reverse the associations by mapping properties using the ManyValuedAssociation attribute :uses...,Generics.Collections;...type...TAgent = classprivate...[ManyValuedAssociation([TAssociationProp.Lazy], CascadeTypeAll, 'FAgentID')]FCallList: Proxy<TList<TCall>>;function GetCallList: TList<TCall>;public...constructor Create;destructor Destroy; override;property CallList: TList<TCall> read GetCallList;end;TDepartment = classprivate...[ManyValuedAssociation([TAssociationProp.Lazy], CascadeTypeAll, 'FDepartmentID')]FCallList: Proxy<TList<TCall>>;function GetCallList: TList<TCall>;public...constructor Create;destructor Destroy; override ;property CallList: TList<TCall> read GetCallList;end;...constructor TAgent.Create;begininherited;FCallList.SetInitialValue(TList<TCall>.Create);end;destructor TAgent.Destroy;beginFCallList.DestroyValue;inherited;end;function TAgent.GetCallList: TList<TCall>;beginresult := FCallList.Value;end;constructor TDepartment.Create;begininherited;FCallList.SetInitialValue(TList<TCall>.Create);end;destructor TDepartment.Destroy;beginFCallList.DestroyValue;inherited;end;function TDepartment.GetCallList: TList<TCall> ;beginresult := FCallList.Value;end;We define CallList as a typical TList<T> property, and we pass it to Aurelius using the ManyValuedAssociation attribute. The field defines a lazy-loaded association which updates and merges any changes with the associated object. Lastly, the fields are nongeneric (TList) and proxified (Proxy); we need to use SetInitialValue and DestroyValue methods from Aurelius to manage their lifetime.
We need to make sure that the compiler includes all the classes in the final binary file. This is easily done by adding the following code in the initialization section of Entities.pas :unit Entities;interface...implementation...initializationRegisterEntity(TAgent);RegisterEntity(TDepartment);RegisterEntity(TCall) ;end.
Database Connection
- 1.
Run the wizard in File ➤ Other… ➤ TMS Business ➤ TMS Aurelius DB Connection.
- 2.
In the wizard, select Native direct connection (Driver Mode) and SQLite in the Driver pop-up menu.
- 3.
Click Finish and allow Aurelius to add a new Data Module in the project under the name ConnectionModule.pas.
- 4.
Open the module in the design editor, right-click AureliusConnection1 component and, in Connection Settings, add the database.db as the name of the database file. Then, close the form with the settings.
- 5.
The ConnectionModule unit exposes a global variable named SQLiteConnection which provides reference to Aurelius connection. Although having a global variable is not considered the best way to write code, in our case it is sufficient.
- 6.In MainForm.pas, add the following code:
We first create an instance of the connection module. This provides access to the IDBConnection interface which is required every time we want to do operations on the database. Then, we define a local variable of TDatabaseManager which is used to update the database. You can notice that in order to create the database manager, I had to pass an instance of IDBConnection. This is supplied by the ConnectionModule as it is demonstrated in the preceding code.
In the code, I call dbManager.UpdateDatabase as the first thing that the application should do. UpdateDatabase retrieves the schema from the database and compares it with the current entity structure (virtual database objects) as defined by the mapping attributes. Then, it executes SQL statements to synchronize the database structure with the virtual database objects.
Note
UpdateDatabase is not destructive. This means that if you delete some properties from an entity, Aurelius will not delete the corresponding columns in the database. This is something you need to do manually via SQL scripts.
The official manual indicates that the database manager provides another procedure that allows the creation and update of the database: BuildDatabase . This method is considered deprecated but still can be in use in code. The end result is the same as it updates the database schema as well; however, there is a slight difference. BuildDatabase does not perform reverse engineering of the database schema and any comparisons with the virtual database object; it, rather, starts executing the SQL statements based on the current state of code in the entities. If your database is really huge with hundreds of tables and columns, this approach may save you a few second which, in reality, may not even be noticeable. In any case, the recommended and modern way to update the database is to use UpdateDatabase.
IDatabaseSession
- 1.
Add a new unit in the project and save it under the name Database.Session.Types.pas.
- 2.Then, add the following code:unit Database.Session.Types;interfaceusesAurelius.Engine.DatabaseManager,Aurelius.Engine.ObjectManager;typeIDatabaseSession = interface['{7CA1B4A1-F339-47EE-AE17-9436853A618E}']function databaseManager: TDatabaseManager;function objectManager: TObjectManager;end;implementationend.
- 3.
Add a new unit in the project as Database.Session.pas.
- 4.Add the next code snippet:unit Database.Session;interfaceusesDatabase.Session.Types,Aurelius.Engine.DatabaseManager,Aurelius.Drivers.Interfaces,Aurelius.Engine.ObjectManager;typeTDatabaseSession = class (TInterfacedObject, IDatabaseSession)privatefConnection: IDBConnection;fDatabaseManager: TDatabaseManager;fObjectManager: TObjectManager;publicconstructor Create(const aConnection: IDBConnection);destructor Destroy; override;{$REGION 'Interface'}function databaseManager: TDatabaseManager;function objectManager: TObjectManager;{$ENDREGION}end;implementationconstructor TDatabaseSession.Create(const aConnection: IDBConnection);beginAssert(aConnection <> nil);inherited Create;fConnection:=aConnection;end;function TDatabaseSession.databaseManager: TDatabaseManager;beginif not Assigned(fDatabaseManager) thenfDatabaseManager:=TDatabaseManager.Create(fConnection);Result:=fDatabaseManager;end;destructor TDatabaseSession.Destroy;beginfDatabaseManager.Free;fObjectManager.Free;inherited;end;function TDatabaseSession.objectManager: TObjectManager;beginif not Assigned(fObjectManager) thenfObjectManager:=TObjectManager.Create(fConnection);Result:=fObjectManager;end;end.
There are a number of things happening in this unit. First, in Create, we check if the passed IDBConnection is valid. This is not relevant to Aurelius, but it is good practice to always check the injected parameters. Then, we store the connection for future use. The functions that return both the database manager and the object manager check if there is already a valid instance of the relative manager stored in the object. If this is the case, then they return the instance; otherwise, they create a new one.
- 5.In MainForm.pas, under the FormCreate event, we can now use the IDatabaseSession interface:uses...,Database.Session.Types,Database.Session;...procedure TFormMain.FormCreate(Sender: TObject);vardbSession: IDatabaseSession;beginSQLiteConnection:=TSQLiteConnection.Create(self);dbSession:=TDatabaseSession.Create(SQLiteConnection.CreateConnection);dbSession.databaseManager.UpdateDatabase;setupGUI;end;
This looks simpler and more efficient. We still need to pass the IDBConnection to the constructor, and then we get access to the database manager and the object manager via the functions as exposed by the interface. In this instance, we do not need to free the objects.
If you are looking for more complete code, a try-except branch should wrap the call to UpdateDatabase, but I do not include it in this code and in the following examples purely for simplicity.
Summary
In this chapter, we set up the background to explore Aurelius further. The example application is introduced (CallCentre), and we defined the entities and the database connection as per our design and requirements. Furthermore, the IDatabaseSession interface is developed. In the next chapter, we will start using this interface by executing simple tasks.
Reference
Bansal, S., 2019. Trump Excel. [Online] Available at: https://trumpexcel.com/call-center-performance-dashboard-excel/ [Accessed 01 04 2019].