Using ORM
DWKit uses its own ORM. Here, we will take a more detailed look at its program API on server.
Metadata
In DWKit ORM, it is crucial to have an object of the EntityModel
type to make database requests for data change and selection. There are
three ways to get this object:
-
By the name of Data Model object:
EntityModel model = await MetadataToModelConverter.GetEntityModelByModelAsync("ModelName", 1);
Absolutely all attributes are moved from DataModel to
EntityModel
. The second function parameter specifies depth to which a model will be assembled by references. For example, if this parameter equals:- 0 - only attributes, located directly in Data Model, will appear in
EntityModel
. - 1 - if there are attributes with Type = Reference in DataModel, attributes of the entities (another referenced Data Models),
which DataModel attributes refer to, are included into
EntityModel
. - 2 and more - the larger the value, the greater the depth of the models included by reference.
- 0 - only attributes, located directly in Data Model, will appear in
-
by Form name:
EntityModel model = MetadataToModelConverter.GetEntityModelByFormAsync("FormName").Result;
In this case information from Form and data mapping is used to form
EntityModel
. Those attributes that have Load checkbox checked, are moved toEntityModel
from DataModel specified in the form just like Main Entity in the Mapping data model section of the admin interface. Depth by references (joins) is determined by the same Load attribute. Besides, the so-called Collections are also included in theEntityModel
. In general, Collections represent theEntityModel
plus filter that selects records based on the parent record.EntityModel
for a collection is obtained from the information in the Mapping data model section of the Collections subsection. Attributes for theEntityModel
collections are selected based on the Load attribute. An example of a collection - mainEntityModel
- invoice and the collection - invoice breakdown that is tied to it. Learn more about data binding to form mechanism in action here. -
Metadata can also be obtained from the
DbObject<T>
inheritor. We cover this topic further in this section.
The EntityModel
class contains the following information:
model.Name
- name of a model.model.SchemaName
- name of the scheme containing the main table of a model.model.TableName
- name of the main table of a model.model.PlainAttributes
- attributes located in the main table of a model. They are selected without joins, and their changes are tracked.model.JoinedAttributes
- requested using joins by references, updates are neither tracked nor applied.model.Attributes
- all attributes combined.model.Collections
- a list of collections bound to model, which are requested and changed together with the model. Changes take place in the transaction.model.PrimaryKeyAttribute
- a primary key attribute.model.ExtensionsContainerAttribute
- attribute which stores extension attributes.
Attribute is represented by the AttributeModel
class and contains the following information:
attribute.Name
- name of an attribute.attribute.ColumnName
- name of a column.attribute.PropertyName
- name of a property in the resulting data object.attribute.Type
- an attribute type, an object that can transform types. Is anAttributeType
object, which contains information on attribute type, its nullability, and additional methods of type casting and parsing strings.attribute.IsReference
- determines whether an attribute is a reference to another Data model.attribute.ReferencedDataModel
- data model (name + scheme + table name), which the attribute refer to.attribute.Children
- child attributes, i.e. attributes fromReferencedDataModel
.attribute.IsCalculated
- if true - this attribute is equal to a calculated column in the database.attribute.IsExtension
- if true - this attribute is an extension attribute.
This collection is described by the CollectionModel
class and contains the following information:
collection.Name
- name that will be the name of a property in the resulting object the collection is bonded to.collection.Model
-EntityModel
which describes collection items model.Filter collection.GetCollectionFilter(List<DynamicEntity> parentEntities)
- builds a filter which is used when requesting collection data from database.List<DynamicEntity> collection.GetCollection(DynamicEntity parentEntity, List<DynamicEntity> collectionEntities)
- selects records related to the passedparentEntity
.
Getting data via ORM and DynamicEntity
If you have EntityModel
, you can use it to request data from the database. Requesting all data without a filter and sorting will look like
this:
List<DynamicEntity> data = await model.GetAsync(Filter.Empty);
List<DynamicEntity> data = await DynamicRepository.GetAsync(model, Filter.Empty);
When requesting data, you can specify Filter
, Order
and Paging
.
Filter
An object filter. You can create complex nested filters, using Filter
class. First, specify which operator (AND or OR) combines conditions
in the root expression of the filter:
var filter = Filter.And;
var filter = Filter.Or;
Then specify a certain number of conditions - predicates and, probably, nested filters:
var filter = Filter.And.Equal(10, "Amount").LikeRightLeft("Document", "Name");
This expression represents the following filter Amount = 10 AND Name LIKE ('%Document%').
You can create any nested filters, for example ... AND (... OR (... AND ...) OR ...) AND .... To do that, use the following methods:
NestOr()
- starts a nested filter which conditions are connected by OR operator.NestAnd()
- starts a nested filter which conditions are joined by AND operator.Parent()
- returns to parent filter to which you can add new conditions.
var filter = Filter.And.Equal(10, "Amount").NestOr()
.LikeRightLeft("Document", "Name").LikeRightLeft("Deed", "Name")
.Parent().GreaterOrEqual(DateTime.Today, "Date");
You are going to get the following filter Amount = 10 AND (Name LIKE ('%Document%') OR Name LIKE ('%Deed%')) AND Date >= '1982-11-05'.
Filters can be merged, using Merge
function:
var filter = Filter.And.Equal(10, "Amount");
var filter1 = Filter.And.LikeRightLeft("Document", "Name");
filter = filter.Merge(filter1);
You are going to get the same expression as in the first case Amount = 10 AND Name LIKE ('%Document%').
To represent the NULL value, use Null.Value
or null
, for example:
var filter = Filter.And.Equal(Null.Value, "Amount");
var filter = Filter.And.Equal(null, "Amount");
Correspondence of the filter functions to SQL expressions:
Function | Expression |
---|---|
f.LikeRight(v, n) | n LIKE ('v%') |
f.LikeLeft(v, n) | n LIKE ('%v') |
f.LikeRightLeft(v, n) | n LIKE ('%v%') |
f.NotLikeRight(v, n) | n NOT LIKE ('v%') |
f.NotLikeLeft(v, n) | n NOT LIKE ('%v') |
f.NotLikeRightLeft(v, n) | n NOT LIKE ('%v%') |
f.Equal(v, n) | n = v |
f.NotEqual(v, n) | n <> v |
f.Greater(v, n) | n > v |
f.Less(v, n) | n < v |
f.GreaterOrEqual(v, n) | n >= v |
f.LessOrEqual(v, n) | n <= v |
f.In(List<> {v1, v2, ...}, n) | n IN (v1, v2, ...) |
f.NotIn(List<> {v1, v2, ...}, n) | n NOT IN (v1, v2, ...) |
f.Custom(subquery, parameters) | user's parameterized subquery |
Order
Sets database sorting order in the database.
var order1 = Order.StartAsc("Name").Desc("Amount");
var order2 = Order.StartDesc("Amount").Asc("Name");
Both expressions will give us a Name ASC, Amount DESC sorting.
Pagination
Sets data page for the selection:
var skip = 100;
var take = 10;
var paging = Paging.Create(skip, take);
Skips 100 records and returns 10 records.
DynamicEntity
You can use Filter
, Order
and Paging
in a request. For example:
List<DynamicEntity> data = await model.GetAsync(filter, order, paging);
At the same time, data
corresponds to List<DynamicEntity>
. DynamicEntity
is a transformed ORM record with specified properties. You
can get and change property values via indexers:
List<DynamicEntity> data = await model.GetAsync(filter, order, paging);
DynamicEntity document = data.First();
var name = document["Name"];
var amount = document["Amount"];
document["Amount"] = amount + 10;
var id = document.GetId();
Or you can cast DynamicEntity
to dynamic
and manipulate data dynamically:
List<DynamicEntity> data = await model.GetAsync(filter, order, paging);
dynamic document = data.First() as dynamic;
var name = document.Name;
document.Amount = document.Amount + 10;
Usually, choice of DynamicEntity
type depends on a particular task.
Changing data (insert, update, delete)
As in the case of data requesting, you need EntityModel
to change data. Two options are available. First, you can use EntityModel
extension methods or DynamicRepository
. For example, you receive a JSON with the changed form data from the client, and you need to
perform insert or update operations for the main record and insert, update or delete operations for the collections. Let's assume that you
receive the data as a request.Data
JSON string:
var model = await MetadataToModelConverter.GetEntityModelByFormAsync("FormName");
var data = new DynamicEntityDeserializer(model).DeserializeSingle(request.Data, resetIdsFromClient: true, initMissingFields: false);
await model.UpdateSingleAsync(data);
This means that you first get EntityModel
for the form, then deserialize JSON into DynamicEntity
and call the UpdateSingleAsync
method
that updates data in the database itself. Besides, in case there are any collections in data
, they will be updated together with the main
record.
You should use another method to delete the main record. You receive the IDs of the records to be deleted in request.Data
, and even in
case you receive records as a whole, you can get IDs from them.
var model = await MetadataToModelConverter.GetEntityModelByFormAsync("FormName");
var ids = JsonConvert.DeserializeObject<List<object>>(request.Data).Select(c => model.PrimaryKeyAttribute.Type.ParseToCLRType(c));
await model.DeleteAsync(ids);
Second, you can request data from the database and get them as a special ObservableEntityContainer
object that not only contains data, but
also tracks changes in data, and sends them to the database.
var container = await DynamicRepository.GetObservableEntitiesAsync(model, filter);
IEnumeralble<DynamicEntity> data = container.Entities;
var document = data.First() as dynamic;
document.Amount = document.Amount + 10;
await container.ApplyAsync();
The container tracks changes of DynamicEntity
located in it. You can also add new DynamicEntity
and delete the existing ones. All the
changes will be applied and saved in the database. Adding a new record:
var newEntity = await model.NewAsync();
newEntity.Name = "New";
container.Merge(new List<DynamicEntity>{newEntity});
await container.ApplyAsync();
Deleting records:
IEnumeralble<DynamicEntity> data = container.Entities;
var document = data.First() as dynamic;
container.Remove(new List<DynamicEntity>{document});
await container.ApplyAsync();
You can also change records using the Merge
function.
List<DynamicEntity> externalData = ...;
var filter = Filter.Empty;
foreach (DynamicEntity en in externalData)
{
if (en.HasPrimaryKey) // it was a record from the database, need to update
{
filter.Equal(en.GetPrimaryKey(), idProperty);
}
else // it is new record, need to insert
{}
}
var container = await DynamicRepository.GetObservableEntitiesAsync(model, filter); // getting existing records from the database
container.Merge(externalData);
await container.ApplyAsync(); // the records will be inserted and updated
Typed ORM objects
DynamicEntity
is a not strongly typed and dynamic object. Using it, you can write shorter generalized algorithms of business logic.
However, in case there are multiple unique algorithms for a certain object, using DynamicEntity
, you will have to write a lot of
repetitious noisy code. In DWKit ORM, you can inherit from a special DbObject<T>
class and obtain all ORM functions as a typed object.
First you should declare a class:
public class TypedOrmEntity : DbObject<TypedOrmEntity>
{
[DbObjectModel(IsKey = true)]
public Guid Id
{
get => _entity.Id;
set => _entity.Id = value;
}
[DbObjectModel]
public byte Type
{
get => _entity.Type;
set => _entity.Type = value;
}
[DbObjectModel]
public string Name
{
get => _entity.Name;
set => _entity.Name = value;
}
}
Here you can see a declared entity stored in the TypedOrmEntity table with the Id, Name and Type columns. These class and table in
the database are all you need to request, change, insert and delete data. As you can notice, the TypedOrmEntity
class is a wrapper
over DynamicEntity
, that's why you have to read and update properties in the inner _entity
object. However, DbObject<T>
also
contains EntityModel
and ObservableEntityContainer
. All properties uploaded from the database should be marked with the DbObjectModel
attribute and should transmit changes to the inner _entity
object. Here's an example of working with data:
var entity1 = new TypedOrmEntity() {Id = Guid.NewGuid(), Name = "Entity1", Type = 1};
var entity2 = new TypedOrmEntity() {Id = Guid.NewGuid(), Name = "Entity2", Type = 2};
await TypedOrmEntity.ApplyAsync(entity1, entity2); // two records will be inserted
entity1.Type = 3;
await TypedOrmEntity.ApplyAsync(entity1); // one record will be updated
await TypedOrmEntity.DeleteAsync(entity1, entity2); // two records will be deleted
An example of requesting data:
var data = await TypedOrmEntity.SelectAsync(filter, order);
Refer to the previous section to find out how to build filter
and order
. In case you want to create another typed entity referring to
the TypedOrmEntity
and include an attribute from the TypedOrmEntity
in it, you should also do it using the attributes.
public class AnotherTypedOrmEntity : DbObject<AnotherTypedOrmEntity>
{
[DbObjectModel(TableType = typeof(TypedOrmEntity), ParentPropertyName = nameof(TypedOrmEntityId), ColumnName = "Name")]
public string TypedOrmEntityName
{
get => _entity.TypedOrmEntityName;
set => _entity.TypedOrmEntityName = value;
}
[DbObjectModel]
public byte Type
{
get => _entity.Type;
set => _entity.Type = value;
}
[DbObjectModel(IsKey = true)]
public Guid Id
{
get => _entity.Id;
set => _entity.Id = value;
}
[DbObjectModel]
public string Name
{
get => _entity.Name;
set => _entity.Name = value;
}
[DbObjectModel(ColumnName = "OrmEntityId")] // use column name if property names differ from the column names
public Guid TypedOrmEntityId
{
get => _entity.TypedOrmEntityId;
set => _entity.TypedOrmEntityId = value;
}
}
In this case, DWKit ORM will build a join between the objects itself.
[DbObjectModel(TableType = typeof(TypedOrmEntity), ParentPropertyName = nameof(TypedOrmEntityId), ColumnName = "Name")]
- sets a join by the TypedOrmEntity.Id = TypedOrmEntityName.OrmEntityId columns.[DbObjectModel(ColumnName = "OrmEntityId")]
- in case the ColumnName is specified, it is not necessary to name a property a same name with a column.
If you received a typed ORM object from the database and want to change it, you need to call the StartTracking()
method before
change this object:
var entity = (await TypedOrmEntity.SelectAsync(filter)).FirstOrDefault();
if (entity != null)
{
entity.StartTracking();
entity.Type = 100;
await entity.ApplyAsync();
}
DWKit ORM and transactions
In DWKit ORM, you can open transactions implicitly in the using
block, and all nested operations will be performed in this transaction. It
is somewhat similar to TransactionScope
, however, it does not use the distributed transactions mechanism.
using (var shared = new SharedTransaction())
{
await shared.BeginTransactionAsync();
await TypedOrmEntity.ApplyAsync(entity1, entity2);
await AnotherTypedOrmEntity.ApplyAsync(anotherEntity1, anotherEntity2);
shared.Commit();
}
In case when the exception will be thrown in the using
block, the transaction will rollback itself. You should call Commit
explicitly.
It is OK if one transaction will be opened inside another one.
using (var shared1 = new SharedTransaction())
{
await shared1.BeginTransactionAsync();
....
using (var shared2 = new SharedTransaction())
{
await shared2.BeginTransactionAsync();
...
shared2.Commit();
}
....
shared1.Commit();
}
In case shared1
was opened and started, shared2
won't be opened and started, and all expressions inside the second using
will
use shared1
. Thus, you can use SharedTransaction
regardless of whether the transaction has opened earlier or not.
However, you should be careful because a transaction cannot be created again, and database management systems cannot increase (or decrease) the transaction isolation level after a transaction is being opened. The isolation level inside a nested transaction will remain the same as inside an external one.
You can change the level of isolation in the parameters of the BeginTransactionAsync
method.
await shared.BeginTransactionAsync(IsolationLevel.ReadCommitted);