BlogAll Blog Posts | Next Post | Previous Post
Friday, March 08, 2013Inheritance 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:
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:
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;
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:
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;
This is what we get as the output:
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;
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.
Class: TPerson; Name: John Person Class: TEmployee; Name: James Employee Class: TEmployee; Name: James Employee; Salary: 1999.99 nil
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):
Each class will have its data saved in a different database, and retrieving a TEmployee object would execute the following statement:
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);
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):
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
Code will be exactly the same. Database structure will become just this:
[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;
and this is how an employee is retrieved from database:
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));
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 R. Landgraf
This blog post has not received any comments yet.
All Blog Posts | Next Post | Previous Post