Search Results for

    Show / Hide Table of Contents

    Manipulating Objects

    This chapter explains how to manipulate objects. Once you have properly connected to the database and configure all mapping between the objects and the database, it's time for the real action. The following topics explain how to save, update, delete and other topics about dealing with objects. Querying objects using complex criteria and projections is explained in a specific chapter only for queries.

    Object Manager

    The Object Manager is implemented by the TObjectManager class which is declared in unit Aurelius.Engine.ObjectManager:

    uses 
      {...}, Aurelius.Engine.ObjectManager;
    

    It's the layer between your application and the database, providing methods for saving, loading, updating, querying objects. It performs memory management, by controlling objects lifetime cycle, destroying them when they are not needed anymore, and caching objects by using identity mappings to ensure a single object is not loaded twice in the same manager.

    The Object Manager also keeps tracking of changes in objects - you can update the content of objects (change properties, add associations, etc.) and then call Flush method to ask the object manager to update all object changes in the database at once.

    The list below is a quick reference for the main methods and properties provided by TObjectManager object. A separate topic is provided for each method listed below.

    Creating a new object manager

    Directly create a TObjectManager instance, passing the IDBConnection interface that represents a database connection:

    Manager := TObjectManager.Create(MyConnection);
    try
      // perform operations with objects
    finally
      Manager.Free; 
    end;
    

    Alternatively, you can also pass a TMappingExplorer instance, which holds a mapping model different than the default.

    Manager := TObjectManager.Create(MyConnection, MyMappingExplorer);
    // or
    Manager := TObjectManager.Create(MyConnection, TMappingExplorer.Get('MyModel'));
    

    Save method

    Use it to save (insert into database) new entity objects:

    Customer := TCustomer.Create;
    Customer.Name := 'TMS Software';
    Manager.Save(Customer);
    

    Update method

    Use it to including a transient into the manager. To effectively persist updates, you need to call Flush method.

    Customer := TCustomer.Create;
    Customer.Id := 10;
    Customer.Email := 'customer@company.com';
    Manager.Update(Customer);
    

    SaveOrUpdate method

    Use it to save or update an object depending on the Id specified in the object (update if there is an Id, save it otherwise):

    Customer.LastName := 'Smith';
    Manager.SaveOrUpdate(Customer);
    

    Flush method

    Performs all changes made to the managed objects, usually to update objects.

    Customer1 := Manager.Find<TCustomer>(1);
    Customer2 := Manager.Find<TCustomer>(2);
    Customer1.Email := 'company@address.com';
    Customer2.City := 'Miami';
    
    // Update Customer1 e-mail and Customer2 city in database
    Manager.Flush;
    

    Flush method for single entity

    Commit to the database changes made to a single object - it's an overloaded version of Flush method that receives an object:

    Customer1 := Manager.Find<TCustomer>(1);
    Customer2 := Manager.Find<TCustomer>(2);
    Customer1.Email := 'company@address.com';
    Customer2.City := 'Miami';
    
    // Update Customer1 e-mail - Customer2 changes are not persisted
    Manager.Flush(Customer1);
    

    Merge method

    Use it to merge a transient object into the object manager and obtain the persistent object.

    Customer := TCustomer.Create;
    Customer.Id := 12;
    Customer.Name := 'New name';
    ManagedCustomer := Manager.Merge<TCustomer>(Customer);
    

    In the example above, Merge will look in the cache or database for a TCustomer with id equals to 12. If it's not found, an exception is raised. If found, it will update the cached customer object with the new information and return a reference to the cached object in ManagedCustomer. Customer reference will still point to an unmanaged object, so two instances of TCustomer will be in memory.

    Replicate method

    The Replicate method behaves exactly the same as the merge method above. The only difference is that, in the example above, if no Customer with id 12 is found in the database, instead of raising an exception, Replicate will create the new customer with that id.

    Find method

    Use Find method to retrieve an object given its Id:

    Customer := Manager.Find<TCustomer>(CustomerId);
    

    The id value is a variant type and must contain a value of the same type of the class Identifier (specified with the Id attribute). For example, if the identifier is a string type, id value must be a variant containing a string. For classes with composite id, a variant array of variant must be specified with all the values of the id fields.

    You can alternatively use the non-generic overload of Find method. It might be useful for runtime/dynamic operations where you don't know the object class at the compile time:

    Customer := Manager.Find(TCustomer, CustomerId);
    

    Remove method

    Use it to remove the object from the persistence (i.e., delete it from database and from object manager cache).

    CustomerToRemove := Manager.Find<TCustomer>(CustomerId);
    Manager.Remove(CustomerToRemove);
    

    Find<T> method

    Use Find<T> to create a new query to find objects based on the specified criteria.

    Results := Manager.Find<TTC_Customer>
      .Where(Linq['Name'] = 'Mia Rosenbaum')
      .List;
    

    CreateCriteria<T> method

    CreateCriteria is just an alias for Find<T> method. Both are equivalent:

    Results := Manager.CreateCriteria<TTC_Customer>
      .Where(Linq['Name'] = 'Mia Rosenbaum')
      .List;
    

    Evict method

    Use to evict (dettach) an entity from the manager:

    Manager.Evict(Customer);
    

    IsAttached method

    Checks if the specified object instance is already attached (persistent) in the object manager.

    if not Manager.IsAttached(Customer) then
      Manager.Update(Customer);
    

    FindCached<T> method

    Use FindCached method to retrieve an object from the manager's cache, given its Id.

    Customer := Manager.FindCached<TCustomer>(CustomerId);
    

    This method is similar to Find method but the difference is that if the object is not in manager cache, Aurelius will not hit the database to retrieve the objec - instead, it will return nil. Because of that, this method should be used only to check if the object is already in the manager - it's not useful to retrieve data from database.

    You can alternatively use the non-generic overload of FindCached method. It might be useful for runtime/dynamic operations where you don't know the object class at the compile time:

    Customer := Manager.FindCached(TCustomer, CustomerId);
    

    IsCached<T> method

    Checks if an object of the specified class with the specified id is already loaded in the object manager.

    if not Manager.IsCached<TCustomer>(CustomerId) then
      ShowMessage('Not loaded');
    

    You can use the non-generic version as well:

    if not Manager.IsCached(TCustomer, CustomerId) then
      ShowMessage('Not loaded');
    

    HasChanges method

    Checks if a call to Flush will result in database operations. In other words, verifies if any object in the manager was modified since it was loaded from the database. HasChanges checks not only if properties were modified but also if any lists of the object has been modified (an item was added or removed).

    Customer := Manager.Find<TCustomer>(1);
    Customer.Email := 'company@address.com';
    if Manager.HasChanges then
      // code will enter here as Email was changed
    

    HasChanges checks for any change in all objects in manager. You can use an overload that receives an object as parameter, to check if that specific entity was modified:

    if Manager.HasChanges(Customer) then // only checks for changes in Customer
    

    OwnsObjects property

    If true (default), all managed objects are destroyed when the TObjectManager object is destroyed. If false, the objects remain in memory.

    Customer := Manager.Find<TCustomer>(CustomerId);
    Manager.OwnsObjects := false;
    Manager.Free;
    // Customer object is still available after Manager is destroyed
    

    ProxyLoad and BlobLoad methods

    Use to load a proxy object (or blob) based on meta information (see Lazy-Loading with JSON for more information).

    function ProxyLoad(ProxyInfo: IProxyInfo): TObject;
    function BlobLoad(BlobInfo: IBlobInfo): TArray<byte>;
    

    UseTransactions property

    When true, all internal operations peformed by the object manager (Save, Update, Merge, Remove, etc.) as enclosed between transactions (it means if no transaction is active, the manager will create one just for the operation, then later commit). This is needed because even a single manager operation can perform several SQL statements in the database (due to cascades for example).

    If false, the manager won't create new transactions, and it's up to you to make sure that a transaction is active, otherwise if the internal process fails, some records might become updated in the database, while others don't.

    The default value of this property is controlled globally by the TGlobalConfiguration object.

    DeferDestruction property

    When true, all manager operations that destroy objects, such as Remove, will not immediately destroy them, but instead hold them in an internal list to be destroyed when the object manager is destroyed.

    This can be useful if you still have references to the removed object in places like lists or datasets, and such references might still be used until manager is destroyed. This sometimes avoids invalid pointer operations and access violations caused by referencing those destroyed instances.

    For backward compatibility, default value is false.

    TAureliusManager Component

    The TAureliusManager component is a non-visual, design-time component that encapsulates the TObjectManager class, used to persist and retrieve objects in database.

    TAureliusManager and TObjectManager have equivalent functionality, the main purpose for TAureliusManager component is to provide an alternative RAD approach: instead of instantiating a TObjectManager from code, you just drop a TAureliusManager component, connects it to a TAureliusConnection component, and you are ready to go.

    Key properties

    Name Description
    Connection: TAureliusConnection Specifies the TAureliusConnection component to be used as the database connection. Any persistence operation performed by the manager will use the connection provided by the TAureliusConnection component.

    TAureliusConnection acts as a connection pool of one single connection: it will create a single instance of IDBConnection and any manager using it will use the same IDBConnection interface for the life of the TAureliusConnection component.

    The IDBConnection interface will be passed to the TObjectManager constructor to create the instance that will be encapsulated.
    ModelName: string The name of the model to be used by the manager. You can leave it blank, if you do it will use the default model. From the model name it will get the property TMappingExplorer component that will be passed to the TObjectManager constructor to create the instance that will be encapsulated.
    ObjManager: TObjectManager The encapsulated TObjectManager instance used to perform the database operations.

    Usage

    As mentioned, TAureliusManager just encapsulates a TObjectManager instance. So for all functionality (methods, properties), just refer to TObjectManager documentation and related topics that explain how to save objects, update them, retrieve, querying, etc.

    The encapsulated object is available in property ObjManager. If you miss any specific method or property in TAureliusManager, you can simply fall back to ObjManager instance and use it from there. For example, the following methods are equivalent:

    AureliusManager1.Save(Customer);
    AureliusManager1.ObjManager.Save(Customer);
    

    Actually effectively, the first method is just a wrapper for the second one. Here is how TAureliusManager.Save method is implemented, for example:

    procedure TAureliusManager.Save(Entity: TObject);
    begin
      ObjManager.Save(Entity);
    end;
    

    TObjectManager memory management

    All entities are managed inside the TObjectManager instance, using the regular memory management mechanism of such class. The only thing you should be aware is the lifecycle of the TObjectManager instance itself:

    • The TObjectManager instance will be created on demand, i.e., when TAureliusManager is created, the TObjectManager is not yet created. It will only be instantiated when needed;

    • If the connection or model name is changed, the encapsulated TObjectManager instance will be destroyed (and as a consequence all entities managed by it). A new TObjectManager instance will be created with the new connection/model, when needed.

    Memory Management

    Entity objects are saved and loaded to/from database using a TObjectManager object, which provides methods and properties for such operations. All entity objects cached in TObjectManager are managed by it, and you don't need to free such objects (unless you set OwnsObjects property to False). Also, entity objects retrieved from database, either loading by identifier or using queries, are also managed by the TObjectManager.

    Concept of object state

    In Aurelius when an object is told to be persistent (or cached, or managed) it means that the TObjectManager object is aware of that object and is "managing" it. When TObjectManager loads any object from the database, the object instances created in the loading process are persistent. You can also turn objects into persistent object when you for example call Save, Update or Merge methods.

    When the TObjectManager is not aware of the object, the object is told to be transient (or uncached, or unmanaged).

    Don't confuse a transient object with an object that is not saved into the database yet. You might have a TCustomer object which has been already saved in the database, but if the TCustomer instance you have is not being managed by the TObjectManager, it's transient.

    Also, don't confuse persistent with saved. A persistent object means that TObjectManager is aware of it and it's managing it, but it might not be saved to the database yet.

    Object lists

    It's important to note that when retrieving object lists from queries, the list itself must be destroyed, although the objects in it are not. Note that when you use projections in queries, the objects returned are not entity objects, but result objects. In this case the objects are not managed by the object manager, but the list retrieved in result queries have their OwnsObjects set to true, so destroying the list will destroy the objects as well.

    Unique instances

    When dealing with entity objects (saving, loading, querying, etc.), object manager keeps an internal Identity Map to ensure that only one instance of each entity is loaded in the TObjectManager object. Each entity is identified by it's Id attribute. So for example, if you execute two different queries using the same object manager, and the query returns the same entity (same id) in the queries, the object instance in the both queries returned will be the same. The object manager will not create a different object instance every time you query the object. If you use a different TObjectManager object for each query, then you will have different instances of the same entity object

    Manually adding ownership

    The manager owns all entity objects it manages, in other words, which are persisted. But sometimes you want to tell the manager to own an object even before a persistence operation, using the AddOwnership method. This is very common when using the Save method. The object passed to Save will only be owned by the manager if the operation is successful. If the operation fails, you would have to manually destroy it.

    To improve this workflow, you can explicitly tell the manager to own the entity to be saved in advance, so regardless if the Save method fails, or not, the manager will always destroy the object:

    Customer := TTC_Customer.Create;
    Customer.Name := 'Customer Name';
    
    // Tell the manager to destroy Customer
    ObjectManager1.AddOwnership(Customer);
    
    // Customer will always be destroyed even 
    // if Save fails
    ObjectManager1.Save(Customer);
    

    Examples

    The code snippets below illustrates several the different situations mentioned above.

    Saving objects

    Customer := TTC_Customer.Create;
    Customer.Name := 'Customer Name';
    ObjectManager1.Save(Customer);
    // From now on, you don't need to destroy Customer object anymore
    // It will be destroyed when ObjectManager1 is destroyed
    

    Loading objects

    Customer := Manager1.Find<TCustomer>(CustomerId);
    Customer2 := Manager1.Find<TCustomer>(CustomerId);
    // Since CustomerId is the same for both queries, the same instance will be
    // returned in Customer and Customer2 (Customer = Customer2), and you don't
    // need to destroy such instance, it's manager by Manager1.
    

    Retrieving entities from queries

    Results := Manager.Find<TCustomer>
      .Add(Linq['Name'] = 'TMS Software')
      .List;
    Results.Free;
    // Results is a TObjectList<TCustomer> object that needs to be destroyed
    // However, the object instances it holds are not destroyed and are kept 
    // in Manager cache. The instances are also ensured to be unique in Manager context
    

    Retrieving projected query results

    Results := Manager.Find<TTC_Estimate>
      .CreateAlias('Customer', 'c')
      .Select(TProjections.ProjectionList
        .Add(TProjections.Sum('EstimateNo'))
        .Add(TProjections.Group('c.Name'))
        )
      .ListValues;
    Results.Free;
    // In this case the query does not return entity objects, but result objects (TCriteriaResult)
    // Such result objects are not managed by TObjectManager. However, in this case,
    // The Results object list is returned with its OwnsObjects property set to true. Thus, when
    // you destroy Results object, the TCriteriaResult objects it holds will also be destroyed.
    

    Using unmanaged objects

    If for some reason you want to keep object instances available even after the object manager is destroyed (for example, after a query, you want to destroy object manager but keep the returned objects in memory), then just set the TObjectManager.OwnsObjects property to false:

    Manager.OwnsObjects := false;
    Results := Manager.Find<TCustomer>
      .Where(Linq['Name'] = 'TMS Software')
      .List;
    Manager.Free;
    // Now although Manager object was destroyed, all objects in Results list
    // will be kept in memory, EVEN if you destroy Results list itself later.
    

    Saving Objects

    Using TObjectManager you can save (insert) objects using Save method. It is analog to SQL INSERT statement - it saves the object in database.

    Customer1 := TCustomer.Create;
    Customer1.Name := 'John Smith';
    Customer1.Sex := tsMale;
    Customer1.Birthday := EncodeDate(1986, 1, 1);
    Manager1.Save(Customer1);
    

    The identifier of the object (mapped using Id attribute) must not have a value, otherwise an exception will be raised - unless the generator defined in Id attribute is TIdGenerator.None. In this case, you must manually provide the id value of the object, and so of course Aurelius will accept an object with an id value. But you must be sure that there are no objects in the database with the same id value, to avoid duplicate values in the primary key.

    When saving an object, associations and items in collections might be saved as well, depending on how cascade options are set when you defined the Association and ManyValuedAssociation attribute. In the example below, customer is defined to have SaveUpdate cascade. It means that when invoice is saved, the customer is saved as well, before the invoice.

    Customer := TTC_Customer.Create;
    Customer.Name := 'Customer Name';
    Invoice := TTC_Invoice.Create;
    Invoice.InvoiceType := 999;
    Invoice.InvoiceNo := 123456;
    Invoice.Customer := Customer;
    Invoice.IssueDate := Date;
    Manager1.Save(Invoice);
    

    You can also use SaveOrUpdate method to save objects. The difference from Save is that if the object has an id value set, SaveOrUpdate will internally call Update method instead of Save method. So, if you use TIdGenerator.None in the Id attribute of your object class, SaveOrUpdate will not work.

    Updating Objects - Flush

    You modify objects using the TObjectManager method Flush. The state of all objects persisted in object manager is tracked by it. Thus, if you change any property of any object after it's loaded by the database, those changes will be updated to the database when Flush method is called. Consider the example below:

    Customer1 := Manager1.Find<TCustomer>(CustomerId);
    Customer1.Email := 'newemail@domain.com';
    Customer2 := Manager1.Find<TCustomer>(Customer2Id);
    Customer2.Email := 'another@email.com';
    Manager1.Flush;
    

    The Flush method will detect all objects which content has been changed since they were loaded, and then update them all in the database. In the example above, both customers 1 and 2 will have their e-mail changed.

    It's possible that, by any reason, you want to update a detached object, in other words, an object that is not being tracked (persisted) by the manager. This might happen, for example, if you loaded an object with the manager, then destroyed the manager but kept the object reference (using TObjectManager.OwnsObjects = false). Or, for example, if you created the object instance yourself, and set its id property to a valid value. In this case the object is not in the manager, but you want to update the database using the object you have.

    In this case, you can use Update method. This method will just take the passed transient instance and attach it to the TObjectManager. Then when you later call Flush, the changes will be persisted to the database. Note that when you call Update, no data is retrieved from the database. This means that the object manager doesn't know the original state of the object (data saved in database). The consequence is that all properties of the object passed to Update method will later be saved to the database when Flush is called. So you must be sure that all the persistent properties of the object have the correct value to be saved to the database.

    Customer2 := Manager1.Find<TCustomer>(Customer2Id);
    Manager1.OwnsObjects := false;
    Manager1.Free;
    Customer2.Name := 'Mary';
    Customer2.Sex := tsFemale;
    Manager2.Update(Customer2);
    Manager2.Flush;
    

    In the example above, a TCustomer object was loaded in Manager1. It's not attached to Manager2. When Update method is called in Manager2, all data in Customer2 object will be updated to the database, and it will become persistent in Manager2.

    The cascades defined in Association attributes in your class are applied here. Any associated object or collection item that has TCascadeType.SaveUpdate defined will also be updated in database.

    Merging

    If you call Update method passing, say, Object1, but there was already another object attached to the TObjectManager with the same id (Object2), an exception will be raised. In this case, you can use Merge method to merge a transient object ("outside" the manager) into a persistent object ("inside" the manager).

    Flushing a single object

    Calling Flush might be slow if you have many entities in the manager. Flush will iterate through all entities and check if any of them is modified - and persist changes to the database. Alternatively, you can flush a single entity by using an overloaded version of Flush that receives a single object:

    Customer1 := Manager1.Find<TCustomer>(CustomerId);
    Customer1.Email := 'newemail@domain.com';
    Customer2 := Manager1.Find<TCustomer>(Customer2Id);
    Customer2.Email := 'another@email.com';
    Manager1.Flush(Customer1);
    

    In the example above, only changes made to Customer1 will be persisted. Customer2 changes will still be in memory only, and you would have to call Flush or Flush(Customer2) to persist the changes. This gives you finer control over what should be persisted and helps you increase performance of your code.

    You must be careful, though, about associated objects. When you call Flush without specifying an object you are safe that all changes in the manager are persisted. You flushing a single object, associated objects might be flushed or not, depending on how the cascade options are set for that Association (or Many-Valued Association). If the association includes the TCascadeType.Flush, then it will also be flushed.

    Merging/Replicating Objects

    When you use Update method in a TObjectManager object, there should be no managed object with same Id in the object manager, otherwise an exception is raised. You can avoid such exception using the Merge or Replicate methods. These methods behave almost exactly the same, and will take a transient instance and merge it into the persistent instance. In other words, all the content of the transient object will be copied to the persistent object. Note that the transient object will continue to be transient.

    If there is no persistent object in the object manager with the same id, the object manager will load an object from the database with the same id of the transient object being merged.

    If the object has an id and no object is found in the database with that id, the behavior depends on the method called (and that is the only difference between Merge and Replicate methods):

    • if Merge method was called, an exception will be raised;

    • if Replicate method was called, a new object with the specified id will be saved (inserted).

    Customer2 := TCustomer.Create;
    Customer2.Id := Customer2Id;
    Customer2.Name := 'Mary';
    Customer2.Sex := tsFemale;
    MergedCustomer := Manager2.Merge<TCustomer>(Customer2);
    Manager2.Flush;
    

    In the example above, a TCustomer object was created and assigned an existing id. When calling Merge method, all data in Customer2 will be copied to the persistent object with same id in Manager2. If no persistent object exists in memory, it will be loaded from the database. Customer2 variable will still reference a transient object. The result value of Merge/Replicate method is a reference to the persistent object in the object manager.

    If the transient object passed to Merge/Replicate has no id, then a Save operation takes place. Merge/Replicate will create a new internal instance of object, copy all the contents from the passed object to the internal one, and Save (insert) the newly created object. Again, the object returned by Merge/Replicate is different from the one passed. Take a look at the following example:

    NewCustomer := TCustomer.Create;
    NewCustomer.Name := 'John';
    MergedCustomer := Manager2.Replicate<TCustomer>(NewCustomer);
    // MergedCustomer <> NewCustomer! NewCustomer must be destroyed
    

    In the example above, NewCustomer doesn't have an id. In this case, Merge/Replicate will create a new customer in database, and return the newly created object. MergedCustomer points to a different instance than NewCustomer. MergedCustomer is the persistent one that is tracked by the object manager (and will be destroyed by it when manager is destroyed). NewCustomer continues to be a transient instance and must be manually destroyed.

    Note that Merge/Replicate does nothing in the database in update operations - it just updates the persistent object in memory. To effectively update the object in the database you should then call Flush method. The only exception is the one described above when the object has no id, or when Replicate saves a new object with existing id. In those cases, a Save (insert) operation is performed immediately in the database.

    The cascades defined in Association and ManyValuedAssociation attributes in your class are applied here. Any associated object or collection item that has TCascadeType.Merge defined will also be merged/replicated into the manager and the reference will be changed. For example, if Customer has a Country property pointing to a transient TCountry object, The TCountry object will me merged, a new instance will be returned from the merging process, and Customer.Country property will be changed to reference the new instanced returned by the merging process.

    Removing Objects

    You can remove an object from the database using Remove method from a TObjectManager object. Just pass the object that you want to remove. The object must be attached to the object manager.

    Customer1 := Manager1.Find<TCustomer>(CustomerId);
    Manager1.Remove(Customer1);
    

    The cascades defined in Association and ManyValuedAssociation attributes in your class are applied here. Any associated object or collection item with delete cascade will also be removed from database.

    The object passed to Remove method will eventually be destroyed. If TObjectManager.DeferDestruction property is false (default), the object will be destroyed immediately. If it's true, object will be destroyed when the manager is destroyed.

    Finding Objects

    You can quickly find (load) objects using Find method of TObjectManager. You just need to pass the Id of the object, and object manager will retrieve the instance of the object loaded in memory. If the object is not attached to the object manager (not in memory), then it tries to load the object from database. If there is no object (record) in the database with that Id, it returns nil.

    Customer1 := Manager1.Find<TCustomer>(CustomerId);
    // Customer1 has an instance to the loaded customer object.
    

    The associations will be loaded depending on how the fetch mode was defined in Association attribute. They will be loaded on the fly or on demand, depending if they are set as lazy-loaded associations or not.

    If you want to retrieve several objects of a class from the database using some criteria (filtering, ordering), just use Find without parameter, it will return a Criteria object which you can use to add filters, ordering and later retrieve the results:

    var
      Customers: TList<TCustomer>;
    begin
      Customers := Manager1.Find<TCustomer>.List;
      // Take just the first 10 customers ordered by name
      Customers := Manager1.Find<TCustomer>.Take(10).OrderBy('Name').List;
    

    Aurelius is very powerful on querying capabilities. There is a full chapter explaining how to perform Aurelius queries.

    Refreshing Objects

    You can refresh an object using Refresh method from a TObjectManager object. Just pass the object that you want to refresh. The object must be attached to the object manager.

    Manager1.Refresh(Customer1);
    

    Refresh method performs operates almost the same way as Find method. The main difference is that Find method only create new instances that don't exist in the manager and if the instance already exists, it's left untouched. Refresh method, instead, will perform the SELECT statement in the database no matter what, and if the instances already exist in manager, it will update its properties and associations with data retrieved from the database, discarding the existing values in memory, if different.

    Note existing transient associations will NOT be destroyed. For example, consider the following code:

    Customer1 := Manager.Find<TCustomer>(1);
    NewCountry := TCountry.Create;
    Customer1.Country := NewCountry;
    Manager.Refresh(Customer1);
    

    In the code above a TCustomer instance is loaded from the database, and its Country property is updated to point to a transient TCountry reference. When Refresh method is called, Customer1 properties will be reloaded from the database, and thus Country property will point again to the original TCountry instance in the manager (or nil if there is no country associated with he customer). However, the instance referenced by NewCountry will not be destroyed. It's up to you to destroy the transient instances unreferenced by Refresh method.

    The cascades defined in Association and ManyValuedAssociation attributes in your class are applied here. Any associated object or collection item with Refresh cascade will also have its properties refreshed.

    Evicting Objects

    In some situations you want to remove (dettach) an object from the TObjectManager, but without deleting it from database (so you can't use Remove method) and without destroying the instance.

    To do that, you can use Evict method. Just pass the object that you want to evict. If the object is not attached to the manager, no operation is performed.

    Manager1.Evict(Customer1);
    

    The cascades defined in Association and ManyValuedAssociation attributes in your class are applied here. Any associated object or collection item with cascade option including TCascadeType.Evict will also be evicted (dettached) from the manager.

    Note that since the object is not in the manager anymore, you must be sure to destroy it (unless of course you attach it to another manager using for example Update method). Also pay attention to associated objects. If TCascadeType.Evict is defined for associated objects, they will also be evicted and must be destroyed as well.

    Transaction Usage

    You can use transactions when manipulating objects, so that you make sure all operations under the transaction are performed successfully (commit) or anything is reverted (rollback). Usage is simple and is done pretty much the same way you would do when accessing a database in a traditional way.

    The transactions are started under the IDBConnection interface context. You can start a transaction using IDBConnection.BeginTransaction method, which will return a IDBTransaction interface. The IDBTransaction in turn has only two methods: Commit and Rollback.

    uses {...}, Aurelius.Drivers.Interfaces;
    
    var 
      Transaction: IDBTransaction;
    begin
      Transaction := Manager.Connection.BeginTransaction;
      try
        { Perform manager operations }
        Transaction.Commit;
      except
        Transaction.Rollback;
        raise;
      end;
    end;
    

    Transactions in Aurelius can be nested. This means that if a transaction was already started in IDBConnection but not commited or rolled back yet, creating a new transaction and commiting or rolling it back has no effect. For example:

    OuterTransaction := Manager.Connection.BeginTransaction;
    InnerTransaction := Manager.Connection.BeginTransaction;
    InnerTransaction.Commit; // This has NO effect, the same for rollback.
    OuterTransaction.Commit; // Commit (or Rollback) is effectively performed here
    

    Concurrency Control

    When working with multiple users/clients, it might be possible that two or more users try to change the same entity (records in database). TMS Aurelius provides some mechanisms to avoid problems in those situations.

    Changed fields

    When updating objects, Aurelius detects which property have changed since the entity was loaded from the database in the manager, and it only updates those columns in the database. For example, suppose two users load the same TCustomer (with same id) from the database at the same time:

    // User1
    User1Customer := Manager1.Find<TCustomer>(1);
    // User2
    User2Customer := Manager2.Find<TCustomer>(1);
    

    Now first user changes customer's city and update, and second user changes customer's document and update:

    // User1
    User1Customer.City := 'New City';
    Manager1.Flush;
    // User2
    User2Customer.Document := '012345';
    Manager2.Flush;
    

    Here are the SQL executed by Aurelius for each user (SQL were simplified for better understanding, the actual SQL uses parameters):

    -- User1:
    Update Customer 
    Set City = 'New City' 
    Where Id = 1
     
    -- User2:
    Update Customer 
    Set Document = '012345' 
    Where Id = 1
    

    Even if TCustomer class has lots of customer, and some properties might be outdated in memory, it doesn't cause any trouble or data loss here, because only changed data will be commited to the database. In the end, the TCustomer object in database will have both the new city and new document correct.

    This is a basic mechanism that solves concurrency problems in many cases. If it's not enough, you can use entity versioning.

    Entity Versioning

    It might be possible that two users change the exactly same property, in this case, one of the users will "lose" their changes, because it will be overwritten by the other user. Or some other types of operations are performed where all fields are updated (when entity is put in manager without being loaded from database for example, so the manager can't tell which properties were changed).

    Or maybe you just need to be sure that the object being updated needs to hold the very latest data. A typical case is where you are updating account balance or inventory records, so you increment/decrement values and need to ensure that no other user changed that data since you loaded.

    In this case, you can use entity versioning. To accomplish this, you just need to create an extra integer property in the class, map it (so it's persisted in database) and add the [Version] attribute to it:

    [Entity, Automapping]
    TCustomer = class
    private
      FId: Integer;
      FName: String; 
      {...}
      [Version]
      FVersion: Integer;
      {...}
    end;
    

    And that's it. Once you do this, Aurelius will make sure that if you update (or delete) an entity, data it holds is the very latest one. If it's not, because for example another user changed the database record in the meanwhile, an exception will be raised and then you can decide what to do (refresh the object for example).

    Let's take a look at how it works. First, two users load the same object at the same time:

    // User1
    User1Customer := Manager1.Find<TCustomer>(1);
    // User1Customer.Version is 1 
    
    // User2
    User2Customer := Manager2.Find<TCustomer>(1);
    // User2Customer.Version is 1 
    

    Then User1 updates customer:

    User1Customer.City := 'New City';
    User1Customer.Flush;
    // User1Customer.Version becomes 2 (also in database)
    

    This is the SQL executed by Aurelius:

    Update Customer 
    Set City = 'New City', Version = 2 
    Where Id = 1 and Version = 1
    

    Record is changed successfully because the current version in database is 1, so the actual record is updated.

    Now, if User2 tries to update the old customer:

    // User2Customer.Version is still 1!
    User2Customer.City := 'Another city';
    User2Customer.Flush;
    

    Aurelius tries to execute the same SQL:

    Update Customer 
    Set City = 'Another City', Version = 2 
    Where Id = 1 and Version = 1
    

    However this will fail, because the version in the database is not 1 anymore, but 2. Aurelius will detect that no records were affected, and will raise an EVersionedConcurrencyControl exception.

    Cached Updates

    When persisting objects by calling object manager methods like Save, Flush or Remove, the manager will immediately perform SQL statements to reflect such changes in the database. For example, calling this:

    Manager.Remove(Customer);
    

    Will immediately execute the following SQL statement in the database:

    DELETE FROM CUSTOMERS WHERE ID = :id
    

    With cached updates feature, you have the option to defer the execution of all SQL statements to a later time, when you call ApplyUpdates method. This is enabled by setting CachedUpdates to true. Take the following code as an example:

    Manager.CachedUpdates := True;
    Manager.Save(Customer);
    Invoice.Status := isPaid;
    Manager.Flush(Invoice);
    WrongCity := Manager.Find<TCity>(5);
    Manager.Remove(City);
    Manager.ApplyUpdates;
    

    This code should perform the following SQL statements:

    1. INSERT (to save the customer - see "exceptions" below);
    2. UPDATE (to modify the status field of the invoice);
    3. DELETE (to delete the city).

    However, all the statements will be executed one after another, when ApplyUpdates is called.

    Exceptions

    There are exceptions for this rule. When the id value of an entity is generated by the database during an INSERT statement, then the INSERT SQL statement will be executed immediately. So for example, the statement below:

    Manager.Save(Customer);
    

    Might be executed immediately, even when CachedUpdates property is true, if Customer is set to be of type IdentityOrSequence, and the underlying database uses the identity mode (the id is generated automatically during the INSERT statement).

    Note that this doesn't apply if the database uses SEQUENCE. In other words, even if the id is of type IdentityOrSequence, if the id is generated by reading the SEQUENCE value, then the SQL statement used to retrieve the SEQUENCE value will be executed, but the INSERT statement will be deferred until ApplyUpdates is called.

    Cached actions

    You can read the CachedCount property to know how many actions are pending and will be applied in the next call to CachedUpdates.

    TotalPendingActions := Manager.CachedCount;
    

    Batch (Bulk) Updates

    Aurelius allows you to modify data in batches. For example, suppose you want to update one hundred customer records, modifying their phone number. With batch updates, a single SQL statement will be executed to modify all records at once. This improves performance significantly in such operations.

    Batch updates works together with cached updates. You need to use cached updates mechanism, then just use TObjectManager.BatchSize property to tell Aurelius the maximum number of records that can be updated using a single SQL statement.

    Consider the following code:

    // Retrieve some customers from database
    CustomerA := Manager.Find<TCustomer>(1);
    CustomerB := Manager.Find<TCustomer>(2);
    CustomerC := Manager.Find<TCustomer>(3);
    
    // Set batch size and enable cache updates
    Manager.BatchSize := 100;
    Manager.CachedUpdates := True;
    
    // Modify customer A, B and C
    CustomerA.City := 'New York';
    Manager.Flush(CustomerA);
    
    CustomerB.City := 'Berlin';
    Manager.Flush(CustomerB);
    
    CustomerC.City := 'London';
    Manager.Flush(CustomerC);
    
    // Now apply updates
    Manager.ApplyUpdates;
    

    Aurelius will execute a single SQL statement to modify the three customers. The SQL statement will be something like this:

    UPDATE Customers SET Name = :p1 WHERE Id = :p2
    

    And the parameter values for all records ("New York" and 1, "Berlin" and 2, "London" and 3) will be sent at once together with the SQL statement.

    Batch algorithm

    Aurelius will perform the batch automatically. You can perform any actions you want. All actions will be cached, and if the actions build SQL statement that are the same, they will be grouped together in a batch, up until the size specified by BatchSize.

    Thus, the order of actions is important. For example, if you perform:

    • Insert CustomerA
    • Modify CustomerA City
    • Insert CustomerB
    • Modify CustomerB City

    No batches will be created and a SQL statement will be executed for each operation. However, if you perform the actions this way:

    • Insert CustomerA
    • Insert CustomerB
    • Modify CustomerA City
    • Modify CustomerB City

    Then two batches of size 2 will be created. A single SQL statement will be executed to insert the two customers, and another SQL statement will be executed to modify the two customers.

    Also note that the batch will be performed if all SQL statements are the same. For example, if you modify Customer A city, and then modify Customer B name, no batch will be created. Even though both are update actions, the first will result in a SQL statement that modifies city field, and the other will result in a SQL statement that modifies name field.

    Driver-dependent behavior

    The way the statement is executed depends on the underlying driver and RDBMS being used. Some drivers support the batch mechanism where all parameters are sent at once. Other drivers do not support it, in this case Aurelius will simulate such batch by executing several SQL statements using a pre-prepared one. Still it will be faster than executing a single statement for every record.

    The current drivers that support real batch updates (array DML) are:

    • Native Aurelius connectivity;
    • FireDAC;
    • UniDAC (except when connecting to Firedac, Interbase and NexusDB).

    For all other drivers, the batch will be simulated. Note that you don't need to modify anything in your code, regardless of the driver and the database you are using. Aurelius will work transparently in all cases, and use the best mechanism available to perform the batch updates.

    In This Article
    • Object Manager
      • Creating a new object manager
      • Save method
      • Update method
      • SaveOrUpdate method
      • Flush method
        • Flush method for single entity
      • Merge method
      • Replicate method
      • Find method
      • Remove method
      • Find<T> method
      • CreateCriteria<T> method
      • Evict method
      • IsAttached method
      • FindCached<T> method
      • IsCached<T> method
      • HasChanges method
      • OwnsObjects property
      • ProxyLoad and BlobLoad methods
      • UseTransactions property
      • DeferDestruction property
    • TAureliusManager Component
      • Key properties
      • Usage
      • TObjectManager memory management
    • Memory Management
      • Concept of object state
      • Object lists
      • Unique instances
      • Manually adding ownership
      • Examples
      • Using unmanaged objects
    • Saving Objects
    • Updating Objects - Flush
      • Merging
      • Flushing a single object
    • Merging/Replicating Objects
    • Removing Objects
    • Finding Objects
    • Refreshing Objects
    • Evicting Objects
    • Transaction Usage
    • Concurrency Control
      • Changed fields
      • Entity Versioning
    • Cached Updates
      • Exceptions
      • Cached actions
    • Batch (Bulk) Updates
      • Batch algorithm
      • Driver-dependent behavior
    Back to top TMS Aurelius v5.21
    © 2002 - 2025 tmssoftware.com