Generated code - Using the entity classes, Adapter
Preface
Using the Adapter template group which ships with LLBLGen Pro, you'll notice that there will be two VS.NET projects generated.
This section describes code referencing both projects as it needs to interact with the database. The code used in the Adapter section of the documentation uses
the General preset, which results in one class per entity. If you want two classes per entity, you've to use the TwoClasses preset, which would result in
two classes per entity, one being named My
entityNameEntity, the one you'd use in your code like the code in this section.
All entity classes derive from a central, generated base class called
CommonEntityBase. This class is the base class for all generated entity classes and it derives from the class
EntityBase2, which is located in the ORMSupportClasses assembly. The CommonEntityBase class is usable to add code (via a partial class or using the user code regions) to all generated entities without having to generate / add that code to all entity classes separately.
Instantiating an existing entity
As described in the concepts in the Designer documentation, an entity is a semantic name for
a group of existing data. An entity has a definition, the entity definition, which is formulated in a database table/view and, when you
added that entity definition to your project, also has that definition in code, namely in the
EntityNameEntityBase.cs/vb class. To load the
entity's data from the persistent storage, we use the generated class related to this entity's definition, create
an instance of that class and order it to load the data of the particular entity via a DataAccessAdapter object.
As an example we're loading the entity identified with the customerID "CHOPS" into an object.
Using the primary key value
One way to instantiate the entity in an object is by passing all primary key values to the constructor of the entity class to use:
// [C#]
DataAccessAdapter adapter = new DataAccessAdapter();
CustomerEntity customer = new CustomerEntity("CHOPS");
adapter.FetchEntity(customer);
' [VB.NET]
Dim adapter As New DataAccessAdapter()
Dim customer As New CustomerEntity("CHOPS")
adapter.FetchEntity(customer)
This will load the entity with the primary key value of "CHOPS" into the object named
customer, directly from the persistent
storage. LLBLGen Pro doesn't use an in-memory cache of objects, to prevent concurrency issues among multiple threads in multiple
appdomains (which is the case when you run a client on two or more machines, when you have a webfarm or when your business logic is stored on
multiple machines).
Using a related entity
Another way to instantiate this same entity is via a related entity. Adapter however doesn't support automatic data loading when you traverse a relation,
all data has to be fetched up-front. A related entity however offers a way to formulate the exact filters to fetch a specific entity very easily.
Let's load the order with ID 10254, which is an order of customer "CHOPS", and via that order, load an instance of the entity "CHOPS". The example
uses the KeepConnectionOpen feature by passing true to the constructor of the DataAccessAdapter object. The example explicitly closes the connection
after the DataAccessAdapter usage is finished.
// [C#]
DataAccessAdapter adapter = new DataAccessAdapter(true);
OrderEntity order = new OrderEntity(10254);
adapter.FetchEntity(order);
order.Customer = (CustomerEntity)adapter.FetchNewEntity(new CustomerEntityFactory(),
order.GetRelationInfoCustomer());
adapter.CloseConnection();
' [VB.NET]
Dim order As New OrderEntity(10254)
Dim adapter As New DataAccessAdapter(True)
adapter.FetchEntity(order)
order.Customer = CType(adapter.FetchNewEntity(New CustomerEntityFactory(), _
order.GetRelationInfoCustomer()), CustomerEntity)
adapter.CloseConnection()
By setting order.Customer to an instance of CustomerEntity, the logic automatically sets the CustomerID field of order to the CustomerID of
the specified CustomerEntity instance. Also the order object will be added to the CustomerEntity instance 'Orders' collection. This means that the
following is true after
order.Customer = (CustomerEntity)adapter.FetchNewEntity(CustomerEntityFactory(), order.GetRelationInfoCustomer()) :
order.CustomerID is equal to order.Customer.CustomerID
order.Customer.Orders.Contains(order) is true
The logic keeps the two in sync as well. Consider the following situation: a new EmployeeEntity instance employee, which has an autonumber primary key field,
and a new OrderEntity instance order. When the following is done: order.Employee = employee;, and the order is saved (or the employee), the field
order.EmployeeID is automatically set to the new key field of the employee object after employee is saved.
If Customer is in an inheritance hierarchy, the fetch is polymorphic. This means that if the order entity, in this case order 10254, has a reference to
a derived type of Customer, for example GoldCustomer, the entity returned will be of type GoldCustomer. See also Polymorphic fetches below.
Using a unique constraint's value
Entities can have other unique identifying attributes and are defined in the database as unique constraints. In addition to the primary key these unique values
can be used to load an entity. The customer entity has a unique constraint defined on its field 'CompanyName', so we can use that field to load the same entity
that the CHOPS example loaded above.
A unique constraint which has the same types of fields as the primary key would result in the same method signature and would not be compileable. Fetching the entity
using a unique constraint is done via these steps: first create an empty entity object, set the fields which form the unique constraint to the lookup value, then
fetch the entity data using a special method call of the DataAccessAdapter.
Because an entity can have more than one unique constraint, you have to specify which unique constraint to use, or better: specify a filter for the unique
constraint columns. Entities with unique constraints have methods to construct these filters automatically as shown in the following example. The entity
Customer has a unique constraint with one field, CompanyName:
// [C#]
DataAccessAdapter adapter = new DataAccessAdapter();
CustomerEntity customer = new CustomerEntity();
customer.CompanyName = "Chop-suey Chinese";
adapter.FetchEntityUsingUniqueConstraint(customer, customer.ConstructFilterForUCCompanyName());
' [VB.NET]
Dim adapter As New DataAccessAdapter()
Dim customer As New CustomerEntity()
customer.CompanyName = "Chop-suey Chinese"
adapter.FetchEntityUsingUniqueConstraint(customer, customer.ConstructFilterForUCCompanyName())
Using a prefetch path
An easy way to instantiate an entity can be by using a Prefetch Path, to read related entities together with the entity or entities to fetch. See
for more information about Prefetch Paths and how to use them:
Prefetch Paths.
Using a collection class
The last way to instantiate an entity is by creating a collection class with one or more entities of the same entity definition (entity type,
like Customer) using the EntityCollection classes or via a related entity which has a 1:n relation with the entity to instantiate.
This method is described in detail in the section about
collection classes.
You can also see
Tutorials and Examples: How Do I? - Read all entities into a collection.
Using a Context object
If you want to get a reference to an entity object already in memory, you can use a
Context object, if that
object was added to that particular Context object. The example below retrieves a reference to the customer object with PK "CHOPS", if that entity
was previously loaded into an entity object which was added to that Context object. If the entity object isn't in the Context object, a new entity
object is returned. An example usage is shown below.
// C#
CustomerEntity customer = (CustomerEntity)myContext.Get(new CustomerEntityFactory(), "CHOPS");
if(customer.IsNew)
{
// not found in context, fetch from database (assumes 'adapter' is a DataAccessAdapter instance)
adapter.FetchEntity(customer);
}
' VB.NET
Dim customer As CustomerEntity = CType(myContext.Get(New CustomerEntityFactory(), "CHOPS"), CustomerEntity)
If customer.IsNew Then
' not found in context, fetch from database (assumes 'adapter' is a DataAccessAdapter instance)
adapter.FetchEntity(customer)
End If
Polymorphic fetches
Already mentioned early in this section is the phenomenon called 'Polymorphic fetches'. Imagine the following entity setup:
BoardMember entity has a relation (m:1) with CompanyCar. CompanyCar is the root of a TargetPerEntityHierarchy inheritance hierarchy and has two subtypes:
FamilyCar and SportsCar. Because BoardMember has the relation with CompanyCar, a field called 'CompanyCar' is created in the BoardMember entity which is
mapped onto the m:1 relation BoardMember - CompanyCar.
In the database, several BoardMember instances have been stored, as well as several different CompanyCar instances, of type FamilyCar or SportsCar.
Using DataAccessAdapter.FetchNewEntity, you can load the related CompanyCar instance of a given BoardMember's instance by using the following code:
// C#
CompanyCarEntity car = adapter.FetchNewEntity(new CompanyCarEntityFactory(),
myBoardMember.GetRelationInfoCompanyCar());
' VB.NET
Dim car As CompanyCarEntity = adapter.FetchNewEntity(New CompanyCarEntityFactory(), _
myBoardMember.GetRelationInfoCompanyCar())
However, 'car' in the example above, can be of a different type. If for example the BoardMember instance in myBoardMember has a FamilyCar as company car
set, 'car' is of type FamilyCar. Because the fetch action can result in multiple types, the fetch is called
polymorphic. So, in our example, if 'car' is
of type FamilyCar, the following code would also be correct:
// C#
FamilyCarEntity car = (FamilyCarEntity)adapter.FetchNewEntity(new CompanyCarEntityFactory(),
myBoardMember.GetRelationInfoCompanyCar());
' VB.NET
Dim car As FamilyCarEntity = CType(adapter.FetchNewEntity(New CompanyCarEntityFactory(), _
myBoardMember.GetRelationInfoCompanyCar()), FamilyCarEntity)
Would this BoardMember instance have a SportsCar set as company car, this code would fail at runtime with a specified cast not valid exception.
DataAccessAdapter.FetchEntity and hierarchial entities
DataAccessAdapter.FetchEntity() is not polymorphic. This is by design as it fetches the entity data into the passed in entity object. As it's already
an instance, it would be impossible to change that instance' type to a derived type if the PK values identify an entity which is of a subtype of the
type of the passed in entity instance.
In our previous example about BoardMember and CompanyCar, BoardMember is a derived type of Manager which is a derived type of Employee. While this might not
be the best OO hierarchy thinkable, it's enough to illustrate the point: if FetchEntity is called by passing in an Employee instance, and the PK identifies
a BoardMember, only the Employee's fields are loaded, however if the entity is in a hierarchy of type TargetPerEntity, LLBLGen Pro will perform joins
with all subtypes from the supertype, to make sure a type is stored OK.
Note:
|
Be aware of the fact that polymorphic fetches of entities in a TargetPerEntity hierarchy
use JOINs between the root entity's target
and all subtype targets when the root type is specified for the fetch. This can have an inpact on performance.
|
Creating a new / modifying an existing entity
Loading an entity is nice, but it has to be created before it can be loaded. To create a new entity, simply instantiate an empty
entity object, in this case a new Customer:
// [C#]
CustomerEntity customer = new CustomerEntity();
' [VB.NET]
Dim customer As New CustomerEntity()
To create the entity in the persistent storage, two things have to be done: 1) the entity's data (which is new) has to be stored in the
new entity object and 2) the entity data has to be persisted / saved in the persistent storage. Let's add the customer Foo Inc. to the
database:
// [C#]
customer.CustomerID = "FOO";
customer.Address = "1, Bar drive";
customer.City = "Silicon Valey";
customer.CompanyName = "Foo Inc.";
customer.ContactName = "John Coder";
customer.ContactTitle = "Owner";
customer.Country = "USA";
customer.Fax = "(604)555-1233";
customer.Phone_Number = "(604)555-1234";
customer.PostalCode = "90211";
// save it. We require an adapter for this
DataAccessAdapter adapter = new DataAccessAdapter();
adapter.SaveEntity(customer, true);
' [VB.NET]
customer.CustomerID = "FOO"
customer.Address = "1, Bar drive"
customer.City = "Silicon Valey"
customer.CompanyName = "Foo Inc."
customer.ContactName = "John Coder"
customer.ContactTitle = "Owner"
customer.Country = "USA"
customer.Fax = "(604)555-1233"
customer.Phone_Number = "(604)555-1234"
customer.PostalCode = "90211"
' save it. We require an adapter for this
Dim adapter As New DataAccessAdapter()
adapter.SaveEntity(customer, True)
Region isn't filled in, which is fine, it can be NULL, and will therefore also end up as NULL in the database. This will save the data
directly to the persistent storage (database) and the entity is immediately available for other threads / appdomains targeting the same database, because
we've specified that it should be refetched right after the SaveEntity() action succeeds.
The entity object customer itself is marked 'out of sync'. This means that the entitys data has to be refetched from the database prior to reading from
one of the entities properties. SelfServicing will handle this automatically but with Adapter you must refetch manually using an adapter object.
In our example we specified true in the SaveEntity call, this automatically refetches the entity for us. If you do not require the saved entity for any
further processing, you don't need to refetch it and you can save yourself a roundtrip by simply omitting the 'true' in the SaveEntity() call.
The code is aware of sequences / identity columns and will automatically set
the value for an identity / sequence column after the entity is physically
saved inside SaveEntity(). The new value for sequenced columns is available
to you after SaveEntity(), even though you haven't specified that the entity
has to be refetched. This can be helpful if you want to refetch the entity
later.
Because the entity saved is new (customer.IsNew is true), SaveEntity() will use an INSERT query. After a successful save, the IsNew flag is set to false and the
State property of the Fields object of the saved entity is set to
EntityState.Fetched (if the entity is also refetched) or
EntityState.OutOfSync.
Note:
|
Fields which get their values from a trigger, from newid() or a default constraint calling a user defined function are not
considered sequenced fields and these values will not be read back, so you'll have to supply a value for these fields prior to saving the entity. This
isn't true for fields which are of type unique_identifier on SqlServer 2005 when the DQE is set in SqlServer 2005 compatibility mode and the
field has in the database a default value of NEWSEQUENTIALID(). See:
Generated code - Database specific features
|
Note:
|
If the entity is in a hierarchy of type TargetPerEntityHierarchy you don't have to set the discriminator value for the entity type,
this is done for you automatically: just create a new instance of the entity type you want to use, and the discriminator value is automatically set and
will be saved with the entity.
|
Modifying an entity
Modifying an entity's data is just as simple and can be done in multiple ways:
- Loading an existing entity in memory, alter one or more fields (not sequenced fields) and call a DataAccessAdapter object's SaveEntity() method
- Create a new entity, set the primary key values (used for filtering), set the IsNew to false, set one or more other fields' values
and call a DataAccessAdapter object's SaveEntity() method. This will not alter the PK fields.
- Via the DataAccessAdapter's UpdateEntitiesDirectly() method, specifying the primary key fields as the filter.
Option 1 is likely the most used one, since an entity might already be in memory. As with all the suggested options,
the DataAccessAdapter's SaveEntity() method will see that the entity isn't new, and will therefore use an UPDATE query to alter the entity's data in the persistent
storage. An UPDATE query will only update the
changed fields in an entity that is saved, which results in
efficient queries. If no fields are changed, no update is performed. If you've loaded an entity from the database into memory and you've changed one or more of its
primary key fields, these fields will be updated in the database as well (except sequenced columns). Changing PK fields is not recommended and changed PK fields are
not propagated to related entities fetched in memory.
An example for code using the first method: (it keeps the connection open for performance)
// [C#]
CustomerEntity customer = new CustomerEntity("CHOPS");
DataAccessAdapter adapter = new DataAccessAdapter(true);
adapter.FetchEntity(customer);
customer.Phone = "(605)555-4321";
adapter.SaveEntity(customer);
adapter.CloseConnection();
' [VB.NET]
Dim customer As New CustomerEntity("CHOPS")
Dim adapter As New DataAccessAdapter(True)
adapter.FetchEntity(customer)
customer.Phone = "(605)555-4321"
adapter.SaveEntity(customer)
adapter.CloseConnection()
This will first load the Customer entity "CHOPS" into memory, alter one field, Phone, and then save that single field back into the
persistent storage. Because the loading of "CHOPS" already set the primary key, we can just alter a field and call SaveEntity() . The Update query
will solely set the table field related to the entity field "Phone".
Reading an entity into memory first can be somewhat inefficient, since all we need to do is an update of an entity row in the database.
Option 2 is more efficient in that it just starts an update, without first reading the data from the database. The following code performs
the same update as the previous example code illustrating option 1. Because it doesn't need to read an entity first, it doesn't have to keep the
connection open. Even though the PK field is changed, it is not updated, because it is not previously fetched from the database.
// [C#]
CustomerEntity customer = new CustomerEntity();
customer.CustomerID="CHOPS";
customer.IsNew=false;
customer.Phone = "(605)555-4321";
DataAccessAdapter adapter = new DataAccessAdapter();
adapter.SaveEntity(customer);
' [VB.NET]
Dim customer As New CustomerEntity()
customer.CustomerID = "CHOPS"
customer.IsNew = False
customer.Phone = "(605)555-4321"
Dim adapter As New DataAccessAdapter()
adapter.SaveEntity(customer)
We have to set the primary key field, so the Update method will only update a single entity, the "CHOPS" entity.
Next, we have to mark the new, empty entity object as not being new, so SaveEntity() will use an UPDATE query, instead of an INSERT query.
This is done by setting the flag IsNew to false. After that comes the altering of a field, in this case "Phone", and the call of SaveEntity().
This will not load the entity back in memory. If you want that, specify 'true' with the SaveEntity() call. Doing updates this way can be very efficient and
you can use very complex update constructs when you apply an Expression to the field(s) to update. See for more information about Expression objects for
fields
Field expressions and aggregates.
Notes:
|
- This same mechanism will work for fast deletes.
- If you want to set an identity primary key column, you'll notice you can't do that because it is marked as read-only. Use the method
entityObject.Fields[fieldindex or fieldname].ForcedCurrentValueWrite(value). See the reference manual for details about this method
(EntityField2.ForcedCurrentValueWrite).
- Setting a field to the same value it already has will not set the field to a value (and will not mark the field as 'changed') unless the entity
is new.
- Recursive saves are performed by default. This means that the DataAccessAdapter SaveEntity() logic will check whether included entities also have to
be saved. In our examples above, this is not the case, but in your own code it can be. If you do not want this, you can specify 'false' for recursive
saves in an overload of SaveEntity() in which case only the specified entity will be saved.
- Each entity which is saved is validated prior to the save action. This validation can be a no-op, if no validation code has been added by the
developer, either through code added to the entity, or through a validator class.
See Validation per field or per entity for more information about LLBLGen Pro's validation functionality.
- (SQLServer specific) If the entity is saved into a table which is part of an indexed view, SqlServer requires that SET ARITHABORT ON is specified prior
to the actual save action. You can tell the DataAccessAdapter class to set that option, by calling the SetArithAbortFlag(bool) method. After each SQL statement
a SET ARITHABORT OFF statement will be executed if the ArithAbort flag is set to true. Setting this flag affects the whole application.
|
Option 3 is implemented directly in the DataAccessAdapter object.
The DataAccessAdapter allows you to manipulate an entity or group of entities directly in the database without first fetching them into memory. This can be much faster
than the conventional method described in option 1 or 2. Below we're setting all 'Discontinued' fields of all product entities to false if the CategoryId of the product
is equal to 3. UpdateEntitiesDirectly() (as well as its look-alike method DeleteEntitiesDirectly, which deletes entities directly from the persistent storage) returns
the number of entities affected by the call, or -1 if rowcounting is switched off inside the database system (SqlServer)
// [C#]
RelationPredicateBucket bucket = new RelationPredicateBucket();
bucket.PredicateExpression.Add(ProductFields.CategoryId == 3);
ProductEntity updateValuesProduct = new ProductEntity();
updateValuesProduct.Discontinued=true;
DataAccessAdapter adapter = new DataAccessAdapter();
int amountUpdated = adapter.UpdateEntitiesDirectly(updateValuesProduct, bucket);
' [VB.NET]
Dim bucket As New RelationPredicateBucket()
bucket.PredicateExpression.Add(New FieldCompareValuePredicate(ProductFields.CategoryId, Nothing, ComparisonOperator.GreaterEqual, 3))
Dim updateValuesProduct As New ProductEntity()
updateValuesProduct.Discontinued = True
Dim adapter As New DataAccessAdapter()
Dim amountUpdated As Integer = adapter.UpdateEntitiesDirectly(updateValuesProduct, bucket)
Setting the EntityState to Fetched automatically after a save
By design an entity which was successfully saved to the database gets as EntityState
OutOfSync. If you've specified to refetch the entity again after the save, the entity is then refetched with an additional fetch statement. This is done to make sure that default constraints, calculated fields and elements which could have been changed after the save action inside the database (for example because a database trigger ran after the save action)
are reflected in the entity after the save action. If you know that this won't happen in your application, you can get a performance gain by specifying that LLBLGen Pro should mark a successfully saved entity as
Fetched instead of OutOfSync. In this situation, LLBLGen Pro won't perform a fetch action to obtain the new entity values from the database.
To use this feature, you've to set the static/Shared property EntityBase2.
MarkSavedEntitiesAsFetched to true (default is false). This will be used for all entities in your application, so if you have some entities which have to be fetched after the update (for example because they have a timestamp field), you should keep the default, false. You can also set this value using the config file of your application by adding the following line to the
appSettings section of your application's config file:
<add key="markSavedEntitiesAsFetched" value="true"/>
You don't need to refetch an entity if it has a sequenced primary key (Identity or sequence), as these values are read back directly with the insert statement.
FK-PK synchronization
Foreign key synchronization with a related Primary key field is done automatically in code.
For example:
- Instantiate a Customer entity, add a new Order object to its Orders collection. Now add OrderDetails objects to the new Order object's OrderDetails
collection,. You can simply save the Customer entity and all included new/'dirty' entities will be saved and any PK-FK relations will be
updated/synchronized automatically.
- Alter the Customer object in the example above, and save the Order object. The Customer object is saved first, then the Order and then the
OrderDetails objects with all PK-FK values being synced
This synchronization of FK-PK values is already done at the moment you set a property to a reference of an entity object, for example
myOrder.Customer = myCustomer, if the entity (in this case myCustomer) is not new, or if the PK field(s) aren't sequenced fields when the entity is new.
Synchronization is also performed after a save action, so identity/sequenced columns are also synchronized.
If you set a foreign key field (for example Order.CustomerID) to a new value, the referenced entity by the foreign key (relation) the field is part of will be
dereferenced and the field mapped onto that relation is set to null (C#) or Nothing (VB.NET). Example:
// C#
OrderEntity myOrder = new OrderEntity();
CustomerEntity myCustomer = new CustomerEntity("CHOPS");
adapter.FetchEntity(myCustomer);
myOrder.Customer = myCustomer; // A
myOrder.CustomerID = "BLONP"; // B
CustomerEntity referencedCustomer = myOrder.Customer; // C
' VB.NET
Dim myOrder As New OrderEntity()
Dim myCustomer As New CustomerEntity("CHOPS")
adapter.FetchEntity(myCustomer)
myOrder.Customer = myCustomer ' A
myOrder.CustomerID = "BLONP" ' B
Dim referencedCustomer As CustomerEntity = myOrder.Customer ' C
After line 'A', myOrder.CustomerID will be set to "CHOPS", because of the synchronization between the PK of Customer and the FK of Order. At line 'B', the foreign
key field CustomerID of Order is changed to a new value, "BLONP". Because the FK field changes, the referenced entity through that FK field, Customer, is dereferenced
and myOrder.Customer will return null/Nothing. Because there is no current referenced customer entity, the variable
referencedCustomer will
be set to null / Nothing at line 'C'.
The opposite is also true: if you set the property which represents a related entity to null (Nothing), the FK field(s) forming this relation will be set
to null as well, as shown in the following example:
// C#
PrefetchPath2 path = new PrefetchPath2((int)EntityType.OrderEntity);
path.Add(OrderEntity.PrefetchPathCustomer);
OrderEntity myOrder = new OrderEntity(10254);
adapter.FetchEntity(myOrder, path); // A
myOrder.Customer = null; // B
' VB.NET
Dim path As New PrefetchPath2((int)EntityType.OrderEntity)
path.Add(OrderEntity.PrefetchPathCustomer)
Dim myOrder As New OrderEntity(10254)
adapter.FetchEntity(myOrder, path) ' A
myOrder.Customer = Nothing 'B
At line A, the prefetch path loads the related Customer entity together with the Order entity
10254. At line B, this customer is dereferenced. This means that the FK field of
order creating this relation, myOrder.CustomerId, will be set to null (Nothing). So if myOrder is saved after this, NULL will be saved in the field
Order.CustomerId
Deleting an entity
Deleting an entity is as simple as Saving an entity. Create a new entity instance into memory, set the PK field values
and call the DataAccessAdapter's method DeleteEntity().
You can also delete an entity using an entity collection (using the DataAccessAdapter's method
DeleteEntityCollection)
or remove it from the persistent storage directly (using the DataAccessAdapter's method
DeleteEntitiesDirectly)
To delete it the simple way: create the new entity object, set the PK field value and call DeleteEntity. We keep the connection open. (Instead of
using a new entity, you can also pass an existing entity object to DeleteEntity())
// [C#]
DataAccessAdapter adapter = new DataAccessAdapter(true);
CustomerEntity customer = new CustomerEntity("CHOPS");
adapter.DeleteEntity(customer);
adapter.CloseConnection();
' [VB.NET]
Dim adapter As New DataAccessAdapter(True)
Dim customer As New CustomerEntity("CHOPS")
adapter.DeleteEntity(customer)
adapter.CloseConnection()
It's wise to start a transaction if you want to be able to roll back the delete later on in your routine. For more information
about transactions, see the section about
Transactions.
Note:
|
Deletes are never recursive. This means that if the delete action of an entity violates a foreign key constraint,
an exception is thrown.
|
Entity state in distributed systems
In distributed environments, you work disconnected: the client holds data and doesn't have a connection with the server for manipulating the
data in the client process, it only contacts the service for persistence and data retrieval from the database. To understand the state of an entity object
the following explanation could help. Think in these steps:
- Create containers (entity objects)
- Add data to/load data in containers (from server for example)
- Show data in modifiers (forms)
- Data is modified and collected for persistence
- Collected data is send to server for persistence
- Process is ended
After step 6) the state should be considered void. It's up to you to ignore that and keep data around on the client. But as you work disconnected,
there is no feedback from the server, so for example if you send an entity from client to server and it is saved there: you won't all of a
sudden have an outofsync entity on the client, as that's just a copy of the object on the server.
So if you want to keep on working on the client with the data, you have to consider that after step 6) you have to rewind to 1) or 2),
unless you know what you can keep (read-only data for example).
If you're in 6) and you rewind to 4), you're modifying data which is out of sync with the server.
LLBLGen Pro doesn't provide you with a layer which takes care of that, as you should control that yourself, because only then the developer has
full control over when what happens. So when you send a UnitOfWork2 object to the server, you have to realize you're in 5) moving to 6) and
it's all over for that process. If that's not the case, then you shouldn't move from 4) to 5) there, but wait and persist the data later.
Concurrency control
Adapter contains an advanced concurrency mechanism, in such a way that you can decide how to implement concurrency control in your application.
It is often better to schedule concurrency aspects at a high level in your application, however if you are required to check whether a save can take
place or not, you can. As does SelfServicing, Adapter allows you to specify a predicate expression object with the SaveEntity() method. This predicate expression
is included in the UPDATE query (it's ignored in an INSERT query) so you can specify exactly when a save should take place. Adapter also allows you
to implement the interface IConcurrencyPredicateFactory, and instances of that interface can be inserted into entity objects. If such a factory is
present inside an entity, SaveEntity() will automatically request a predicate object from that factory to include in the UPDATE query. This way you can still
provide concurrency predicates during a recursive save action.
To filter on the original database values fetched into the entity to be saved, you can create for example FieldCompareValuePredicate instances which use
the EntityField2's
DbValue property. Even though a field is changed in memory through code, the DbValue property of a field will have the original value
read from the database. You can use this for optimistic concurrency schemes. See for an example the example below. If the field is NULL in the database, DbValue
is null (C#) or Nothing (VB.NET).
See for more information on predicates and filtering
Getting started with filtering.
Below an example implementation
of IConcurrencyPredicateFactory, which returns predicates which test for equality on EmployeeID for the particular order. This will make sure the Save or Delete
action will only succeed if the entity in the database has still the same value for EmployeeID as the in-memory entity had when it was loaded from the database.
// [C#]
private class OrderConcurrencyFilterFactory :
IConcurrencyPredicateFactory
{
public IPredicateExpression CreatePredicate(
ConcurrencyPredicateType predicateTypeToCreate, object containingEntity)
{
IPredicateExpression toReturn = new PredicateExpression();
OrderEntity order = (OrderEntity)containingEntity;
switch(predicateTypeToCreate)
{
case ConcurrencyPredicateType.Delete:
toReturn.Add(OrderFields.EmployeeID == order.Fields[(int)OrderFieldIndex.EmployeeID].DbValue);
break;
case ConcurrencyPredicateType.Save:
// only for updates
toReturn.Add(OrderFields.EmployeeID == order.Fields[(int)OrderFieldIndex.EmployeeID].DbValue);
break;
}
return toReturn;
}
}
' [VB.NET]
Private Class OrderConcurrencyFilterFactory
Implements IConcurrencyPredicateFactory
Public Function CreatePredicate( _
predicateTypeToCreate As ConcurrencyPredicateType, containingEntity As object) _
As IPredicateExpression Implements IConcurrencyPredicateFactory.CreatePredicate
Dim toReturn As IPredicateExpression = New PredicateExpression()
Dim order As OrderEntity = CType(containingEntity, OrderEntity)
Select Case predicateTypeToCreate
Case ConcurrencyPredicateType.Delete
toReturn.Add(OrderFields.EmployeeID = _
order.Fields(CInt(OrderFieldIndex.EmployeeID)).DbValue)
Case ConcurrencyPredicateType.Save
' only for updates
toReturn.Add(OrderFields.EmployeeID = _
order.Fields(CInt(OrderFieldIndex.EmployeeID)).DbValue))
End Select
Return toReturn
End Function
End Class
During recursive saves, if a save action fails, which can be caused by a ConcurrencyPredicateFactory produced predicate, thus if no rows are affected by the save
action, an ORMConcurrencyException is thrown by the save logic, which will terminate any transaction started by the recursive save.
To set an IConcurrencyPredicateFactory object when an entity is created or initialized, please see the section
Adding your own code to the generated classes which discusses various ways to modify the generated code to
add your own initialization code which for example sets the IConcurrencyPredicateFactory instance for a particular object. You can also set an IConcurrencyPredicateFactory
instance of an entity using the
ConcurrencyPredicateFactoryToUse property of an entity collection to automatically set the ConcurrencyPredicateFactoryToUse property
of an entity when it's added to the particular entity collection.
Entities, NULL values and defaults
Some datatypes, like date related datatypes and strings, are not always mandatory and are set to an unknown value. In most cases this
is NULL: the fields in the table are nullable and, if these fields do not yet have a value, they're set to NULL. Nullable fields
often have a 'default' value set; this is a value which is inserted by the database server when a NULL is inserted in such a column.
These default values are defined in the table definition itself.
Nullable(Of valueType) types
In .NET 2.0, Microsoft introduced the concept of Nullable valuetypes, which means that a field of type int/Integer or any other ValueType can be null / Nothing.
By default, LLBLGen Pro generates all ValueTyped fields as Nullable(Of
valueType) if the target platform is .NET 2.0 or CF.NET 2.0. You can overrule this
setting on a per-field basis by setting the preference (and project property)
GenerateNullableFieldsAsNullableTypes to true or false, which controls the
value of the setting for each field if the field has to be generated as nullable
or not. With Nullable types for valuetyped fields, LLBLGen Pro won't
convert a null / Nothing value for a field to a default value, but will return null / Nothing from the field's property.
NULL values read from the database
If a field is NULL in the database, the in-memory value will then become null / Nothing. This means that the CurrentValue
property of the field object in the entity's Fields collection (entity.Fields[index].CurrentValue) will be null / Nothing in this situation, not a default value.
Setting a field to NULL
Setting a field to NULL is easy. When you create a new entity, you simply do not supply a value for a field you want to set to NULL.
The INSERT query will notice that the field isn't changed (because you didn't supply a value for it), and will skip the field.
If you have set a default value for that column, the database engine will automatically fill in the default value for that field in
the database; this is standard database behaviour. If you want to set a field of an existing entity to NULL, you have to use a
special function: SetNewFieldValue(). You can set the field's value to null/Nothing and when you then save the entity, the value
in the table will be NULL. You have to use this method and not a set operation on a property, because value types like int/Integer
do not accept null/Nothing as a valid value. Using this method will not bypass checks, it's the same method used by properties
to set the value for the fields related to the property. Example:
// [C#]
OrderEntity order = new OrderEntity(10254);
DataAccessAdapter adapter = new DataAccessAdapter();
adapter.FetchEntity(order);
order.ShippingDate = null;
adapter.SaveEntity(order);
' [VB.NET]
Dim order As New OrderEntity(10254)
Dim adapter As New DataAccessAdapter()
adapter.FetchEntity(order)
order.ShippingDate = Nothing
adapter.SaveEntity(order)
Usually, you won't be needing this much: most of the time fields will be set to NULL when the entity is created and will be updated with
a value somewhere during the entity's lifecycle.
To test if a field is
currently representing a NULL value, or better: if the entity would be saved now, does the field become NULL
in the database, you can use a different method: TestCurrentFieldValueForNull():
// [C#]
CustomerEntity customer = new CustomerEntity("CHOPS");
customer.SetNewFieldValue((int)CustomerFieldIndex.ContactTitle, null);
customer.TestCurrentFieldValueForNull(CustomerFieldIndex.ContactTitle); // returns true
' [VB.NET]
Dim customer As New CustomerEntity()
customer.SetNewFieldValue(CType(CustomerFieldIndex.ContactTitle, Integer), Nothing)
customer.TestCurrentFieldValueForNull(CustomerFieldIndex.ContactTitle)' returns true
Note:
|
The usage of NULLs in databases should be discouraged and NULLs should only be used for fields which are
optional and often not filled in with a value. In other situations, always use a default value for a NULLable column.
|
Extending an entity by intercepting activity calls
During the entity's lifecycle and the actions in which the entity participates, various methods of the entity are called, and which might be a good candidate for
your own logic to be called as well, for example when the entity is initialized you might want to do your own initialization as well. The entity classes offer
a variety of methods for you to override so you can make your code to be called in various situations. These methods start all with
On and can be found
in the LLBLGen Pro reference manual in the class
EntityBase2. The entity classes also offer events for some situations, like the Initializing and Initialized
events.
If you want to perform a given action when one of these methods are called, you can override them in the generated entity classes, preferably
using the methods discussed in
Adding your own code to the generated classes.
IDataErrorInfo implementation
The .NET interface IDataErrorInfo is now implemented on EntityBase. Two methods have been added to the entities:
SetEntityError and
SetEntityFieldError, which allows external code to set the error of a field and/or entity. If append is set to true with SetEntityFieldError, the error message
is appended to an existing message for that field using a semi-colon as separator.
Entity field validation, which is triggered by the entity's method SetNewFieldValue() (which is called by a property setter), sets the field error if an
exception occurs or when the custom field validator fails. The error message is appended to an existing message.