© John Kouraklis 2019
John KouraklisIntroducing Delphi ORMhttps://doi.org/10.1007/978-1-4842-5013-6_2

2. TMS Aurelius

John Kouraklis1 
(1)
London, UK
 

TMS Aurelius is an ORM framework for Delphi developers. The framework is written in Delphi, and it is a proprietary product developed by TMS Software. Aurelius has been available in the market for a number of years, and it enjoys a good customer base. The framework plays its part in a wide range of applications with different scope and different requirements. Aurelius is not limited to Windows platforms, and it can work efficiently on macOS, iOS, Android, and Linux, and it supports all Delphi versions from XE2 to the latest.

In this chapter, we start clean and install Aurelius from scratch. Then, the chapter explores some fundamental concepts (database connectivity, SQL dialects, etc.) that allow us to use Aurelius, and we look at our first code snippets that put Aurelius to work for us.

Installation

Aurelius comes in two versions: a trial version and a licensed one. The difference is that with the trial version you only get the DCU files; it is intended for noncommercial use and it expires at some point. There is a free version as well which does not expire and you can use it for commercial applications, but there are some limitations. For more information about the free version, contact TMS Software directly. The next steps guide you how to install the trial and the licensed versions.

Trial Version

In you want to install the trial version, follow the next steps:
  1. 1.

    Go to the product’s web page on TMS Software site at this address https://tmssoftware.com/site/aurelius.asp or visit https://tmssoftware.com and select “Business Tools” from the tiles with their products. This will load a page with several interesting pieces of information. At the bottom of the page, you can find the link to access the Aurelius web page.

     
  2. 2.
    In Aurelius page, scroll at the bottom of the page. You will be able to see a grayed area with download links for different Delphi versions (Figure 2-1). Select the link for the Delphi version you use and download the file in your computer.
    ../images/481234_1_En_2_Chapter/481234_1_En_2_Fig1_HTML.jpg
    Figure 2-1

    TMS Aurelius trial download links

    Note In the same page you can also find the technical manual, as shown in Figure 2-1. This is also copied by the installer, but if you wish to have the manual without installing the package, download it from the link.

     
  3. 3.

    The downloaded file is a compressed zip file that contains all the products in the TMS Business Tools package. Extract the files and execute the installer.

     
  4. 4.

    Let the installer complete the installation of all packages.

     
  5. 5.

    If you haven’t changed the default location, Aurelius is now installed in Documents mssoftwareusinessrioAurelius.

    Note: The businessrio part in the preceding path indicates the Delphi version you install the package for. In my case, I installed Aurelius for Delphi 10.3 Rio.

     
  6. 6.
    Now, you can launch Delphi and check that the package has been installed correctly. In the splash screen, you should be able to see the BIZ logo and a reference to the package similar to Figure 2-2.
    ../images/481234_1_En_2_Chapter/481234_1_En_2_Fig2_HTML.jpg
    Figure 2-2

    Splash screen item for Aurelius trial version (Delphi 10.3 Rio)

     
  7. 7.
    Additionally, if you look at the OptionsLibrary paths for different platforms, you should be able to see the paths to the compiled packages (Figure 2-3).
    ../images/481234_1_En_2_Chapter/481234_1_En_2_Fig3_HTML.jpg
    Figure 2-3

    Library paths for Aurelius trial version (Win32)

     
  8. 8.

    Now, you are ready to use Aurelius in your Delphi programs.

     

Note

The trial version of the package installs compiled .dcu units for all the available platforms and for the Release configuration. Source files for several database drivers have also been installed. Additionally, you can find Demos and Documentation in their respective folders.

Licensed Version

If you are a registered user with TMS Software, you can log in to your account in their web site. In your account, you are able to download all the products you have purchased license for. Note that licensed installers build packages for all the installed Delphi version in your system, eliminating the need to run separate installation for each Delphi IDE.

Prerequisite Packages

TMS Aurelius requires the TMS Business Core Library (BCL) to be installed in the system. Download the compressed setup file from your account, unzip it, and install the package.

Installation

The following steps describe how you can install the registered version of TMS Aurelius:
  1. 1.

    Make sure TMS Business Core Library (BCL) is installed (see previous section).

     
  2. 2.
    Download the compressed setup file for Aurelius from your account, unpack it, and run the installer (Figure 2-4).
    ../images/481234_1_En_2_Chapter/481234_1_En_2_Fig4_HTML.jpg
    Figure 2-4

    Registered installer of TMS Aurelius

     
  3. 3.
    The next page in the installer invites you to accept the license, and then you have to enter your registration email and code (supplied by TMS). This step requires your system to be able to access the Internet as the installer validates your license online.
    ../images/481234_1_En_2_Chapter/481234_1_En_2_Fig5_HTML.jpg
    Figure 2-5

    License page in TMS Aurelius installer

     
  4. 4.

    Verify the settings that are presented in the following pages of the installer and allow it to copy all the necessary files to your system.

     
  5. 5.

    When all files are copied, the installer will launch the Package Rebuild Tool to build the packages for all the installed Delphi versions and all available releases. For more details, see the next section.

     
  6. 6.

    When the building of the package finishes, Aurelius is installed and it is ready to use. You should be able to see the TMS Business product line logo in the Delphi splash screen as in Figure 2-2 and also inspect the relevant library paths.

     

Package Rebuild Tool

The Package Rebuild Tool is a stand-alone application that is installed with TMS Aurelius. You can find it in the Start Menu group and launch it independently. The installer, as mentioned earlier, uses this tool to generate the binaries of the Aurelius packages, but you can use it every time you want to recompile the packages for any reasons. Corrupted .dcu files or updates to the IDE may lead to unusable binary packages. In such cases and in the cases where you modify the source code directly or receive patches sent by TMS Software, you can manually rebuild the packages.

When the tool is launched, it scans for the installed Delphi IDE environments and presents them in the top half of the window of the application (Figure 2-6). You can use the checkboxes to select or deselect the IDEs according to your preference.
../images/481234_1_En_2_Chapter/481234_1_En_2_Fig6_HTML.jpg
Figure 2-6

The Package Rebuild Tool

At the bottom half of the window, the available platforms are shown, and you can select which ones you would like to install or rebuild the packages for. This part of the user interface can be confusing as the design indicates that the platform selection is independent to the Delphi version. For example, if I want to build the packages for OSX32 for Delphi 10.2 but not for Delphi 10.3, I need to rebuild the packages twice; one for 10.2 with OSX32 selected and one for 10.3 with OSX32 deselected.

Note

Figure 2-6 demonstrates a very common situation with the available platform. Android and Linux are not enabled because the tool identifies these platforms either as wrongly configured or not available. In my system, Linux is not available as built platform and Android is not properly configured. If you have the same situation, check the SDK Manager in Delphi, correct any errors that appear, and rerun the Package Rebuild Tool.

Click Install/Rebuild and allow the application to finish the building of the packages (Figure 2-7). This step may take considerable amount of time to complete.
../images/481234_1_En_2_Chapter/481234_1_En_2_Fig7_HTML.jpg
Figure 2-7

The Package Rebuild Tools compiling for several Delphi versions and platforms

TMS Subscription Manager

TMS Software offers a simpler and more automated way to manage any packages you have purchased. As a registered user, you have access to a tool called TMS Subscription Manager. This tool allows you to keep track of the installed packages and see new versions, and it facilitates the installation (and uninstallation) of the packages.

In the case of Aurelius and Business Core Library, the Subscription Manager would look like in Figure 2-8. The Manager shows that there are new versions available and they can be, automatically, downloaded and installed.
../images/481234_1_En_2_Chapter/481234_1_En_2_Fig8_HTML.jpg
Figure 2-8

TMS Subscription Manager

Database Connectivity

Aurelius can connect to a number of database engines. The framework requires the following elements in order to manage an underlying database:
  • A component that actually allows Aurelius to access the database. There are two ways for this to be defined:
    • Using an adapter (Adapter Mode): The adapter works as a proxy between Aurelius and a database, and it requires components by the providers of the adapter to be installed and configured properly. This option provides high flexibility and great degree of customization of the connection. FireDAC component belongs in this group.

    • Using a native driver (Driver Mode): This mode provides a convenient way to use a database in Aurelius without the need to install or employ any third-party software as in the adapter mode. Although the flexibility in customization is not as high as in adapter mode, this mode is a very convenient and quick way to set up the database connectivity of the framework. In the version of Aurelius at the time of writing, this mode allows native connectivity to SQLite databases and databases hosted by Microsoft SQL Server.

  • An SQL Dialect: When Aurelius gets the connection, it needs to know which variation of the SQL language should be used to communicate with the database. This information is passed by supplying an SQL dialect during the database connectivity setup stage. The ability to define the dialect is exposed to the developers only in the case of the adapter mode. In the native driver mode, Aurelius can work out the required dialect by looking at the selected native driver.

Table 2-1 shows which components can be used to connect to specific databases. According to Aurelius manual, these are the combinations that TMS has tested and officially supports. As the table indicates, there are components that can be used to provide access to multiple databases. This can be very beneficiary and convenient in cases where there is a database change as a matter of infrastructure or your application requires to connect to different databases. You can save writing lots of lines of code and time if you know that a specific adapter works with different databases. Aurelius technical manual provides a full list of the available adapters, SQL dialects, native drivers, and which component and database versions are supported.
Table 2-1

Databases and Connection Adapter Compatibility (Homologation) in TMS Aurelius

Adapter

Database

Native

Absolute

AnyDAC

dbExpres

dbGo

DOA

ElevateDB

FireDAC

FIBPlus

IBO

IBX

NativeDB

NexusDB

SQLDirect

UniDAC

UIB

ZeosDB

AbsoluteDB

 

               

IBM DB2

  

 

  

     

  

ElevateDB

      

          

FireBird (including FireBird 3)

  

   

    

Interbase

  

   

 

  

 

MS SQL Server

 

  

     

 

MySQL

  

   

     

 

NexusDB

            

 

  

Oracle

  

 

 

     

 

PostgreSQL

  

    

      

  

SQLAnywhere

       

   

     

SQLite

 

    

      

  

IDBConnection

In terms of coding, a connection to a database in Aurelius is represented by the IDBConnection interface . This interface is perhaps one of the most fundamental data structures in Aurelius as it makes the database operations happen. The adapters or the drivers discussed in the previous section are used to create a valid IDBConnection interface. In turn, this means that an IDBConnection is tied to an adapter or a driver and not to a database engine (although when you use an adapter that can access only one database, then the IDBConnection is ultimately tied to one database as well), and this is

where the value of having adapter that are able to access multiple databases appears. There are three ways to instantiate an IDBConnection interface:
  1. 1.

    Using pure code

     
  2. 2.

    Using the TAureliusConnection component

     
  3. 3.

    Using the TMS Aurelius DBConnection wizard in RAD IDE

     

Using Code

To demonstrate how we can get an IDBConnection programmatically, we’ll create a connection to an SQLite database. Table 2-1 indicates that this can be achieved in Aurelius by either using a database adapter or natively. For this example, we are going to create an in-memory SQLite database.

Using an Adapter
Let’s start with an adapter:
  1. 1.

    Create a new project in Delphi. It can be a VCL, FMX, or console application, but it is much easier if you choose a VCL or FMX project.

     
  2. 2.

    Drop a FireDAC connection (TFDConnection) in the form (FDConnection1).

     
  3. 3.
    Select the connection component, right-click, and launch the connection editor (Figure 2-9). Select SQLite in Driver ID field and enter “:memory:” in the Database field as in the figure and choose OK.
    ../images/481234_1_En_2_Chapter/481234_1_En_2_Fig9_HTML.jpg
    Figure 2-9

    FireDAC Connection Editor for in-memory SQLite database

     
  4. 4.

    Click the OnCreate event of the form and write the following code:

     
../images/481234_1_En_2_Chapter/481234_1_En_2_Figa_HTML.png

After the call to the TFireDacConnectionAdapter constructor , the fConnection property holds a reference to the database and can be used to perform operations.

Using Native Drivers
As mentioned earlier, Aurelius can provide native support to SQLite databases. This means that there is no need to create a FireDAC connection. Aurelius can access directly the SQLite database as shown in the code that follows:
...
implementation
uses
  Aurelius.Drivers.SQLite;
...
procedure TForm1.FormCreate(Sender: TObject);
begin
  fConnection:=TSQLiteNativeConnectionAdapter.Create(':memory:');
end;

Note

The native component is very useful as it requires the minimal possible code to generate a database connection and does not require any third-party components. However, it does not provide a way to access encrypted SQLite databases. If you want to achieve this, you need to use a FireDAC component.

Using the TAureliusConnection Component

Aurelius provides a design-time component that can be used with a FireDAC connection.
  1. 1.

    Create a new project in Delphi.

     
  2. 2.

    Drop a FireDAC connection (TFDConnection) in the form (FDConnection1) as before.

     
  3. 3.
    Drop a TAureliusConnection component in the form (AureliusConnection1). You should be able to see two components as in Figure 2-10.
    ../images/481234_1_En_2_Chapter/481234_1_En_2_Fig10_HTML.jpg
    Figure 2-10

    FireDAC and Aurelius connections in a form

     
  4. 4.
    Select the Aurelius connection component, right-click, and select “Connection Settings…”. In the editor make the necessary options to connect to the FireDAC component (Figure 2-11) and select OK. You can also test the connection using the button at the bottom of the form.
    ../images/481234_1_En_2_Chapter/481234_1_En_2_Fig11_HTML.jpg
    Figure 2-11

    TAureliusConnection Connection Editor

     
  5. 5.
    Retrieve the IDBConnection using the following code:
    procedure TForm1.FormCreate(Sender: TObject);
    begin
      fConnection:=AureliusConnection1.CreateConnection;
    end;
     

Using the TMS Aurelius DBConnection Wizard

TMS provides an even easier way to generate a database connection. Aurelius installer adds a DBConnection wizard in the File➤New➤Other➤TMS Business menu in the RAD Studio.
  1. 1.

    Go to FileNewOtherTMS Business in Delphi and select the TMS Aurelius DBConnection wizard as in Figure 2-12.

     
  2. 2.

    In the next step (Figure 2-13), choose whether you want to connect to the database using an adapter or a native driver. For this example, I opted for the native driver to generate an SQLite database.

     
  3. 3.

    The wizard creates a unit (ConnectionModule.pas) with a data module and a TAureliusConnection attached to it. The name of the module is after the connection. In my case, it is called TSQLiteSQLiteConnection and the associated variable is SQLiteSQLiteConnection. This naming convention may appear confusing to the Delphi developer as the name implies that the underlying object is a connection when it is, actually, a data module. This may not be a big problem because the only real need for this module is to generate an IDBConnection instance once. In a typical application, this will most likely be done at the beginning of the application, and the interface will be injected as needed.

     
  4. 4.

    Once the data module is created and the Aurelius connection has been configured, the connection interface can be retrieved as in the following code. Note that the wizard assigns a variable to the data module, but it is not instantiated. This needs to be done manually.

     
../images/481234_1_En_2_Chapter/481234_1_En_2_Fig12_HTML.jpg
Figure 2-12

TMS Aurelius DBConnection option in Delphi

../images/481234_1_En_2_Chapter/481234_1_En_2_Fig13_HTML.jpg
Figure 2-13

The options in DBConnection wizard for the adapter and the driver modes

../images/481234_1_En_2_Chapter/481234_1_En_2_Figb_HTML.png

ORM Paradigm in Aurelius

In the previous chapter, the ORM building elements were discussed (entity, properties, associations, criteria, projections, container). In order for Aurelius to implement these elements, the framework defines a number of attributes that can be used.

The attributes determine how the virtual object database in Aurelius is defined and managed and how the operations at the database level are performed. They also work as indicators in the code. If you browse through the code, you can understand the ORM structure by observing the attributes. To demonstrate the use of attributes in Aurelius, we will continue with the blog example from the first chapter.

Note

If you are not familiar with attributes in Delphi or you would like a refresher, visit the official Delphi documentation (Embarcadero, n.d.). For a more detailed treatment of attributes, you may find useful to check Nick Hodges’ book (Hodges, 2014).

Entity

Entities in ORM frameworks are represented by classes and their instances. In Aurelius, you declare a class to be an entity for the framework and, therefore, persistent in the database, by using the attribute [Entity]. In this case, the TPost class is declared to be an entity by, simply, writing the following code:
[Entity]
TPost = class
private
  FCategories: TList<string>;
  FContent: string;
  FTags: TList<string>;
  FUser: TUser;
public
  property Categories: TList<string> read FCategories write FCategories;
  property Content: string read FContent write FContent;
  property Tags: TList<string> read FTags write FTags;
  property User: TUser read FUser write FUser;
end;
This attribute tells Aurelius that TPost class exists in the underlying database. The next step is to decorate the class with the [Table] attribute and the desired name of the table.
[Entity]
[Table('Posts')]
TPost = class
  ...
end;

Aurelius knows that it has to include in the virtual object database any class with the [Entity] attribute. However, at the initial stages of your code and if you follow the model-first or code-first workflow, you may declare classes that are not used anywhere in the code yet. This results in the classes being removed during the compilation and linking phase in Delphi.

As a result, those classes will not appear as tables in the underlying databases as Aurelius will not know that those classes exist. Initially, this may not pose a real concern because the next time you use the class, the linker will include it to the executable and Aurelius will update the database schema accordingly. Problems may arise when associations exist between classes that represent association endpoints where one endpoint is used in the application and another one is not yet. This will make the final executable to link only the classes from the endpoints that are used, leaving the associations at Aurelius level in nonfunctional state.

In order to avoid this situation, which, admittedly, may be a headache to debug, best practice indicates that you register the classes right at the beginning of the application. This is typically done by using the RegisterEntity procedure in the initialization section of a unit or as the very first line an application executes. For the TPost entity , this would look like the following code:
uses
  ...,
  Aurelius.Mapping.Attributes;
type
  [Entity]
  [Table('Posts')]
  TPost = class
    ...
  end;
...
initialization
...
  RegisterEntity(TPost) ;
...
end.

Properties

Properties in ORM represent columns in database tables and are mapped in typical class properties in Delphi implemented by either a (usually private) field or a field and a getter and setter method. From Aurelius’ point of view, properties (with the exception of associations) can be of four types:
  1. 1.

    Identifier

     
  2. 2.

    Column

     
  3. 3.

    Column (NULL)

     
  4. 4.

    Blob

     

Identifier

Each entity in Aurelius should be, uniquely, identified in the framework as a record is, uniquely, identified in a table by a primary key. This is done by declaring a property of the class to use the [Id] attribute. The most common approach here is to allow the database to manage the generation of the identifier value as in the case of a primary key. This is not restrictive as you can take control of the values of the identifier and manage it in your code. Typical values include sequential numbers, but for our TPost I will use a GUID as identifier as this approach is gradually becoming more common among database engineers.

../images/481234_1_En_2_Chapter/481234_1_En_2_Figc_HTML.png

The code defines a new property ID of TGUID type and the corresponding private variable (fID). The [Id] attribute is used to declare to Aurelius that fID is the variable to be used as the identifier of the class and that Aurelius will need to generate GUID identifiers. There are more identifier engines available, and they are discussed in the manual.

If you need to declare composite identifiers in your database, just add as many [Id] properties as required. In that case, Aurelius will completely manage the generation of the identifier so the engine in the attribute should be TIdGenerator.None. However, broadly speaking, the official manual suggests that composite identifiers should be avoided.

Column

Each property in a class that needs to be persistent in the database corresponds to a column in the equivalent table. In Aurelius, the [Column] attribute is used to declare a column. When an identifier is declared, the column in the table should never be empty (null). This is passed to Aurelius by using the TColumnProp.Required in the declaration of the [Column] property and can be seen in the preceding code snippet. As an example, we are going to add a property DateTime in TPost of TDateTime.
[Entity]
[Table('Post')]
[Id('FID', TIdGenerator.SmartGuid)]
TPost = class
private
  ...
  [Column('DateTime', [TColumnProp.Required])]
  fDateTime: TDateTime;
  ...
public
  ...
  property DateTime: TDateTime read FDateTime write FDateTime;
  ...
end;

Note that the DateTime field has been declared as NOT NULL (required) in the database. If you want to declare the length of a field or its precision for numeric fields (columns), the [Column] attribute receives parameters that can accommodate these requirements. The manual provides more details on this.

Column (NULL)

In databases, columns can be empty (NULL), a concept that is different from empty fields in classes. For example, in TPost, the Content field is a string. An empty string field in the class yields length of 0; however, a null column in the database table does not as it does not have any content. This situation is managed by Aurelius with the introduction of the generic record Nullable<T> and it is used as in the following code. Note that I have declared the length of the column to 65535 which is the maximum value for VarChar in SQLite, but this seems a poor choice for full-scale production database.

../images/481234_1_En_2_Chapter/481234_1_En_2_Figd_HTML.png

Blob

Databases are capable of storing binary large objects (blobs) , and Aurelius naturally provides support to such data types by introducing the TBlob record. This is pretty straightforward to use in an entity. As an example, we introduce an Image property in TPost to hold a picture related to the post.
uses
  ...,
  Aurelius.Types.Blob;
...
[Entity]
[Table('Post')]
[Id('FID', TIdGenerator.SmartGuid)]
TPost = class
private
  ...
  [Column('Image', [TColumnProp.Lazy])]
  FImage: TBlob;
public
  ...
  property Image: TBlob read FImage write FImage;
end;

The [Column] attribute provides a way to affect the behavior of TBlob. In the example code, the FImage field will not be loaded immediately when a Post instance (record) is retrieved in the code (lazy loading); instead the content will become available the very first moment the Image property is accessed. This is common behavior with blobs. We will also see this behavior again when we deal with associations.

Associations

As discussed in the previous chapter, associations are formed between endpoints (properties in entities). Aurelius implements two types of associations: one-to-many and many-to-one. If you need to implement one-to-one associations, you need to handle this by code.

According to our design, a user in our blog case owns several posts. From the user’s point of view, this is a one-to-many association. On the other hand, looking at the situation from a post and considering the previous association, the relationship between a post and a user is many-to-one (Figure 2-14).
../images/481234_1_En_2_Chapter/481234_1_En_2_Fig14_HTML.jpg
Figure 2-14

Users and posts associations

Many-to-One Association

A TUser is the owner of a specific post; therefore, this entity deserves a property in TPost. Typically, in an OOP fashion, we would call this property User. Then, we need to tell Aurelius that this property relates to the entity TUser. We accomplish this by decorating the corresponding field of User property with the [Association] attribute.
[Entity]
[Table('Post')]
[Id('FID', TIdGenerator.SmartGuid)]
TPost = class
private
  ...
  [Association([], CascadeTypeAll - [TCascadeType.Remove])]
  [JoinColumn('User', [], 'ID')]
  FUser: TUser;
public
  ...
  property User: TUser read FUser write FUser;
end;
The Association attribute tells Aurelius that there is a relationship between the two entities. In database theory, this refers to the concept of foreign keys. In the preceding code, I also define the behavior of the association by indicating that all changes except Remove will be cascaded by using the constant CascadeTypeAll - [TCascadeType.Remove]. Instead of this expression, I could also use the CascadeTypeAllButRemove identifier. Only with the Association attribute, Aurelius is not in the position to know which column (field) in the User table serves as the foreign key. We provide this information by using the [JoinColumn] attribute. In the preceding code, the foreign key links to the ID (primary key) column in the User table. Up to this point, we have not defined it so we add a property in TUser.
[Entity]
[Table('User')]
[Id('FID', TIdGenerator.SmartGuid)]
TUser = class
private
 [Column('ID', [TColumnProp.Required])]
 FID: TGuid;
 ...
public
 property ID: TGuid read FID write FID;
 ...
end;

It is imperative to use both these attributes together; otherwise, you will get an error. As you can see in the code, both attributes provide a way to tailor the behavior of the foreign key relationship by supplying additional parameters. The manual includes very detailed information for both attributes.

One-to-Many Association

As code-first developers, the premise that “a user owns several posts” is basically implemented by the use of a TList as in the following code. A new read-only property (Posts) is created to hold this list.
[Entity]
[Table('User')]
[Id('FID', TIdGenerator.SmartGuid)]
TUser = class
private
  ...
  FPosts: TList<TPost>;
public
  property Posts: TList<TPost> read FPosts;
end;
In order to create this type of association in Aurelius, we have to use the [ManyValuedAssociation] attribute. This attribute is used in two ways. If there is already an association with the other endpoint (bidirectional), you can pass the field or the property directly to the attribute as demonstrated in the following code:
[Entity]
[Table('User')]
[Id('FID', TIdGenerator.Guid)]
TUser = class
private
  ...
  [ManyValuedAssociation([], CascadeTypeAll], 'FUser')]
  FPosts: TList<TPost>;
public
  ...
end;

This tells Aurelius that the corresponding field in TPost is FUser. In other words, when Aurelius loads FPosts, it populates the list with those TPost objects that have their fUser attribute the same as the identifier FID of the current instance of TUser.

In the case where a class does not have a defined association, the same result can be achieved by supplying the [ForeignJoinColumn] attribute immediately after [ManyValuedAssociation]. More information and examples appear in the manual, but it seems that this attribute exists to cover some corner-case situations.

One-to-One Association

Aurelius does not provide a special attribute to manage one-to-one associations. This can be resolved if you see one-to-one association as one-to-many and filter the retrieval of the data to provide one and only result. To demonstrate this, let us assume that the user in our blog site can create one and only one post. The next snippet shows how we would create a one-to-one relationship:
[Entity]
[Table('User')]
[Id('FID', TIdGenerator.Guid)]
TUser = class
private
  ...
  [ManyValuedAssociation([], CascadeTypeAll], 'FUser')]
  fPosts: TList<TPost>;
  function getPost: TPost;
  procedure setPost (const aPost: TPost);
public
  ...
  property Post: TPost read getPost write setPost;
end;
...
function TUser.getPost: TPost;
begin
  if FPosts.Value.Count > 0 then
    result := FPosts.Value[0]
  else
    result := nil;
end;
procedure TUser.setPost (const aPost: TPost);
begin
  if FPosts.Value.Count = 0 then
    FPosts.Add(Value)
  else
    FPosts[0] := Value;
end;

Lazy Loading

All the previous examples are designed in such a way that when a Post is retrieved by Aurelius, the User property will get the object with the user at the same time. Similarly, when a User is loaded, the Posts property (TList) will be created and loaded with content. This is the default (eager) loading.

Although in our examples the footprint of such approach is subtle, in full-scale applications with complex databases, it will load unnecessary content stretching the available resources. The behavior of the properties can be altered to allow loading of valid content only when they are accessed (lazy loading).

Lazy loading requires a virtual proxy, a mechanism that shows the same properties as the underlying entity but controls when the loading of the actual content is done. Aurelius provides a generic record Proxy<T> for lazy loading.

Let’s make the User property in TPost a lazy loading property. We need to use the Proxy<TUser> record . Now, we are not able to access the TUser directly, but we need to use Proxy’s properties (Value). As a last step, we need to inform the [Association] property that the relationship behaves in a lazy manner. The following code shows the changes in bold:

../images/481234_1_En_2_Chapter/481234_1_En_2_Fige_HTML.png

Next, we would like to change the loading behavior of Posts in TUser entity. This time, the property is of TList type. Proxy<T> record can be used in this case as well with a small caveat; in addition to the TPost objects, we need to manually instantiate and destroy the corresponding TList; otherwise an access violation will be raised the moment we try to retrieve the content of Posts. Aurelius provides some methods for this purpose as shown in the following code:

../images/481234_1_En_2_Chapter/481234_1_En_2_Figf_HTML.png

Object Manager (Container)

Aurelius implements the concept of the entity container by providing an Object Manager (TObjectManager). The object manager comes with all the required functionality to manipulate the underlying database. It operates on entities (objects) loaded from the database and allows the developer to save, update, and load them and perform queries in order to retrieve results. When the state of an entity changes, the object manager makes sure that the database is updated accordingly. Because the object manager holds a state of entities in memory, it is capable of managing the lifetime of the entity objects. Entities are instantiated and destroyed by the object manager automatically.

The object manager is designed to provide a lightweight buffer between developer and the database in use. It is meant to be a short-lived object. Very often, this point confuses newcomers to Aurelius. You instantiate an object manager every time you want to do a specific database-related operation like finding records or retrieving records under specific criteria. Once this operation is completed, the object manager should be destroyed. This means that keeping an object manager alive for the lifetime of an application is not a good use of the manager architecture and capabilities.

Typically, object manager objects are treated in try-except or try-finally blocks because the manager hits the database, and this may result to errors due to corrupted files, unavailable database servers, or other unpredictable factors.

../images/481234_1_En_2_Chapter/481234_1_En_2_Figg_HTML.png

This code snippet demonstrates the typical use of object managers. As you can see, we have to free the object explicitly. It is obvious that the object manager could be implemented as an interface to save the manual lifetime management, but instead it is provided as a typical class. This can be inconvenient as it generates lots of boilerplate code. In the following chapter, when we start writing code for our example application, we will see how we can do this automatically.

Criteria

The object manager provides a way to execute queries in order to retrieve results from the database. The queries are constructed by putting criteria together that represent the query you want to execute.

The simplest way to use the criteria is to take advantage of the Find<T> method in object managers. Find uses generics to determine the entity the query will return. For example, if you want to retrieve the list of posts, you can use the following code:

../images/481234_1_En_2_Chapter/481234_1_En_2_Figh_HTML.png

A call to Find<TPost>.List always creates a new TObjectList even if the query itself returns nothing. You can be confident that you always have a valid instance of postsList after such call. Consequently, you need to free the instance manually; otherwise it will lead to memory leaks.

Another point to note is the type of list the object manager returns. In the preceding example, I used a TObjectList, but you can equally use a TList. There is no real difference.

The query in the code fetches all the posts from the database. This is a very basic operation, but you usually want to retrieve entities that meet some criteria. In order to achieve this, you can use Aurelius’ fluent interface and provide the constraints you need. As an example, let’s assume we need to retrieve all the posts that were created today. There is a number of ways to do this in Aurelius, but I find the use of Linq language extremely simple. In the following example, I use the Add method but you can also use the Where statement. They are equivalent.
uses
  ...,
  System.SysUtils,
  Aurelius.Criteria.Linq;
...
begin
  ...
  try
    ...
    postsList:=objManager.Find<TPost>
                         .Add(Linq['DateTime'] = Now)
                         .List;
    ...
    postsList.Free;
  finally
    ...
  end;
  ...
end;

As you can observe in the code, Aurelius allows the use of relational operators as part of the criteria statements. It supports a range of operations including string manipulation and the ability to directly use SQL statements. The official technical manual provides more details on this topic.

One very interesting consequence of the fluent interface is the ability to drill deep into properties of entities that represent associations. TPost holds a reference to the owner (TUser). This represents an association in ORM terms. If we want to expand the preceding query and extract the posts for which the user’s name is John, we have to add the following lines:
...
postsList:=objManager.Find<TPost>
                    .Add(Linq['DateTime'] = Now)
                .CreateAlias('User','u')
                .Add(Linq['u.Name'] = 'John')
                     .List;
...

What we did is to create an alias to the property of the entity class that represents the association. We assigned the letter u to the property User, but this is an arbitrary choice; I can use any string for alias including the string user. The important point here is that we need to create an alias to get access to the properties of the associated entity. From this point onward, we can create any criteria we need and refer back to the properties of the association by using the u. connotation. For completion, creating aliases is not the only way to access associations; SubCriteria can also be used but I find aliases much more efficient purely because it generates clear statements in the code, and I can follow the logic of the criteria very easily. But if you prefer SubCriteria, there is no real reason not to use it.

Note

Whenever you want to access an association in an entity object, you should always create an alias or subcriteria. Otherwise, Aurelius will throw an error as it will try to locate a field by that name. In the following code, Aurelius will attempt to locate the User.Name field, which will lead to an error:

...postsList:=objManager.Find<TPost>                     .Add(Linq['DateTime'] = Now)                     .Add(Linq['User.Name'] = 'John')                     .List;...

Projections

Projections are also provided by the object manager as they are basically extensions to the criteria. Generally speaking, we refer to projections when we want to retrieve specific values either directly from a list of entities or as a result of calculations and grouping. For all other cases, we refer to criteria.

We use Select to create a projection. Select can manage a list of projections via TProjections.ProjectionList, and there are a number of functions that can be used with it. Please refer to the official manual for the most up-to-date list of the functions.

Because projections are designed to retrieve values, it follows that they do not return lists of entities but rather lists of values. In Aurelius, each projection result is held in a TCriteriaResult object.

Building on our example, let’s find out how many posts exist in the database.
uses
  ...,
  Aurelius.Criteria.Base,
  Aurelius.Criteria.Projections;
...
var
  ...
  projResults: TObjectList<TCriteriaResult>;
  projRes: TCriteriaResult;
begin
  projResults:=objManager.Find<TPost>
                         .Select(TProjections.ProjectionList
                                   .Add(TProjections.Count('ID'))
                                )
                         .ListValues;
  for projRes in projResults do
    Writeln(projRes.Values[0]);
  projResults.Free;
end;
In the code, I created a projection list. Because I have only one projection, this is not strictly necessary. I could have just written it in one line as follows using UniqueValue because I know that I expect only one value from the projection:
...
  .Select(TProjections.Count('ID')))
  .UniqueValue;
...

I personally prefer to create lists even when I have only one projection because it gives me a standardized way of building projections and also allows me to add another condition to the list very easily. In terms of accessing the projection results, you need to iterate through the list of TCriteriaResult in order to retrieve the desired value. Additionally, we need to manually free the instance of the result list.

A call to ListValues always creates an instance of TObjectList<TCriteriaResult> even if the content is empty. This means that we need to check what the list holds every time we access the content. The following code adds the check for this situation:
...
for projRes in projResults do
  if projRes.Values[0] <> Null then
    Writeln(projRes.Values[0]);
...

In the case of UniqueValue, the call can return nil if there is no relevant content. In this case, we need to do a separate check using Assigned.

So far, we have the total number of posts. Now, we would like to know the number of posts per month. In order to get this, we can group the counts of posts based on the month of the field DateTime . This is easy in Aurelius as the code that follows shows:
begin
  projResults:=objManager.Find<TPost>
                      .Select(TProjections.ProjectionList
                 .Add(TProjections.Count('ID').As_('Num'))
                 .Add(TProjections.Month('DateTime').As_('Month'))
                 .Add(TProjections.Group(
                      TProjections.Month('DateTime')
                      ))
                              )
                 .ListValues;
  ...
end;

There are two steps here. First, I add a projection to retrieve the month of the posts using the Month projection method. Then, I group the results based on the month component of the DateTime field of TPost.

This code will populate projResults with TCriteriaResult objects . Each TCriteriaResult will provide values for the two projections (count of posts and month of posts) we defined. I can get the values using a simple for loop.
...
  for projRes in projResults do
  begin
    if (projRes.Values[0] <> Null) and (projRes.Values[1] <> Null) then
      Writeln('Num: '+VarToStr(projRes.Values[0]) +
               ' | Month: '+VarToStr(projRes.Values[1]));
  end;
...
The indices correspond to the order I added the projections when I built the statement. Note that in this case, I had to use VarToStr to build the string in the Writeln statement . Many times, keeping track of the order of the projection values is not convenient especially if you need to add and remove projections during testing phases. A better and more efficient way to manage the values in TCriteriaResult is to name the values you retrieve. This can easily be done using the .As_ method as demonstrated in the preceding code. Once you assign the names, you can access them in a more readable way.
...
  for projRes in projResults do
  begin
    if (projRes.Values['Num'] <> Null) and
                          (projRes.Values['Month'] <> Null) then
      Writeln('Num: '+VarToStr(projRes.Values['Num']) +
               ' | Month: '+VarToStr(projRes.Values['Month']));
  end;
...
One last point about the projection code: you may notice that in order to group the result by the month of the DateTime field , I call again TProjections.Month method even after I assign the name Month to the same projection in the preceding two lines. Thus, it looks natural to write
...
 .Add(TProjections.Group('Month'))
...

Unfortunately, this approach does not work and for good reason. The value Month is known to Aurelius only after the list of projections has been executed. Therefore, Aurelius cannot resolve it as part of the projection itself.

On a more general note, Aurelius provides an implementation of the Linq language . This simplifies the code in a way as it offers code that is more compact. For example, instead of writing
...
  .Add(TProjections.Count('ID').As_('Num'))
 .Add(TProjections.Month('DateTime').As_('Month'))
...
I could use the following statements:
...
 .Add(Linq['Id'].As_('Num'))
 .Add(Linq['DateTime'].Month.As_('Month'))
...

Automapping

The attributes to map a class to an entity provide great degree of flexibility to the developer when it comes to defining how Aurelius should treat the underlying tables, columns, and associations in the database. One drawback to this is that it may turn to a time-consuming process especially when the workflow of work puts the database first. For such cases, but, also, to provide simplicity of use, Aurelius introduces the [Automapping] attribute . Decorating a class with the [Entity] and [Automapping] attributes removes the need to visit every property and association in the class and provide explicit instructions about how Aurelius should treat each field and property. The automapping procedure follows a number of rules and conventions that can be found in the official documentation.

There is a small catch with automapping. When Aurelius identifies fields to map, it checks the name and if it starts with capital F (a commonly followed convention), it extracts the letter and names the table column after the remainder string. For example, if we have a field named FSurname, Aurelius will map it to the Surname column in the database table. In all other cases, Aurelius will add the F_ prefix. Thus, a field named fSurname will become F_fSurname.

If you still find the procedure of adding the [Automapping] attribute to each entity overwhelming or time-consuming in a large database schema, you can change Aurelius global behavior by changing the global configuration AutoMappingMode property to Full from the default ByClass value. Full automapping mode will automatically map any entity that is registered to Aurelius.
uses
  ...,
  Aurelius.Global.Config;
var
  ...,
  config: TGlobalConfigs;
begin
  ...
  config:=TGlobalConfigs.GetInstance;
  config.AutoMappingMode:=TAutomappingMode.Full;
  ...
end

If you want to achieve the opposite result (exclude properties and fields from automapping), you can decorate them with the [Transient] attribute.

Summary

This chapter presents TMS Aurelius. Step by step, we start by looking at the different installation options and then move to explore how ORM features as provided by the framework. This introduction sets the scene to show how Aurelius can be used in a real-life application. The next chapter presents the example application that we are going to use to put Aurelius at work.

References

Embarcadero, n.d. Attributes (RTTI). [Online] Available at: http://docwiki.embarcadero.com/RADStudio/Rio/en/Attributes_ (RTTI) [Accessed 04 03 2019].

Hodges, N., 2014. Coding in Delphi. s.l.:Nepeta Enterprises.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.220.139.168