Blog

All Blog Posts  |  Next Post  |  Previous Post

Crash Course TMS Aurelius – Inheritance and Polymorphism

Bookmarks: 

Friday, March 8, 2013

Inheritance is one of my favorite features in Aurelius. One of benefits of using an ORM is abstracting the SQL and start thinking (almost) purely in OOP. Inheritance and polymorphism are fundamental features of Object-oriented programming, and if when designing your model you can't use it, then the "object-relational" mapping would just become a simple "property>column" mapping in the end.

Aurelius allows you to build a class hierarchy that can be persisted, and provides you with two strategies to persist it: joined tables and single table. The former will create a different table for each class and add the proper relationships, and the later will save the whole hierarchy in the same table. You can learn more about it reading the topic "Inheritance Strategies" in documentation.

Let me illustrate how it works. Considering the following classes and mapping:
type
  [Entity, Automapping]
  [Inheritance(TInheritanceStrategy.JoinedTables)]
  TPerson = class
  private
    FId: integer;
    FName: string;
  public
    property Id: integer read FId write FId;
    property Name: string read FName write FName;
  end;

  [Entity, Automapping]
  TEmployee = class(TPerson)
  private
    FSalary: Currency;
  public
    property Salary: Currency read FSalary write FSalary;
  end;
Note that mapping is also very straightforward, all you need to do is specify the strategy to be used in the base class of your hierarchy. Now you can save your objects in the same way we did in previous posts:
function SavePerson(Manager: TObjectManager): integer;
var
  Person: TPerson;
begin
  Person := TPerson.Create;
  Person.Name := 'John Person';
  Manager.Save(Person);
  Result := Person.Id;
end;

function SaveEmployee(Manager: TObjectManager): integer;
var
  Employee: TEmployee;
begin
  Employee := TEmployee.Create;
  Employee.Name := 'James Employee';
  Employee.Salary := 1999.99;
  Manager.Save(Employee);
  Result := Employee.Id;
end;
After calling the above methods, we have one TPerson object and one TEmployee object persisted in the database. We can use the following code to retrieve them using the generated id's:
procedure OutputPerson(Person: TPerson);
begin
  if Person <> nil then
    WriteLn(Format('Class: %s; Name: %s', [Person.ClassName, Person.Name]))
  else
    WriteLn('nil');
end;

procedure OutputEmployee(Employee: TEmployee);
begin
  if Employee <> nil then
    WriteLn(Format('Class: %s; Name: %s; Salary: %s',
      [Employee.ClassName, Employee.Name, FloatToStr(Employee.Salary)]))
  else
    WriteLn('nil');
end;

procedure CheckPersonAndEmployee(Manager: TObjectManager; PersonId, EmployeeId: integer);
var
  Person: TPerson;
  Employee: TEmployee;
begin
  Person := Manager.Find<TPerson>(PersonId);
  OutputPerson(Person);
  Person := Manager.Find<TPerson>(EmployeeId);
  OutputPerson(Person);
  Employee := Manager.Find<TEmployee>(EmployeeId);
  OutputEmployee(Employee);
  Employee := Manager.Find<TEmployee>(PersonId);
  OutputEmployee(Employee);
end;
This is what we get as the output:
Class: TPerson; Name: John Person
Class: TEmployee; Name: James Employee
Class: TEmployee; Name: James Employee; Salary: 1999.99
nil
Now you see polymorphism in action. The first two Find calls ask for a TPerson object. It happens that the first id is a TPerson object indeed, but the second is an id for a TEmployee object. Both are retrieved because a TEmployee is a TPerson. Also note that the retrieved object in second Find is actually a TEmployee object.

The last two Find calls ask for a TEmployee object. When the EmployeeId is provided, the correct TEmployee object is retrieved. But when we ask for a TEmployee object passing PersonId as Id, nil is returned - although the object is in database with that id, it's not returned because the object is not a TEmployee, but only a TPerson.

As in the previous posts, I will provide here some SQL statements generated by Aurelius, for a better understanding. When using joined tables strategy, Aurelius will create the following database structure (SQL Server syntax):
CREATE TABLE PERSON (
  ID INTEGER IDENTITY(1,1) NOT NULL,
  NAME VARCHAR(255) NOT NULL,
  CONSTRAINT PK_PERSON PRIMARY KEY (ID));

CREATE TABLE EMPLOYEE (
  ID INTEGER NOT NULL,
  SALARY NUMERIC(20, 4) NOT NULL,
  CONSTRAINT PK_EMPLOYEE PRIMARY KEY (ID));

ALTER TABLE EMPLOYEE ADD CONSTRAINT 
  FK_EMPLOYEE_PERSON_ID FOREIGN KEY (ID) REFERENCES PERSON (ID);
Each class will have its data saved in a different database, and retrieving a TEmployee object would execute the following statement:
SELECT A.ID AS A_ID, A.SALARY AS A_SALARY, B.ID AS B_ID, B.NAME AS B_NAME
FROM EMPLOYEE A
  LEFT JOIN PERSON B ON (B.ID = A.ID)
WHERE  B.ID = :p_0
To conclude this post, let's change the strategy to single table. This will make the mapping look like this (Salary property has to be nullable because all data will stay in a single table):
  [Entity, Automapping]
  [Inheritance(TInheritanceStrategy.SingleTable)]
  [DiscriminatorColumn('PERSON_TYPE', TDiscriminatorType.dtString)]
  [DiscriminatorValue('Person')]
  TPerson = class
  private
    FId: integer;
    FName: string;
  public
    property Id: integer read FId write FId;
    property Name: string read FName write FName;
  end;

  [Entity, Automapping]
  [DiscriminatorValue('Employee')]
  TEmployee = class(TPerson)
  private
    FSalary: Nullable<Currency>;
  public
    property Salary: Nullable read FSalary write FSalary;
  end;
Code will be exactly the same. Database structure will become just this:
CREATE TABLE PERSON (
  ID INTEGER IDENTITY(1,1) NOT NULL,
  NAME VARCHAR(255) NOT NULL,
  PERSON_TYPE VARCHAR(30) NOT NULL,
  SALARY NUMERIC(20, 4) NULL,
  CONSTRAINT PK_PERSON PRIMARY KEY (ID));
and this is how an employee is retrieved from database:
SELECT A.ID AS A_ID, A.NAME AS A_NAME, A.PERSON_TYPE AS A_PERSON_TYPE, A.SALARY AS A_SALARY
FROM PERSON A
WHERE A.PERSON_TYPE = :p_1
 AND A.ID = :p_0

p_0 = "1" (ftInteger)
p_1 = "Employee" (ftString)


Wagner Landgraf


Bookmarks: 

This blog post has not received any comments yet.



Add a new comment

You will receive a confirmation mail with a link to validate your comment, please use a valid email address.
All fields are required.



All Blog Posts  |  Next Post  |  Previous Post