⬆️ ⬇️

Using ORM in the development of enterprise applications

There is a lot of controversy about the pros and cons of ORM , let's try to focus on the pros when using it in ERP applications.



I have been developing an ERP platform for 5 years, I have developed three versions of the platform. It all started with EAV, after which there was a normal model, stored procedures, view-chi, and now evolved to use ORM. Let me share the experience why ORM is good.



To demonstrate the advantages of this approach, I developed a small application for a real estate agency (I drew inspiration from Cyan and a data model from it) and try to describe why, thanks to ORM, I did everything in 1 day.

')

image

I am a supporter of the CodeFirst approach, because This is the only correct way to plan the structure of a business application.

In our latest platform, after a long selection, we decided to use ORM DataObjects.Net , but the essence of the article will be clear to any ORM supporter, be it NHibernate, the Entity Framework, etc.



So, we plan a simple application for a real estate agency:

Realtor real estate agency (Agent) - makes to the system proposals for the lease and waiting for requests from tenants.

The tenant reviews the offers, selects interesting criteria for him according to a set of criteria and contacts the agent to make a deal.



Data model design



Creating a model is creating classes in C #, adding field properties, attributes, comments.

Model classes for real estate agency

in code, it looks like this:

/// <summary> ///    /// </summary> [HierarchyRoot] [Index("CreationTime", Clustered = true)] public abstract class RentOfferBase : DocumentBase { … /// <summary> ///  /// </summary> [Hidden] [Field(Length = 64)] public string Name { get; set; } /// <summary> ///   /// </summary> [Field] public DateTime CreationTime { get; set; } /// <summary> ///  /// </summary> [Field(Scale = 0)] public decimal Price { get; set; } /// <summary> ///  /// </summary> [Field(Scale = 0)] public decimal Comission { get; set; } /// <summary> ///  ///      /// </summary> [Field(Nullable = false)] public EnCurrency Currency { get; set; } /// <summary> ///  ///   /// </summary> [Field] public MetroLine Line { get; set; } /// <summary> ///  ///       /// </summary> [Field] public MetroStation Metro { get; set; } } 


Some features that make sense to pay attention to:



In this example, I applied inheritance to offers from a landlord (RentOfferBase) - the basic offer contains some of the fields, more detailed offers, such as an apartment offer - contains specifying fields - Kitchen area, Number of rooms.



Inheritance


When working with ORM, we can use such a powerful OOP tool as inheritance.

For the base class, rental offers are creating heirs: Apartment offers and Room offers

image

With obvious simplicity, this approach allows to drastically reduce the amount of code and simplify the development of similar entities, this is especially effective when developing similar documents that differ in several fields.



Encapsulation


In addition to the familiar to many encapsulations from the OOP world, when using ORM, we also encapsulate the physical data storage model. We can use any inheritance scheme for the same business code. Those. we change the structure of the database without changing the application code, or almost without changing it.

From the previous class structure, it is not entirely clear how the tables containing the data from the landlord will look like, but they can look in three different ways, depending on the value of the attribute indicating the inheritance scheme:



Classtable
Used by default, and creates a table for each class, starting from the root of the hierarchy

Table schema for the inheritance model ClassTable



SingleTable
One table for all hierarchy classes

The table schema for the SingleTable inheritance model



Concretetable
According to the table for each non-abstract class

Table layout for the ConcreteTable inheritance model



Why is all this necessary?


In some cases, it is convenient to store normalized data, and in others, for optimization, it is more convenient to denormalize tables. The advantage of ORM is that it can be done very simply - just by changing one line - in our case
 [HierarchyRoot] 
will be replaced by
 [HierarchyRoot(InheritanceSchema.SingleTable)] 
and
 [HierarchyRoot(InheritanceSchema.ConcreteTable)] 
respectively. In this case, because we write queries not in SQL, then all queries will be automatically translated to use the appropriate inheritance scheme. Those. A report on rental offers / apartments / rooms written on LINQ and working through ORM will work with each scheme and will not require any modifications.



Adding Business Logic



Form Events


Most platforms (like ours) are able to automatically generate forms according to the model. But we have few static forms, let's revive it, add dynamics. In our system, we have introduced such a concept as a form event handler - a class that implements the handler interface with an indication of which fields are tied events. By changing the data on the client, data is sent to the server, deserialization, .net object processing, serialization, sending data to the client.



For example, change the form Cost, immediately, on the fly, recalculated interest. And vice versa. But how concisely it looks in the code:

 /// <summary> ///    Price  ComissionPercent /// </summary> [OnFieldChange("Price", "ComissionPercent")] public class RentalPriceFormEvent : RentOfferFormEventsBase<RentOfferBase> { public override void OnFieldChange(RentOfferBase item) { if (item.ComissionPercent != decimal.Zero) { item.Comission = item.Price * 0.01m * item.ComissionPercent; } } } 


This is an event of calculating the commission on interest and price, the logic is very simple, but we can write here any code on .net. If necessary, make a request to the database or web-service.



Polymorphism


In the previous example, we wrote an event for only one RentOfferBase entity, this event will work with successors, but what if we have several price / commission entities? Write the same code every time?

Select the interface

 /// <summary> ///   /// </summary> public interface IWithComission { /// <summary></summary> decimal Price { get; set; } /// <summary></summary> decimal Comission { get; set; } /// <summary>%</summary> decimal ComissionPercent { get; set; } } 
and rewrite the event as
 /// <summary> ///    RentalPrice  ComissionPercent /// </summary> /// <typeparam name="TEntity"> </typeparam> [OnFieldChange("Price", "ComissionPercent")] public class RentalPriceFormEvent<TEntity> : RentOfferFormEventsBase<TEntity> where TEntity : DocumentBase, IWithComission { public override void OnFieldChange(TEntity item) { if (item.ComissionPercent != decimal.Zero) { item.Comission = item.Price * 0.01m * item.ComissionPercent; } } } 


Now this code will work for any entity that implements the IWithComission interface. At the same time, if it is necessary to make changes to the logic of interest calculation, then it should be done in a single place, in all other places everything will be applied automatically. For example, create an entity for the application for the purchase of an apartment.

This approach can significantly reduce the amount of code and ensure convenient product support.



Entity events


Entity events are very similar to form events, but they are transactional at the time an entity changes. This is a kind of analogue of the DB triggers, but, unlike the triggers, and similar to form events, you can use the OOP approach. For example, we need to control the change of entities on the status “closed” so that no one except the administrator can change them. Pretty simple code



 /// <summary> ///       /// </summary> [FireOn(EntityEventAction.Updated)] public class CheckStatus<TEntity> : IEntityEvent<TEntity> where TEntity : EntityBase, IWithStatus { /// <summary> ///   /// </summary> /// <param name="item">    </param> public void Execute(TEntity item) { if (item.Status.Name == "" && !Roles.IsUserInRole("admin")) { throw new ErrorException("     ''!"); } } /// <summary> ///       /// </summary> public EntityEventAction CurrentAction { get; set; } } 




Which checks that if the entity being changed is on the “Closed” status and the user does not belong to the admin role, an exception is generated. Similar to the events of the forms, the events of the entities will be applied to all entities compatible with them, in this case implementing the IWithStatus interface.



Code separation


Some approaches use RichDomainModel, we have it. Anemic

and this means that there is practically no business logic in the entity class. (For this there are Events Forms / Entities / Filters, etc.)

The advantage of this approach is the ability to modify the behavior of external entities. For example, one company developed the Addresses module and delivers it as a library, we do not have access to the source code of this library and want to add some behavior to the form, for example, when choosing an incorrect address to warn.

To do this, we can write a form event that will be applied to the external component.



Filters


The use of ORM allows filtering to use such powerful .net tools as ExpressionTrees. We can write a filtering expression in advance for use as constraints of business logic, we can filter the grid based on user actions.



For example, to limit the visibility of irrelevant tickets, for the manager, the following filtering expression from the code is applied:

 public static Expression<Func<TOffer, bool>> FilterOffers<TOffer>() where TOffer : RentOfferBase { return a => a.Creator.SysName == SecurityHelper.CurrentLogin || a.Status.Name == ""; } 




This is a simple filter that is used to restrict access rights only to their applications , or to applications on the “Actual” status.

This filter is not explicitly attached to any entity right now, the generic parameter only says that it can be used for RentOfferBase and any of its heirs. For whom it will actually be applied will be determined later, at the time of setting up the application.



We can also filter one form field depending on another.

 [FilterFor(typeof(RentOfferBase), "Metro")] public static Expression<Func<MetroStation, bool>> MetroFilter([Source("Line")]MetroLine line) { return a => line == null || a.MetroLine == line; } 




Here we filter metro stations depending on the selected branch, specifying entities and fields in the attributes, which are used as sources of values ​​and filtering objects.



Making changes to the system



ERP system, unlike other applications, requires frequent changes in business logic and data model, and this process should be simple and reliable.



Here it must be said that not only ORM is important, but CodeFirst ideology. In the previous version of our system, we also used ORM - Linq2SQL. In this case, the Database-first approach was used, the database was stored as a “master database” and update scripts. A typical error encountered in this approach is that the code of the .net classes does not correspond to the database. To solve the problem, we wrote our own validators for the database structure.



What do we get in CodeFirst:





But what about the updates?



Migration


Imagine that we are preparing an update that the customer installs on their database. Simple migrations are performed fully automatically. Those. if we made safe changes to the model - then ORM itself will migrate the database to the new version.

Secure changes. these changes do not delete data from the database, for example:



Of course, these actions are not enough when developing serious applications, what to do with the task of renaming a field / entity?

  1. Apply rename refactoring (we use ReSharper). In this case, all uses of this field in our code are renamed.

    Including renamed uses in Filters, Form Events, Entity Events. Difficulty can deliver only the field names in the Attributes found in the form of text, but if the field name is sufficiently unique, then there will be no problems. In this case, after renaming, you can start the compilation and make sure that the fields in the attributes are renamed correctly.
  2. Add hint rename. Hint is a tooltip for ORM, what to do with a database if there are differences between a schema built by classes and a real schema in SQL. hint rename field looks like this:

     public class RenameFieldUpgrader : ModelUpgraderBase { public override Version Version { get { return new Version("3.5.0.8764"); } } public override void AddUpgradeHints(ISet<UpgradeHint> hints) { hints.Add(new RenameFieldHint(typeof(RentOfferBase), "OldName", "NewName")); } } 


With similar hints, we can indicate renaming the entity, deleting the field / entity. If there is such a hint, the next time ORM starts, it automatically applies renaming refactoring for the database and renames the field with the data saved.



Results



As a result of the application of ORM, we received:

Source: https://habr.com/ru/post/140713/



All Articles