TMS Aurelius and MVVM design: an example (Part 1)
Model-View-ViewModel (MVVM) design pattern allows developers to write software in a way that separates applications to layers that serve the business logic, the presentation logic and the elements of the user interface.
TMS Aurelius is an ORM framework developed and marketed by TMS which provides a great degree of abstraction, flexibility and simplicity. I have been using Aurelius for sometime now in different applications and I appreciate its benefits. But when I attempted to combine MVVM and Aurelius at first place, I fell into some traps mostly by a few misconceptions I had about the framework. The guys at TMS were very helpful and spent time replying to my emails. This helped me clarify and better understand how Aurelius has been designed.
In this post, I describe my approach to using Aurelius in MVVM with the hope it is useful and educative to others and, at the same time, with an invitation to others to share their approaches and improve my code and understanding.
Contents
The Example Application
The goal is to develop a FireMonkey application which meets the following requirements:
- Shows a list of customers
- Allows the user to add a new customer
- Allows the user to edit an existing customer
- Allows the user to delete an existing customer
- Keeps the following data for each customer: first name, last name and a photo
The application uses the MVVM pattern and stores data in a SQLite database file, which is located in the local drive. The communication between the application and the database is done via Aurelius.
The application has two screen: the Main Screen and the Customer Screen. Figure 1 and Figure 2 show these screens.
Figure 1: The Main Screen
Figure 2: The Customer Screen
Both screens contain a frame to present the details of the customer (Figure 3).
Figure 3: The frame for the details of the customer
MVVM Design
There are many resources available which provide an explanation of the MVVM pattern but, of course, the best one can be found in my book :-). I will not go into details here since this post is, primarily, about Aurelius. I will, only, describe my approach to the example application under the MVVM paradigm.
Each screen in the application (View) is supported by a dedicated ViewModel and there is one Model which serves both ViewModels (Figure 4 and Figure 5).
Figure 4: MVVM design of the Main Screen
Figure 5: MVVM design of the Customer Screen
TMS Aurelius Design
There are three core concepts in Aurelius which define how the framework is designed and is being used:
- The Entities: They define the classes that correspond to the tables in the database
- The Connection: Defines the type of the database to be used, the chosen dialect and how the actual connection to the database takes place
- The Object Manager: Manages the entities and takes care of the database actions
The Entity
For our application, we have one object which represents a customer as seen in the following Figure 6.
Figure 6: The Customer Entity
I keep the entities in a dedicated unit and, therefore, I declare Core.Database.Entities.pas
unit to keep the TCustomer
class.
unit Core.Database.Entities; interface uses SysUtils, Aurelius.Mapping.Attributes, Aurelius.Types.Blob, Aurelius.Types.DynamicProperties, Aurelius.Types.Nullable, Aurelius.Types.Proxy, Aurelius.Criteria.Dictionary; type TCustomers = class; TCustomersTableDictionary = class; [Entity] [Table('Customers')] [Id('FId', TIdGenerator.IdentityOrSequence)] TCustomers = class private [Column('ID', [TColumnProp.Required, TColumnProp.NoInsert, TColumnProp.NoUpdate])] FId: Integer; [Column('FirstName', [], 65535)] FFirstname: Nullable; [Column('LastName', [], 65535)] FLastname: Nullable; [Column('Photo', [TColumnProp.Lazy])] FPhoto: TBlob; public property Id: Integer read FId write FId; property Firstname: Nullable read FFirstname write FFirstname; property Lastname: Nullable read FLastname write FLastname; property Photo: TBlob read FPhoto write FPhoto; end; ... implementation ... end.
The Database Connection
We can very easily and in a direct way create the database manager and the database connection when the application starts but here I will try to develop code that promotes good design practices. I declare an interface which allows access to the database manager and the connection and develop the corresponding class. The following units show these two steps (Core.Database.Aurelius.Types
and Core.Database.Aurelius
).
unit Core.Database.Aurelius.Types; interface uses Aurelius.Engine.ObjectManager, Aurelius.Drivers.Interfaces, Aurelius.Engine.DatabaseManager; type IAurelius = interface ['{BC578F5C-4D4F-43CD-8B7C-C5D4A57CE164}'] function getDatabaseConnection: IDBConnection; function getDatabaseManager: TDatabaseManager; procedure createDatabaseManager; property DatabaseConnection: IDBConnection read getDatabaseConnection; property DatabaseManager: TDatabaseManager read getDatabaseManager; end; implementation end.
unit Core.Database.Aurelius; interface uses Core.Database.Aurelius.Types; function createAureliusDatabase (const path: string=''): IAurelius; implementation uses Aurelius.Engine.DatabaseManager, Aurelius.Drivers.Interfaces, Aurelius.Drivers.SQLite, Aurelius.Sql.SQLite, System.SysUtils, System.IOUtils; type TAurelius = class (TInterfacedObject, IAurelius) private fDatabaseConnection: IDBConnection; fDatabaseManager: TDatabaseManager; fDatabasePath: string; function getDatabaseConnection: IDBConnection; function getDatabaseManager: TDatabaseManager; procedure createDatabaseManager; public constructor Create (const path: string=''); destructor Destroy; override; end; const databaseFilename='test.sqlite'; { TAurelius } constructor TAurelius.Create(const path: string); begin fDatabasePath:=Trim(path); try fDatabaseConnection:= TSQLiteNativeConnectionAdapter.Create( TPath.Combine(fDatabasePath, databaseFilename)); except raise Exception.Create('Local database can''t be created'); end; createDatabaseManager; if not FileExists(TPath.Combine(fDatabasePath, databaseFilename)) then fDatabaseManager.BuildDatabase; end; procedure TAurelius.createDatabaseManager; begin if Assigned(fDatabaseConnection) then fDatabaseManager:=TDatabaseManager.Create(fDatabaseConnection) else raise Exception.Create('Database Connection is nil'); end; destructor TAurelius.Destroy; begin fDatabaseManager.Free; inherited; end; function TAurelius.getDatabaseConnection: IDBConnection; begin Result:=fDatabaseConnection; end; function TAurelius.getDatabaseManager: TDatabaseManager; begin Result:=fDatabaseManager; end; function createAureliusDatabase (const path: string=''): IAurelius; begin Result:=TAurelius.Create(path); end; end.
This may look complicated and unnecessary but it allows for expansion. With very little modification, you can create subclasses of TAurelius
to handle local databases and databases in memory very easily and take advantage of dependency injection to choose the correct database type according to your needs.
In our application, the database needs to be available in all our units. There are a few ways to do this (global variables, singleton, etc.). For simplicity, I declare a global variable in Model.Database.pas
unit.
unit Model.Database; interface uses Core.Database.Aurelius.Types; var database: IAurelius; implementation uses Core.Database.Aurelius; initialization database:=createAureliusDatabase; end.
At this stage, the application creates the database file in the Win32/Debug
folder or in the relevant folder depending on the target and platform you have chosen to compile the project.
The Object Manager
The last part we need is the object manager which allows the real manipulation of the database records and tables. We have to be able to create an instance of the object manager and because this is, directly, linked to the database manager, we get this instance from the same place we find the database manager (ie, the TAurelius
class). We do this by adding the necessary code in the relevant units.
unit Core.Database.Aurelius.Types; interface uses ... type IAurelius = interface ['{BC578F5C-4D4F-43CD-8B7C-C5D4A57CE164}'] ... function createDatabaseObjectManager: TObjectManager; ... end; implementation end.
unit Core.Database.Aurelius; interface ... implementation uses ... Aurelius.Engine.ObjectManager; type TAurelius = class (TInterfacedObject, IAurelius) private ... function createDatabaseObjectManager: TObjectManager; public ... end; { TAurelius } ... function TAurelius.createDatabaseObjectManager: TObjectManager; begin result:=TObjectManager.Create(fDatabaseConnection); end;
When I started reading and experimenting with Aurelius, I found the concept of object manager easy to grasp. Admittedly, I had misunderstood it and this made me reach dead-ends several times. The manual describes the concept and mentions that the object manager is not meant to be long-lived like the database connection object or the database manager. Unfortunately, the manual doesn’t give much to figure out how “short” the life of the object manager is meant to be.
After several emails with the TMS people and a couple of trial-and-error attempts, I came to understand that you should keep the object manager alive for as long as you need it to complete all the database related tasks for records and entries that are likely to be reused several times under a specific operation in your application.
To decipher the above (:-)) and apply it to our application, when the user arrives at the main screen, the application (a) retrieves the list of the customers and (b) gets the details for the customer who is selected in the listbox. These operations require an object manager under Aurelius. Based on the above, both (a) and (b) should be done using the same object manager instead of creating different instances for each part. One small note here; in our case we could use different object manager for (a) and (b) but the Photo field (TBlob
) is lazy-loaded, which complicates things a little. One object manager, though, solves this challenge.
We’ve set out the basics for our implementation. Now, head towards Part 2 of this post.
Nice…. Please write master detail demo…. martin
Thanks Martin.
How do you mean a “master detail demo”?
For example, show list of phone numbers (details) of selected customer (master)
How would that be different from what I do in the post already? What would be the challenge? And from what perspective? Aurelius or MVVM?
Good article
Please write how work master detail example
Orders Customers
And how work with transactions commit and rollback etc
Thanks.
I will try to come up with something 🙂
Nice article, thanks!
Whats this TDicDictionary and TCustomersTableDictionary all about?
Regards.
Thanks a lot.
I use TMS Data Modeler to create the unit with the Entities. DM adds the dictionaries you see (although there is an option to disable this).
I asked Wagner Landgraf (the developer of Aurelius) about this and here is what he says:
Sweet! Thank you very much for explaining.