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
:
uses
{...}, Aurelius.Bind.Dataset;
Basic usage is done by these steps:
Set the source of data to be associated with the dataset, using SetSourceList method, or a single object, using SetSourceObject.
Optionally, create a TField for each property/association/sub-property you want to display/edit. If you do not, default fields will be used.
Optionally, specifiy 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 method:
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, TAureliusDataset will not destroy it. 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 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 method 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 method, 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, just call SetSourceCursor method and pass the ICriteriaCursor interface you have 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 method.
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 just execute the query specified by the TCriteria and fill the internal object list with the retrieved objects.
This approach is actually not very different than 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 method has an overloaded signature that received 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, when new records are inserted or deleted, they are added to and removed from the internal list. 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.
So, there might be situations where you need to access such list. TAureliusDataset provides a property InternalList for that. This property is declared as following:
property InternalList: IReadOnlyObjectList;
The list is accessible through a IReadOnlyObjectList, so you can't modify it (unless, of course, indirectly by using the TDataset itself). The IReadOnlyObjectList has the following methods:
IReadOnlyObjectList = interface
function Count: integer;
function Item(I: integer): TObject;
function IndexOf(Obj: TObject): integer;
end;
Count method returns the current number of objects in the list.
Item method returns the object in the position I of the list (0-based).
IndexOf method returns the position of the object Obj in the list (also 0-based).
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:
If you provide objects by passing a generic list to SetSourceList method, Aurelius will consider the base class as the generic type in the list. For example, if the list type it TList<TCustomer>, then the base class will be TCustomer.
If you provide an object by using SetSourceObject, the base class will just be the class of object passed to that method.
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. Just like any other TDataset, you do that at design-time, or at runtime:
with TStringField.Create(Self) do
begin
FieldName := 'Country.Name';
Dataset := AureliusDataset1;
end;
Entity Fields (Associations)
Entity Fields are fields that maps to an object property in a container object. In other words, entity fields represent associations in the object. Consider the following class:
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;
By default, TAureliusDataset will create fields "Id" and "Name" (scalar fields) and "Country" (entity field). An entity field is just a field of type TAureliusEntityField that holds a reference to the object itself. Since Delphi DB library doesn't provide a field representing an object pointer (which makes sense), this new field type is provided by TMS Aurelius framework for you to manipulate the object reference.
The TAureliusEntityField is just a TVariantField descendant with an additional AsObject property, and an addition generic AsEntity<T> function that you can use to better manipulate the field content. To access such properties, you can just cast the field to TAureliusEntityField, or use TAureliusDataset.EntityFieldByName method.
Please note that the entity field just represents an object reference. It's useful for lookup fields and to programatically change the object reference in the property, but it's not useful (and should not be used) for visual binding, like a TDBGrid or to be edited in a TDBEdit, since its content is just a pointer to the object. To visual bind properties of associated objects, use sub-property fields.
The following code snippets are examples of how to use the entity field.
// following lines are equivalent and illustrates how to set an association through the dataset
AureliusDataset1.EntityFieldByName('Country').AsObject := TCountry.Create;
(AureliusDataset1.FieldByName('Country') as TAureliusEntityField).AsObject := TCountry.Create;
Following code shows how to retrieve the value of an association property using the dataset field:
Country := AureliusDataset1.EntityFieldByName('Country').AsEntity<TCountry>;
Dataset Fields (Many-Valued Associations)
Dataset fields represent collections in a container object. In other words, dataset fields represent many-valued associations in the object. 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 expected to be a TDatasetField, and represents all objects (records) in the Items collection. Different from entity fields, you don't access a reference to the list itself, using the dataset field.
In short, you can use the TDatasetField to build master-detail relationships. You can have, for example, a TDBGrid linked to a dataset representing a list of TInvoice objects, and a second TDBGrid linked to a dataset representing a list of TInvoiceItem objects. To link the second dataset (invoice items) to the first (invoices) you just need to set the DatasetField property of the second dataset. This will link the detail dataset to the collection of items in the first dataset. You can do it at runtime or design-time.
The following code snippet illustrates better how to link two datasets using the dataset field. It's worth to note that these dataset fields work as a regular TDatasetField. For a better understanding of how a TDatasetField works, please refer to Delphi documentation.
InvoiceDataset.SetSourceList(List);
InvoiceDataset.Manager := Manager1;
InvoiceDataset.Open;
ItemsDataset.DatasetField := InvoiceDataset.FieldByName('Items') as TDatasetField;
ItemsDataset.Open;
Note that by default there is no need to set the Manager property of nested datasets. There is a TAureliusDataset.ParentManager property which defaults to true, that indicates that the Manager of the dataset will be same as the Manager of the parent dataset (which is the dataset of the linked DatasetField). In this case, whenever you Post or Delete a record in the detail dataset, the detail object will be 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, for example. 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 the base class is used to create a new object instance when inserting new records (creating objects). The following code illustrates how to use a dataset associated with a TList<TAnimal> and still creating two different object types:
Animals := Manager.FindAll<TAnimal>;
DS.SetSourceList(Animals); // base class is TAnimal
DS.ObjectClass := TDog; // now base is 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 sufix ".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
TCriteria
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
The 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. This is because since the objects are fetched automatically by the dataset, it manages it's life-cycle. 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 note that TAureliusDataset load and save 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
The following topics explain some more details about modifying data with TAureliusDataset.
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 TAureliusDataset.CreateObjectOnPost property 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!
Setting the base class manually is also important if you are using heterogeneous lists and want to create instances of different classes when posting records, depending on an specific situation.
Alternatively, you can set OnCreateObject event handler. This event is called when the dataset needs to create the object, and the event type declaration is below:
type
TDatasetCreateObjectEvent = procedure(Dataset: TDataset; var NewObject: TObject) of object;
//<snip>
property OnCreateObject: TDatasetCreateObjectEvent;
If the event handler sets a valid object into 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';
AureliusDatase1.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 Manager property. If you set the Manager property 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 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 using 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! The only two exceptions are described at the end of this topic.
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 the Post, but when record is deleted, object is not destroyed, although it's removed from the list.
But, be aware that the TObject
Exceptions
There are only two exceptions when objects are destroyed by the dataset:
A record in Insert state is not Posted.
When you Append a record in the dataset, an object is created (unless CreateObjectsOnPost property is set to true). If you then Cancel the inserting of this record, the dataset will silently destroy that object.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 by properly destroying when needed, you would usually use the Manager property and associate a TObjectManager object to the dataset.
Alternatively, you can also 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 event. Or maybe you just want to hook a handler for the time when an object is updated and perform additional operations.
The following events for handling objects persistence are available in TAureliusDataset, and all of them are of type TDatasetObjectEvent:
type
TDatasetObjectEvent = procedure(Dataset: TDataset; AObject: TObject) of object;
//<snip>
property OnObjectInsert: TDatasetObjectEvent;
property OnObjectUpdate: TDatasetObjectEvent;
property OnObjectRemove: TDatasetObjectEvent;
OnObjectInsert event is called when a record is posted after an Insert or Append operation, right after the object instance is created.
OnObjectUpdate event is called when a record is posted after an Edit operation.
OnObjectRemove event is called when a record is deleted.
In all events, the AObject parameter related to the object associated with the current record.
Note
If one of those event handlers are set, the object manager specified in Manager property will be ignored and not used. So if for example you set an event handler for OnObjectUpdate event, 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. Even if 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 with any other dataset. Note that when calculating fields, you can use regular Dataset.FieldByName approach, or you can use Current<T> property and 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 it's worth note, though, is how to use 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 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 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 componentes like TDBLookupComboBox and TDBGrid. It would display a combo with a list of customer names, and will allow you to change the customer of TInvoice object by choosing the item in combo (the field "Customer" in Invoices dataset will be updated with the value of field "Self" in Customers dataset).
Filtering
TAureliusDataset supports filtering of records by using regular TDataset.Filtered property and TDataset.OnFilterRecord event. It works just as any TDataset descendant. Note that when filtering records, you can use regular Dataset.FieldByName approach, or you can use Current<T> property and 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 and as any TDataset component you can set its fields using 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.
A dialog appears allowing you to choose a class to import the definitions from. Note that the classes are retrieving 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. So, for a better design-time experience with TAureliusDataset, create a package with all your entity classes, compile it, and load it in this dialog.
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. That's its only purpose, and they are not used at runtime.
Other Properties And Methods
List of TAureliusDataset methods and properties not coverered by other topics in this chapter.
Methods
Name | Description |
---|---|
procedure FillRecord(Obj: TObject); | Updates all dataset field values with the respective property values of Obj object. This is useful to "copy" all values from Obj to the dataset fields. |
procedure RefreshRecord; | Updates all dataset field values from the existing underlying object. Use RefreshRecord if you have modified the object properties directly and want the dataset to reflect such changes. |
Properties
Name | Description |
---|---|
CreateSelfField: Boolean | When True (default), the dataset will include the Self field in the list of default fieldsdefs. If False, the field will not be created. |
DefaultsFromObject: Boolean | When True, brings field default values with object state. When inserting a new record in TAureliusDataset, all fields come with null values by default (DefaultsFromObject is False). By setting this property to True, default (initial) value of the fields will come from the property values of the underlying object. |
FieldInclusions: TFieldInclusions | Determines which special "categories" of fields will be created automatically by the dataset when it's open and no persistent fields are defined. This is a set of TFieldInclusion enumeration type which have the following options. The value of this property by default is [TFieldInclusion.Entity, TFieldInclusion.Dataset]. |
IncludeUnmappedObjects: Boolean | When True, the dataset will also create field definitions for object (and lists) properties that are not mapped. In other words, you can view/edit transient object properties. The default is False which means only Aurelius associations will be visible. |
ReadOnly: Boolean | If true, puts the dataset in read-only mode, so data cannot be edited by visual data-aware controls. Default is false. |
SubpropsDepth: Integer | Allows automatic loading of subproperty fields. When loading field definitions for TAureliusDataset at design-time, or when opening the TAureliusDataset without persistent fields, one TField for each property in object will be created. By increasing SubpropsDepth to 1 or more, TAureliusDataset will also automatically include subproperty fields for each property in each association, up to the level indicated by SubpropsDepth. For example, if SubpropsDepth is 1, and there is an association field named "Customer", the dataset will also create fields like "Customer.Name", "Customer.Birthday", etc.. Default is 0 (zero). |
SyncSubProps: Boolean | Allows automatic updating of associated fields. When an entity field (e.g., "Customer") of the TAureliusDataset component is modified, all the subproperty fields (e.g., "Customer.Name", "Customer.Birthday") will be automatically updated with new values if this property is set to True. Default is False. |
RecordCountMode: TRecordCountMode | When using dataset in paged mode using SetSourceCriteria, by default the total number of records is not known in advance until all pages are retrieved. RecordCount property returns -1 until all records are fetched. You can use this property change the dataset algorithm used to return the RecordCount property value. See below the valid values. |
TFieldInclusions
type
TFieldInclusion = (Entity, Dataset);
TFieldInclusions = set of TFieldInclusion;
TFieldInclusion.Entity:
If present, Aurelius dataset will create entity fields for properties that hold object instances (usually associations). For example, for a class TCustomer with a property Country of type TCountry, an entity field "Country" will be created.TFieldInclusion.Dataset:
If present, Aurelius dataset will create 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
type
TRecordCountMode = (Default, Retrieve, FetchAll);
TRecordCountMode.Default:
RecordCount always return -1 if not all records are fetched from the database. No extra statements are performed.TRecordCountMode.Retrieve:
An extra statement will performed in database to retrieve the total number of records to be retrieved. RecordCount property will return the correct value even if not all records are fetched from the database. This has a small penalty performance since it requires another statement to be executed. The extra statement will only be executed if RecordCount property is read.TRecordCountMode.FetchAll:
All records will be retrieved to properly return the RecordCount value. This is maximum penalty performance in exchange for always returning the correct value of RecordCount property.