Generated code - Using the context, Adapter
Preface
Selfservicing and Adapter both support so called
uniquing contexts. These contexts, implemented in the
Context class, available in the
ORM Support classes, represent a semantic context in your program. Within such a semantic context, the representing Context object,
assures that an entity loaded by the framework is loaded in just one object. This is required for for example refetching trees of objects
using prefetch paths, or for usage where more than one object with the same data is problematic. This section discusses the Context object more in
detail. It is not necessary to use a Context object in your application, for example in state-less environments like ASP.NET, it's not of much use. Though
it can sometimes be required to have just one entity class instance with a given entity's data in a given semantic context, for example in an edit form in
a windows forms application.
The Context class
Context objects have to be created by the developer and live as long as the developer wants and keep objects in their cache as long as the Context
objects live. A developer can create multiple Context objects to create different semantic contects in which entity objects are unique.
This can help when the developer doesn't want two screens with the same object listed to assure that editing the entity on one screen doesn't
automatically alter the other instance as well (because the user can click Cancel for example).
The Context class works with instances of entity classes. This means that when you pass an entity instance using remoting or webservices to a server which
then returns it back after processing, you won't reference the same instance of the entity you sent to the server. This is because of the serialization and
deserialization which takes place during remoting. The Context class identifies entities using the PK field values. This means that new entities
aren't directly added to the Context's internal object cache, as for example Identity columns won't have a value for the PK field until they're saved and the
transaction has been committed. When an entity is saved and the transaction is committed (if any), the entity is added to the Context's cache if the
entity is new. A non-new entity object which is added to a Context object, is directly added to the Context's internal object cache, if the entity
object hasn't been added to another Context already.
Entity objects can be added to the Context at any time, as well as Entity collection objects. When an entity object is added to a Context, its internally
referenced related entity objects and entity collection objects are added to the same Context object as well. When an entity collection is added to a
Context object, all its entity objects will be added to the Context and every entity object added to the entity collection after that point will be
added to the same Context object the collection is added to as well, which makes it easy to work with a Context object, as it is mostly transparent.
The Context objects don't act as a cache which is used to prevent database activity. Every query is still executed on the database. If an entity is already
loaded in the used Context object, the entity data is not added to a new entity object, but the entity object already loaded is updated. If the
already loaded object is dirty, the data isn't updated and the loaded entity data is simply skipped, and the already loaded entity object is
returned as is. This is done in the Get routine of the Context object. The Context has a flag to disallow this particular action:
SetExistingEntityFieldsInGet. See the LLBLGen Pro reference manual for details on this flag.
You can of course use the Context as an object cache for
single object fetches, though keep in mind that a Context object is simply acting as a unique instance supplier, it doesn't fetch objects from the database,
so if you request an entity instance from a Context object using
Get and the Context object can't find it in its cache, you have to test if the
returned object is indeed a fetched entity object or a new entity object. The Context objects don't fetch data for themselves to keep the fetch logic
placed at just a few known places, to avoid fragmentation of this logic which could blur the overview where the code actually performs fetch activity.
Adding an entity object which is already present in the Context is a no-op, as well as when an entity object is already part of another Context
object. After an entity is added to a Context object, and a 1:1/m:1 reference is set to an entity class instance, the related entity
is not added to the context automatically, this has to be done manually by the developer, though when an entity is added to a collection which is
added to a context, the entity is added to that context as well. The Context object an entity is added to is returned by the entity's
ActiveContext property.
When an entity is deleted, the status of the entity is set to Deleted by the delete routines. The Context.Get method will remove an
entity from the store if the entity is deleted and not participating in a transaction. Till then, the entity is kept in the Context's object cache.
Using the Context class
The Context class should be seen as a convenience providing class for uniquing within a semantic context. It shouldn't be confused with a UnitOfWork +
Object Fetch object, because it leaves that functionality to other objects and methods.
Retrieving instances from a Context
A Context object supplies a
Get method which offers different ways to retrieve the already loaded instance for a given entity. As a Context
object uses the value(s) of the PK field(s), you can use this to retrieve the unique instance. Below are the different ways illustrated: it will try
to retrieve the instance which already contains the entity data for the customer with CustomerID "CHOPS".
// C#
// using a factory
CustomerEntity c = (CustomerEntity)myContext.Get(new CustomerEntityFactory(), "CHOPS");
// using a fetched entity
CustomerEntity c = new CustomerEntity("CHOPS");
adapter.FetchEntity(c);
c = (CustomerEntity)myContext.Get(c);
' VB.NET
' using a factory
Dim c As CustomerEntity = CType(myContext.Get(new CustomerEntityFactory(), "CHOPS"), CustomerEntity)
' using a fetched entity
Dim c As New CustomerEntity("CHOPS")
adapter.FetchEntity(c)
c = CType(myContext.Get(c), CustomerEntity)
Single entity fetches
Creating an entity object using a normal constructor, creates a new instance of the entity class. Fetching data into that object will then
be loaded into a new instance. To be able to load the entity's data into a new entity class instance if the Context used doesn't have an instance with that
data present and just return the already loaded instance if the Context
does have an instance of the entity class with the entity data, use
the construct mentioned above:
// C#
CustomerEntity c = new CustomerEntity("CHOPS");
adapter.FetchEntity(c);
c = (CustomerEntity)myContext.Get(c);
' VB.NET
Dim c As New CustomerEntity("CHOPS")
adapter.FetchEntity(c)
c = CType(myContext.Get(c), CustomerEntity)
This will fetch customer "CHOPS" from the database but the context will check if the entity is already loaded in this context. If so,
it will return that instance, not the newly fetched instance. If the entity object isn't known by the Context, it is added to the Context and the
Context returns the instance passed to the Get() method call.
Entities can also be added manually first and then fetched:
// C#
CustomerEntity c = new CustomerEntity("CHOPS");
myContext.Add(c);
adapter.FetchEntity(c);
' VB.NET
Dim c A New CustomerEntity()
myContext.Add(c)
adapter.FetchEntity(c)
Or, using a unique constraint:
// C#
CustomerEntity c = new CustomerEntity();
c.CompanyName = "Foo Inc.";
myContext.Add(c);
adapter.FetchEntityUsingUniqueConstraint(c, c.ConstructFilterForUCCompanyName());
' VB.NET
Dim c A New CustomerEntity()
c.CompanyName = "Foo Inc."
myContext.Add(c)
adapter.FetchEntityUsingUniqueConstraint(c, c.ConstructFilterForUCCompanyName())
Though it has to be understood that the actual instance 'c' is only unique if the particular entity hasn't been loaded yet. This is
due to the c = new CustomerEntity() line. Fetching using unique constraints is a bit problematic in this case. To avoid that you can do:
// C#
CustomerEntity c = new CustomerEntity();
c.CompanyName = "Foo Inc.";
myContext.Add(c);
adapter.FetchEntityUsingUniqueConstraint(c, c.ConstructFilterForUCCompanyName());
c = (CustomerEntity)myContext.Get(c); // get unique version. No db activity.
' VB.NET
Dim c As New CustomerEntity()
c.CompanyName = "Foo Inc."
myContext.Add(c)
adapter.FetchEntityUsingUniqueConstraint(c, c.ConstructFilterForUCCompanyName())
c = CType(myContext.Get(c), CustomerEntity) ' get unique version. No db activity.
Prefetch Path fetches
Fetching an entity or set of entities and using a prefetch path can fully utilize a Context by one of the overloads of FetchEntityCollection, FetchEntity,
FetchNewEntity etc., which accepts a Context object.
If you're fetching a graph and you want to have for every already loaded entity in a particular Context the
instance in which the entity is already loaded, you can pass in the context in which these entity objects already are added to. The fetch
logic will then build the object graph using the instances from the passed in Context, otherwise it will read the entity data in newly created
entity objects. Below is an example which uses a Context object to fetch additional nodes of an object graph after parts of the graph were
fetched earlier. The example shows this in one routine, but these two activities could take place in separate routines of course.
// C#
DataAccessAdapter adapter = new DataAccessAdapter();
try
{
Context myContext = new Context();
CustomerEntity customer = new CustomerEntity("BLONP");
IPrefetchPath2 prefetchPath = new PrefetchPath2((int)EntityType.CustomerEntity);
prefetchPath.Add(CustomerEntity.PrefetchPathOrders);
adapter.FetchEntity(customer, prefetchPath, myContext);
// ...
// customer and its orders are now loaded. Say we want to add later on the order details
// of each order of this customer to this graph. We can do that with the following code.
// redefine the prefetch path, as if we're somewhere else in the application
prefetchPath = new PrefetchPath2((int)EntityType.CustomerEntity);
prefetchPath.Add(CustomerEntity.PrefetchPathOrders).SubPath.Add(OrderEntity.PrefetchPathOrderDetails);
// fetch the customer again. As it is already added to a Context (myContext), the fetch logic
// will use that Context object. This fetch action will fetch all data again, but into the same
// objects and will for each Order entity loaded load the Order Detail entities as well.
adapter.FetchEntity(customer, prefetchPath);
}
finally
{
adapter.Dispose();
}
' VB.NET
Dim adapter As New DataAccessAdapter()
Try
Dim myContext As New Context()
Dim customer As New CustomerEntity("BLONP")
Dim prefetchPath As New PrefetchPath2(CInt(EntityType.CustomerEntity))
prefetchPath.Add(CustomerEntity.PrefetchPathOrders)
adapter.FetchEntity(customer, prefetchPath, myContext)
' ...
' customer and its orders are now loaded. Say we want to add later on the order details
' of each order of this customer to this graph. We can do that with the following code.
' redefine the prefetch path, as if we're somewhere else in the application
prefetchPath = new PrefetchPath2(CInt(EntityType.CustomerEntity))
prefetchPath.Add(CustomerEntity.PrefetchPathOrders).SubPath.Add(OrderEntity.PrefetchPathOrderDetails)
' fetch the customer again. As it is already added to a Context (myContext), the fetch logic
' will use that Context object. This fetch action will fetch all data again, but into the same
' objects and will for each Order entity loaded load the Order Detail entities as well.
adapter.FetchEntity(customer, prefetchPath)
Finally
adapter.Dispose()
End Try
Note: |
When fetching an entity collection, you've to add the collection to fetch to the context object and then call the FetchEntityCollection method |
Entity Save calls
When an entity is saved, the DataAccessAdapter class will signal the Context the entity is in (if any), that the entity in question
is saved and if the entity is new, it should be moved to the normal object cache inside the Context, if the entity is not in a transaction.
If the entity is in a transaction, this activity is performed after the transaction is committed, in the entity's base class transaction commit routine. This
is done to prevent that the Context object will move a new entity to the object cache even though the transaction rolled back.
If a recursive save saves an entity which is not yet in the active context, the entity is added to the active context.
Multi-entity activity
Actions on entity collections work inside the active context if the collection is first added to a context. All persistence
logic will re-use objects from the Context object if the entity collection used is added to a context.
SaveEntityCollection() will first add any entities saved to the context the collection is in, if the entity isn't already in the context.
Remarks
- PK values shouldn't be changed. The context relies on non-changing PK values.
- A Context shouldn't be used as a cache, nor should it kept alive for a long time, just long enough for the
semantic context to use unique objects in.
- Deleted entities which are deleted in the database directly are not picked up by the Context. This is something the developer has to take into
account when deleting entities directly.
- As the Context class doesn't use any locking mechanism, the Context object isn't thread-safe and should be used for single-thread semantic contexts