Table of Contents

Data Binding - TAureliusDataset

TMS Aurelius allows you to bind your entity objects to data-aware controls by using a TAureliusDataset component. By using this component you can for example display a list of objects in a TDBGrid, or edit an object property directly through a TDBEdit or a TDBComboBox. TAureliusDataset is declared in unit Aurelius.​Bind.​Dataset.

Basic usage is done by these steps:

  1. Set the source of data to be associated with the dataset, using SetSourceList, or a single object, using SetSourceObject.

  2. Optionally, create a TField for each property/association/sub-property you want to display/edit. If you do not, default fields will be used.

  3. Optionally, specify a TObjectManager using the Manager property. If you do not, you must manually persist objects to database.

TAureliusDataset is a TDataset descendant, thus it's compatible with all data-aware controls provided by VCL, the Firemonkey live bindings framework and any 3rd-party control/tool that works with TDataset descendants. It also provides most of TDataset functionality, like calculated fields, locate, lookup, filtering, master-detail using nested datasets, among others.

The topics below cover all TAureliusDataset features.

Providing Objects

To use TAureliusDataset, you must provide to it the objects you want to display/edit. The objects will become the source of data in the dataset.

The following topics describe several different methods you can use to provide objects to the dataset.

Providing an Object List

A very straightforward way to provide objects to the dataset is specifying an external object list where the objects will be retrieved from (and added to).

You do that by using SetSourceList:

var
  People: TList<TPerson>;
begin
  People := Manager.Find<TPerson>.List;
  AureliusDataset1.SetSourceList(People);

You can provide any type of generic list to it. When you insert/delete records in the dataset, objects will be added/removed to the list.

By default, TAureliusDataset doesn't own the passed list object, meaning you are responsible for destroying the list object itself. You can change this behavior passing a second boolean parameter to SetSourceList indicating you want the dataset to destroy it:

  // Passing True will not require you destroy People list
  AureliusDataset1.SetSourceList(People, True);

With the code above, you don't have to worry about destroying the People list.

Note

When the source list is owned, Aurelius dataset will destroy it when it's closed. If you want to close and reopen the dataset in this case, you must provide a new list object, since the previous one was destroyed.

Providing a Single Object

Instead of providing multiple objects, you can alternatively specify a single object. It's a straightforward way if you intend to use the dataset to just edit a single object.

You must use SetSourceObject for that:

Customer := Manager.Find<TCustomer>(1);
AureliusDataset1.SetSourceObject(Customer);

Be aware that TAureliusDataset always works with lists. When you call SetSourceObject, the internal object list is cleared and the specified object is added to it. The internal list then is used as the source list of dataset. This means that even if you use SetSourceObject, objects might be added to or removed from the internal list, if you call methods like Insert, Append or Delete.

Using Fetch-On-Demand Cursor

You can provide objects to TAureliusDataset by using a query object cursor. This approach is especially useful when returning a large amount of data, since you don't need to load the whole object list first and then provide the whole list to the dataset.

Only needed objects are fetched (for example, the objects being displayed in a TDBGrid that is linked to the dataset). Additional objects will only be fetched when needed, i.e, when you scroll down a TDBGrid, or call TDataset.Next method to retrieve the next record.

Note that the advantage of this approach is that it keeps an active connection and an active query to the database until all records are fetched (or dataset is closed).

To use a cursor to provide objects, call SetSourceCursor and pass the ICriteriaCursor interface obtained when opening a query using a cursor:

var
  Cursor: ICriteriaCursor;
begin
  Cursor := Manager.Find<TPerson>.Open;
  AureliusDataset1.SetSourceCursor(Cursor);

  // Or just this single line version:
  AureliusDataset1.SetSourceCursor(Manager.Find<TPerson>.Open);

You don't have to destroy the cursor, since it's an interface and is destroyed by reference counting. When the cursor is not needed anymore, dataset will destroy it.

When you call SetSourceCursor, the internal object list is cleared. When new objects are fetched, they are added to the internal list. So, the internal list will increase over time, as you navigate forward in the dataset fetching more records.

Using Criteria for Offline Fetch-On-Demand

Another way to provide objects to TAureliusDataset is providing a TCriteria object to it. Just create a query and pass the TCriteria object using SetSourceCriteria:

var
  Criteria: TCriteria;
begin
  Criteria := Manager.Find<TPerson>;
  AureliusDataset1.SetSourceCriteria(Criteria);

  // Or just this single line version:
  AureliusDataset1.SetSourceCriteria(Manager.Find<TPerson>);

In the code above, Aurelius will execute the query specified by the TCriteria and fill the internal object list with the retrieved objects.

This approach is not very different from providing an object list to the dataset. The real advantage of it is when you use an overloaded version of SetSourceCriteria that allows paging.

Offline fetch-on-demand using paging

SetSourceCriteria has an overloaded signature that receives an integer parameter specifying a page size:

AureliusDataset1.SetSourceCriteria(Manager.Find<TPerson>, 50);

It means that the dataset will fetch records on demand, but without needing to keep an active database connection.

When you open a dataset after specifying a page size of 50 as illustrated in the code above, only the first 50 TPerson objects will be fetched from the database, and query will be closed. Internally, TAureliusDataset uses the paging mechanism provided by Take and Skip methods. If more records are needed (a TDBGrid is scrolled down, or you call TDataset.Next method multiple times, for example), then the dataset will perform another query in the database to retrieve the next 50 TPerson objects in the query.

So, in summary, it's a fetch-on-demand mode where the records are fetched in batches and a new query is executed every time a new batch is needed. The advantage of this approach is that it doesn't retrieve all objects from the database at once, so it's fast to open and navigate, especially with visual controls. Another advantage (when comparing with using cursors, for example) is that it works offline — it doesn't keep an open connection to the database. One disadvantage is that it requires multiple queries to be executed on the server to retrieve all objects.

You don't have to destroy the TCriteria object. The dataset uses it internally to re-execute the query and retrieve a new set of objects. When all records are fetched or the dataset is closed, the TCriteria object is automatically destroyed.

Internal Object List

TAureliusDataset keeps an internal object list that is sometimes used to hold the objects associated with the dataset records. When you provide an external object list, the internal list is ignored. However, when you use other methods for providing objects, like using cursor (SetSourceCursor), paged TCriteria (SetSourceCriteria), or even a single object (SetSourceObject), then the internal list is used to keep the objects.

When the internal list is used, new records are added to and removed from it when you insert or delete. When fetch-on-demand modes are used (cursor and criteria), fetched objects are incrementally added to the list. Thus, when you open the dataset you might have 20 objects in the list, when you move the cursor to the end of dataset, you might end up with 100 objects in the list.

Use InternalList to access the internal list. It is exposed as IReadOnlyObjectList, so you can't modify it directly — use the TDataset methods to add and remove records. See the API reference for available members.

Using Fields

In TAureliusDataset, each field represents a property in an object. So, for example, if you have a class declared like this:

TCustomer = class
// <snip>
public
  property Id: Integer read FId write FId;
  property Name: string read FName write FName;
  property Birthday: Nullable<TDate> read FBirthday write FBirthday;
end;

when providing an object of class TCustomer to the dataset, you will be able to read or write its properties this way:

CustomerName := AureliusDataset1.FieldByName('Name').AsString;
if AureliusDataset1.FieldByName('Birthday').IsNull then
  AureliusDataset1.FieldByName('Birthday').AsDateTime := EncodeDate(1980, 1, 1);

As with any TDataset descendant, TAureliusDataset will automatically create default fields, or you can optionally create TField components manually in the dataset, either at runtime or design-time. Creating persistent fields might be useful when you need to access a field that is not automatically present in the default fields, like a sub-property field or when working with inheritance.

The following topics explain fields usage in more details.

Default Fields and Base Class

When you open the dataset, default fields are automatically created if no persistent fields are defined. TAureliusDataset will create a field for each property in the "base class", either regular fields, or fields representing associations or many-valued associations like entity fields and dataset fields. The "base class" mentioned is retrieved automatically by the dataset given the way you provided the objects:

  1. If you provide objects by passing a generic list to SetSourceList, Aurelius will consider the base class as the generic type in the list. For example, if the list type is TList<TCustomer>, then the base class will be TCustomer.

  2. If you provide an object by using SetSourceObject, the base class will just be the class of object passed to that method.

  3. You can alternatively manually specify the base class, by using the ObjectClass property. Note that this must be done after calling SetSourceList or SetSourceObject, because these two methods update the ObjectClass property internally. Example:

AureliusDataset1.SetSourceList(SongList);
AureliusDataset1.ObjectClass := TMediaFile;

Self Field

One special field that is created by default or you can add manually in persistent fields is a field named "Self". It is an entity field representing the object associated with the current record. It's useful for lookup fields.

In the following code, both lines are equivalent (if there is a current record):

Customer1 := AureliusDataset1.Current<TCustomer>;
Customer2 := AureliusDataset1.EntityFieldByName('Self').AsEntity<TCustomer>;
// Customer1 = Customer2

Sub-Property Fields

You can access properties of associated objects (sub-properties) through TAureliusDataset. Suppose you have a class like this:

TCustomer = class
// <snip>
public
  property Id: Integer read FId write FId;
  property Name: string read FName write FName;
  property Country: TCountry read FCountry write FCountry;
end;

You can access properties of Country object using dots:

AureliusDataset1.FieldByName('Country.Name').AsString := 'Germany';

As you might have noticed, sub-property fields can not only be read, but also written to. There is not a limit for level access, which means you can have fields like this:

CountryName := AureliusDataset1.FieldByName('Invoice.Customer.Country.Name').AsString;

It's important to note that sub-property fields are not created by default when using default fields. In the example of TCustomer class above, only field "Country" will be created by default, but not "Country.Name" or any of its sub-properties. To use a sub-property field, you must manually add the field to the dataset before opening it:

with TStringField.Create(Self) do
begin
  FieldName := 'Country.Name';
  Dataset := AureliusDataset1;
end;

Entity Fields (Associations)

Entity fields represent associations — they map to an object property in a container object. TAureliusDataset creates TAureliusEntityField fields for properties that hold object instances. Since Delphi's DB library doesn't provide a field type for object pointers, TAureliusEntityField is an Aurelius-specific field type that lets you manipulate the object reference directly.

Use EntityFieldByName or cast with as [TAureliusEntityField](~/api/Aurelius.Bind.BaseDataset/TAureliusEntityField/index.md) to access the AsObject property or the generic AsEntity<T> function.

Warning

Entity fields hold object references, not displayable data. Do not use them for visual binding in grids or edit controls — the displayed value would just be a pointer. To visually bind properties of associated objects, use sub-property fields instead.

The following code snippets illustrate how to use entity fields:

// Set an association through the dataset
AureliusDataset1.EntityFieldByName('Country').AsObject := TCountry.Create;
(AureliusDataset1.FieldByName('Country') as TAureliusEntityField).AsObject := TCountry.Create;
// Retrieve the value of an association property
Country := AureliusDataset1.EntityFieldByName('Country').AsEntity<TCountry>;

Dataset Fields (Many-Valued Associations)

Dataset fields represent collections in a container object — they map to many-valued associations. Consider the following class:

TInvoice = class
// <snip>
public
  property Id: Integer read FId write FId;
  property Items: TList<TInvoiceItem> read GetItems;
end;

The field "Items" is a TDatasetField, representing all objects in the Items collection. You can use it to build master-detail relationships. The following code links an ItemsDataset to an InvoiceDataset through the "Items" dataset field:

InvoiceDataset.SetSourceList(List);
InvoiceDataset.Manager := Manager1;
InvoiceDataset.Open;
ItemsDataset.DatasetField := InvoiceDataset.FieldByName('Items') as TDatasetField;
ItemsDataset.Open;

By default, ParentManager is true on detail datasets, meaning the detail dataset inherits the Manager of its parent. Whenever you post or delete a record in the detail dataset, the detail object is immediately persisted in the database.

In case you don't want this behavior (for example, you want the details dataset to save objects in memory and only when the master object is saved you have details being saved at once), you can explicitly set the Manager property of the details dataset to nil. This will automatically set the ParentManager property to false:

InvoiceDataset.SetSourceList(List);
InvoiceDataset.Manager := Manager1;
// Set Manager to nil so only save items when InvoiceDataset is posted.
// ItemsDataset.ParentManager will become false
ItemsDataset.Manager := nil;
InvoiceDataset.Open;

As with any master-detail relationship, you can add or remove records from the detail/nested dataset, and it will add/remove items from the collection:

ItemsDataset.Append;
ItemsDataset.FieldByName('ProductName').AsString := 'A';
ItemsDataset.FieldByName('Price').AsCurrency := 1;
ItemsDataset.Post;

ItemsDataset.Append;
ItemsDataset.FieldByName('ProductName').AsString := 'B';
ItemsDataset.FieldByName('Price').AsCurrency := 1;
ItemsDataset.Post;

Heterogeneous Lists (Inheritance)

When providing objects to the dataset, the list provided might have objects instances of different classes. This happens for example when you perform a polymorphic query.

Suppose you have a class hierarchy which base class is TAnimal, and descendant classes are TDog, TMammal, TBird, etc. When you perform a query like this:

Animals := Manager.Find<TAnimal>.List;

You might end up with a list of objects of different classes like TDog or TBird. Suppose for example TDog class has a DogBreed property, but TBird does not. Still, you need to create a field named "DogBreed" so you can display it in a grid or edit that property in a form.

TAureliusDataset allows you to create fields mapped to properties that might not exist in the object. Thus, you can create a persistent field named "DogBreed", or you can change the base class of the dataset to TDog so that the default fields will include a field named "DogBreed".

To allow this feature to work well, when such a field value is requested and the property does not exist in the object, TAureliusDataset will not raise any error. Instead, the field value will be null. Thus, if you are listing the objects in a DBGrid, for example, a column associated with field "DogBreed" will display the property value for objects of class TDog, but will be empty for objects of class TBird. Please note that this behavior only happens when reading the field value. If you try to set the field value and the property does not exist, an error will be raised when the record is posted. If you don't change the field value, it will be ignored.

Also note that ObjectClass is used to create a new object instance when inserting new records. The following code illustrates how to use a dataset associated with a TList<TAnimal> and still create two different object types:

Animals := Manager.FindAll<TAnimal>;
DS.SetSourceList(Animals); // base class is TAnimal
DS.ObjectClass := TDog; // now base class is TDog
DS.Open;
DS.Append;
DS.FieldByName('Name').AsString := 'Snoopy';
DS.FieldByName('DogBreed').AsString := 'Beagle';
DS.Post; // Create a new TDog instance
DS.Append;
DS.ObjectClass := TBird; // change base class to TBird
DS.FieldByName('Name').AsString := 'Tweetie';
DS.Post; // Create a new TBird instance. DogBreed field is ignored

Enumeration Fields

Fields that relate to an enumerated type are integer fields that hold the ordinal value of the enumeration. Example:

type TSex = (tsMale, tsFemale);

TheSex := TSex(DS.FieldByName('Sex').AsInteger);
DS.FieldByName('Sex').AsInteger := Ord(tsFemale);

Alternatively, you can use the suffix .EnumName after the property name so you can read and write the values in string format (string fields):

SexName := DS.FieldByName('Sex.EnumName').AsString;
DS.FieldByName('Sex.EnumName').AsString := 'tsFemale';

Fields for Projection Values

When using projections in queries, the result objects might be objects of type TCriteriaResult. TAureliusDataset treats such values as fields, so you can define a field for each projection value. Since TAureliusDataset cannot tell in advance what are the available fields, you must previously define the persistent fields for each aliased projection.

The following code snippet illustrates how you can use projection values in TAureliusDataset:

with TStringField.Create(Self) do
begin
  FieldName := 'CountryName';
  Dataset := AureliusDataset1;
  Size := 50;
end;
with TIntegerField.Create(Self) do
begin
  FieldName := 'Total';
  Dataset := AureliusDataset1;
end;

// Retrieve number of customers grouped by country
AureliusDataset1.SetSourceCriteria(
  Manager.Find<TCustomer>
    .Select(TProjections.ProjectionList
      .Add(TProjections.Group('Country').As_('CountryName'))
      .Add(TProjections.Count('Id').As_('Total'))
      )
    .AddOrder(TOrder.Asc('Total'))
);

// Retrieve values for the first record: country name and number of customers
FirstCountry := AureliusDataset1.FieldByName('CountryName').AsString;
FirstTotal := AureliusDataset1.FieldByName('Total').AsInteger;
Note

TCriteriaResult objects provided to the dataset might be automatically destroyed when the dataset closes, depending on how you provide objects to the dataset. If you use SetSourceCursor or SetSourceCriteria, they are automatically destroyed. When you use SetSourceList or SetSourceObject, they are not destroyed and you need to do it yourself.

Modifying Data

Modifying data with TAureliusDataset is just as easy as with any TDataset component. Call Edit, Insert, Append methods, and then call Post to confirm or Cancel to rollback changes.

It's worth noting that TAureliusDataset loads and saves data from and to the objects in memory. It means when a record is posted, the underlying associated object has its properties updated according to field values. However the object is not necessarily persisted to the database. It depends on if the Manager property is set, or if you have set event handlers for object persistence, as illustrated in code below:

// Change Customer1.Name property
DS.Close;
DS.SetSourceObject(Customer1);
DS.Open;
DS.Edit;
DS.FieldByName('Name').AsString := 'John';
DS.Post;
// Customer1.Name property is updated to "John".
// Saving on database depends on setting Manager property
// or setting OnObjectUpdate event handler

New Objects When Inserting Records

When you insert new records, TAureliusDataset will create new object instances and add them to the underlying object list provided to the dataset.

The object might be created when the record enters insert state (default) or only when you post the record (if you set CreateObjectOnPost to true). The class of object being created is specified by the base class (either retrieved from the list of objects or manually using ObjectClass property). See Default Fields and Base Class topic for more details.

In the following code, a new TCustomer object will be created when Append is called (if you call Cancel the object will be automatically destroyed):

Customers := TObjectList<TCustomer>.Create;
DS.SetSourceList(Customer); // base class is TCustomer
DS.Open;
DS.Append; // Create a new TCustomer instance
DS.FieldByName('Name').AsString := 'Jack';
DS.Post;
// Destroy Customers list later!

If you set CreateObjectOnPost to true, the object will only be created on Post:

Customers := TObjectList<TCustomer>.Create;
DS.SetSourceList(Customer); // base class is TCustomer
DS.Open;
DS.Append;
DS.FieldByName('Name').AsString := 'Jack';
DS.Post; // Create a new TCustomer instance
// Destroy Customers list later!

Alternatively, you can set OnCreateObject event handler. If the event handler sets a valid object into the NewObject parameter, the dataset will not create the object. If NewObject is unchanged (remaining nil), then a new object of the class specified by the base class is created internally.

Here is an example of how to use it:

procedure TForm1.AureliusDataset1CreateObject(Dataset: TDataset; var NewObject: TObject);
begin
  NewObject := TBird.Create;
end;

//<snip>

AureliusDataset1.OnCreateObject := AureliusDataset1CreateObject;
AureliusDataset1.Append; // a TBird object named "Tweetie" will be created here
AureliusDataset1.FieldByName('Name').AsString := 'Tweetie';
AureliusDataset1.Post;
Note

After Post, objects created by TAureliusDataset are not destroyed anymore. See Objects Lifetime Management for more information.

Manager Property

When posting records, object properties are updated, but are not persisted to the database, unless you manually set events for persistence, or set the Manager property. If you set it to a valid TObjectManager object, then when records are posted or deleted, TAureliusDataset will use the specified manager to persist the objects to the database, either saving, updating or removing the objects.

Customers := TAureliusDataset.Create(Self);
CustomerList := TList<TCustomer>.Create;
Manager := TObjectManager.Create(MyConnection);
try
  Customers.SetSourceList(CustomerList);
  Customers.Open;
  Customers.Append;
  Customers.FieldbyName('Name').AsString := 'Jack';
  // On post, a new TCustomer object named "Jack" is created, but not saved to database
  Customers.Post;

  // Now set the manager
  Customers.Manager := Manager;

  Customers.Append;
  Customers.FieldbyName('Name').AsString := 'John';
  // From now on, any save/delete operation on dataset will be reflected on database
  // A new TCustomer object named "John" will be created, and Manager.Save
  // will be called to persist object in database
  Customers.Post;

  // Record is deleted from dataset and object is removed from database
  Customers.Delete;
finally
  Manager.Free;
  Customers.Free;
  CustomerList.Free;
end;

In summary: if you want to manipulate objects only in memory, do not set the Manager property. If you want dataset changes to be reflected in database, set Manager property or use events for manual persistence.

Please refer to the topic Dataset Fields to learn how the Manager property is propagated to datasets which are linked to dataset fields.

Objects Lifetime Management

TAureliusDataset usually does not manage any object it holds, either the entity objects itself, the list of objects that you pass in SetSourceList when providing objects to it, or the objects it created automatically when inserting new records. So you must be sure to destroy all of them when needed!

Even when deleting records, the object is not destroyed (if no Manager is attached). The following code causes a memory leak:

Customers := TAureliusDataset.Create(Self);
CustomerList := TList<TCustomer>.Create;
try
  Customers.SetSourceList(CustomerList);
  Customers.Open;
  Customers.Append;
  Customers.FieldbyName('Name').AsString := 'Jack';

  // On post, a new TCustomer object named "Jack" is created, but not saved to database
  Customers.Post;

  // Record is deleted from dataset, but object is NOT DESTROYED
  Customers.Delete;
finally
  Manager.Free;
  Customers.Free;
  CustomerList.Free;
end;

In code above, a new object is created in Post, but when record is deleted, object is not destroyed, although it's removed from the list.

Be aware that the TObjectManager object itself manages the objects. If you set the Manager property of the dataset, then records being saved will cause objects to be saved or updated by the manager, meaning they will be managed by it. So usually you would not need to destroy objects if you are using a TObjectManager associated with the dataset (but you would still need to destroy the TList object holding the objects).

Exceptions

There are only two exceptions when objects are destroyed by the dataset:

  1. A record in Insert state is not Posted.
    When you Append a record in the dataset, an object is created (unless CreateObjectOnPost property is set to true). If you then Cancel the inserting of this record, the dataset will silently destroy that object.

  2. When objects of type TCriteriaResult are passed using SetSourceCursor or SetSourceCriteria.
    In this case the objects are destroyed by the dataset.

Manual Persistence Using Events

To properly persist objects to the database and manage them, you would usually use the Manager property and associate a TObjectManager to the dataset.

Alternatively, you can use events for manual persistence and management. Maybe you just want to keep objects in memory but need to destroy them when records are deleted, so you can use OnObjectRemove. Or maybe you just want to hook a handler for the time when an object is updated and perform additional operations.

The following events are available in TAureliusDataset:

  • OnObjectInsert: called when a record is posted after an Insert or Append operation, right after the object instance is created.
  • OnObjectUpdate: called when a record is posted after an Edit operation.
  • OnObjectRemove: called when a record is deleted.

In all events, the AObject parameter refers to the object associated with the current record.

Note

If one of those event handlers is set, the TObjectManager specified in the Manager property will be ignored and not used. So if for example you set an event handler for OnObjectUpdate, be sure to persist it to the database if you want to, because Manager.Update will not be called even if Manager property is set.

Locating Records

TAureliusDataset supports usage of Locate method to locate records in the dataset. Use it just as with any regular TDataset descendant:

Found := AureliusDataset1.Locate('Name', 'mi', [loCaseInsensitive, loPartialKey]);

You can perform locate on entity fields. Just note that since entity fields hold a reference to the object itself, you just need to pass a reference in the locate method. Since objects cannot be converted to variants, you must typecast the reference to an Integer or IntPtr (Delphi XE2 and up):

{$IFDEF DELPHIXE2}
Invoices.Locate('Customer', IntPtr(Customer), []);
{$ELSE}
Invoices.Locate('Customer', Integer(Customer), []);
{$ENDIF}

The Customer object must be the same reference. Even if a customer object has the same Id as the object in the dataset, if the object references are not the same, Locate will fail. Alternatively, you can also search on sub-property fields:

Found := Invoices.Locate('Customer.Name', Customer.Name, []);

In this case, the record will be located if the customer name matches the specified value, regardless if object references are the same or not.

You can also search on calculated and lookup fields.

Calculated Fields

You can use calculated fields in TAureliusDataset the same way as with any other dataset. When calculating fields, you can use the regular Dataset.FieldByName approach, or use Current to access the object properties directly:

procedure TForm1.AureliusDataset1CalcFields(Dataset: TDataset);
begin
  if AureliusDataset1.FieldByName('Birthday').IsNull then
    AureliusDataset1.FieldByName('BirthdayText').AsString := 'not specified'
  else
    AureliusDataset1.FieldByName('BirthdayText').AsString :=
      DateToStr(AureliusDataset1.FieldByName('Birthday').AsDateTime);

  case AureliusDataset1.Current<TCustomer>.Sex of
    tsMale:
      AureliusDataset1.FieldByName('SexDescription').AsString := 'male';
    tsFemale:
      AureliusDataset1.FieldByName('SexDescription').AsString := 'female';
  end;
end;

Lookup Fields

You can use lookup fields with TAureliusDataset, either at design-time or runtime. Usage is not different from any TDataset.

One thing worth noting is how to use a lookup field for entity fields (associations), which is probably the most common usage. Suppose you have a TInvoice class with a property Customer that is an association to a TCustomer class. You can have two datasets with TInvoice and TCustomer data, and you want to create a lookup field in Invoices dataset to lookup for a value in Customers dataset, based on the value of Customer property.

Since "Customer" is an entity field in the Invoices dataset, you need to lookup for its value in the Customers dataset using the "Self" field, which represents a reference to the TCustomer object in the Customers dataset. The following code illustrates how to create a lookup field in Invoices dataset to lookup for the customer name based on "Customer" field:

// Invoices is a dataset which data is a list of TInvoice objects
// Customers is dataset which data is a list of TCustomer objects

// Create the lookup field in Invoices dataset
LookupField := TStringField.Create(Invoices.Owner);
LookupField.FieldName := 'CustomerName';
LookupField.FieldKind := fkLookup;
LookupField.Dataset := Invoices;
LookupField.LookupDataset := Customers;
LookupField.LookupKeyFields := 'Self';
LookupField.LookupResultField := 'Name';
LookupField.KeyFields := 'Customer';

Being a regular lookup field, this approach also works with components like TDBLookupComboBox and TDBGrid. It would display a combo with a list of customer names, and will allow you to change the customer of the TInvoice object by choosing the item in the combo.

Filtering

TAureliusDataset supports filtering of records by using the regular TDataset.Filtered property and TDataset.OnFilterRecord event. It works just as any TDataset descendant. When filtering records, you can use the regular Dataset.FieldByName approach, or use Current to access the object properties directly:

procedure TForm1.DatasetFilterRecord(Dataset: TDataset; var Accept: boolean);
begin
  Accept :=
    (Dataset.FieldByName('Name').AsString = 'Toby')
    or
    (TAureliusDataset(Dataset).Current<TAnimal> is TMammal);
end;

//<snip>

begin
  AureliusDataset1.SetSourceList(Animals);
  AureliusDataset1.Open;
  AureliusDataset1.OnFilterRecord := DatasetFilterRecord;
  AureliusDataset1.Filtered := True;
end;

Design-time Support

TAureliusDataset is installed in Delphi component palette and can be used at design-time. As with any TDataset component you can set its fields using the fields editor, specify master-detail relationships by setting DatasetField property to a dataset field, create lookup fields, among other common TDataset tasks.

However, creating fields manually might be a boring task, especially if you have a class with many properties and need to create many fields manually. So TAureliusDataset provides a design-time menu option named "Load Field Definitions..." (right-click on the component), which allows you to load a class from a package and create the field definitions from that class.

aureliusdataset design menuoption

A dialog appears allowing you to choose a class to import the definitions from. Note that the classes are retrieved from available packages. By default, classes from packages installed in the IDE are retrieved. If you want to use a package that is not installed, you can add it to the packages list. For a better design-time experience with TAureliusDataset, create a package with all your entity classes, compile it, and load it in this dialog.

aureliusdataset design importfields

The packages in the list are saved in the registry so you can reuse it whenever you need. To remove the classes of a specified package from the combo box, just uncheck the package. The package will not keep loaded: when the dialog closes, the package is unloaded from memory.

Note that the dialog will fill the FieldDefs property, not create field components in the fields editor. The FieldDefs behaves as if the field definitions are being retrieved from a database. You would still need to create the field components, but now you can use the FieldDefs to help you, so you can use "Add All Fields" or "Add Field..." options from the fields editor popup menu. The FieldDefs property is persisted in the form so you don't need to reload the package in case you close the form and open it again.

Additional Properties and Methods

TAureliusDataset exposes additional properties and methods beyond what is covered above — see the API reference for the full list. A few notable ones:

  • FillRecord: updates all dataset field values with the respective property values of a given object. Useful to "copy" all values from an object to the dataset fields without entering edit state manually.
  • RefreshRecord: updates all dataset field values from the existing underlying object. Use it when you have modified the object properties directly and want the dataset to reflect such changes.
  • SubpropsDepth: when set to 1 or more, automatically creates sub-property fields for each association, up to the specified depth. For example, if set to 1 and there is an association field "Customer", the dataset will also create fields like "Customer.Name", "Customer.Birthday", etc. Default is 0.
  • SyncSubProps: when true, automatically updates sub-property fields (e.g., "Customer.Name") whenever the parent entity field (e.g., "Customer") is modified. Default is false.
  • DefaultsFromObject: when true, default field values come from the underlying object's current state when inserting new records. Default is false (all fields start as null).
  • CreateSelfField: when false, suppresses automatic creation of the Self field in the default field definitions. Default is true.
  • IncludeUnmapped​Objects: when true, also creates field definitions for object and list properties that are not mapped by Aurelius — transient properties become visible and editable. Default is false.
  • RecordCountMode: controls how record count is determined in paged mode. See TRecordCountMode below.

TFieldInclusions

FieldInclusions is a set property that determines which categories of fields are created automatically when the dataset opens and no persistent fields are defined. The default value is [TFieldInclusion.Entity, TFieldInclusion.Dataset].

  • TFieldInclusion.​Entity: when present, TAureliusDataset creates entity fields for properties that hold object instances (associations). For example, for a class TCustomer with a property Country of type TCountry, an entity field "Country" will be created.

  • TFieldInclusion.​Dataset: when present, TAureliusDataset creates dataset fields for properties that hold object lists. For example, for a class TInvoice with a property Items of type TList<TInvoiceItem>, a dataset field "Items" will be created.

TRecordCountMode

When using TAureliusDataset in paged mode with SetSourceCriteria, the total number of records is not known in advance until all pages are retrieved. The RecordCountMode property controls how RecordCount is returned:

  • TRecordCount​Mode.​Default: RecordCount returns -1 until all records are fetched. No extra statements are performed.

  • TRecordCount​Mode.​Retrieve: an extra statement is executed in the database to retrieve the total number of records. RecordCount will return the correct value even if not all records have been fetched. The extra statement is only executed if RecordCount is actually read.

  • TRecordCount​Mode.​FetchAll: all records are retrieved in order to compute the count. This has maximum performance penalty but always returns the correct value.