Using ORM in enterprise application development

    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.

    For 5 years I have been developing a platform for ERP, I have developed three versions of the platform. It all started with EAV, after that there was a normal model, stored procedures, view-hee, and now it has evolved before using ORM. Let me share my experience of 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, as this is the only thing right for planning the structure of a business application.
    In our lastplatform we after a long selection decided to use the ORM DataObjects.Net , but the essence of the article will be clear to any follower of ORM, whether NHibernate, Entity Framework, etc.

    So, we’re planning a simple application for a real estate agency: Real estate
    agency realtor (Agent) - makes rental offers in the system and waits for requests from tenants.
    The tenant reviews the offers, selects interesting criteria for him by a variety of criteria and contacts the agent to conclude a transaction.

    Data Model Design


    Creating a model is creating classes in C #, adding field properties, attributes, comments.
    Class model for a real estate agency
    in code, it looks something like this:
    /// 
    /// Предложение от арендодателя
    /// 
    [HierarchyRoot]
    [Index("CreationTime", Clustered = true)]
    public abstract class RentOfferBase : DocumentBase
    {
        …
        /// 
        /// Наименование
        /// 
        [Hidden]
        [Field(Length = 64)]
        public string Name { get; set; }
        /// 
        /// Дата публикации
        /// 
        [Field]
        public DateTime CreationTime { get; set; }
        /// 
        /// Стоимость
        /// 
        [Field(Scale = 0)]
        public decimal Price { get; set; }
        /// 
        /// Комиссия
        /// 
        [Field(Scale = 0)]
        public decimal Comission { get; set; }
        /// 
        /// Валюта
        /// Валюта в которой указаны цены
        /// 
        [Field(Nullable = false)]
        public EnCurrency Currency { get; set; }
        /// 
        /// Линия
        /// Линия метро
        /// 
        [Field]
        public MetroLine Line { get; set; }
        /// 
        /// Метро
        /// Станция метро расположенная рядом с объектом
        /// 
        [Field]
        public MetroStation Metro { get; set; }    
    }
    

    Some features that it makes sense to pay attention to:
    • Xml comments
      They are made not only for the comprehensibility of the code, but also for use in the system interface. What is on the first line will be the displayed field name. All that follows is a tooltip. With this approach, we have simplified our support for code documentation and object name management.
    • Attributes
      
      [Field(Length = 64)]
      
      Field - a general indication that the property will be a persistent property
      Length - the length of the string
      Scale = 0 - the number of decimal places in decimal
      [Index("CreationTime", Clustered = true)] 
      
      An attribute of an entity (class) indicating the creation of an index, key fields, type
      
      [HierarchyRoot]
      
      An attribute indicating that the class is the root of the entity hierarchy, i.e. all instances of both this class and its descendants are stored

    In this example, I applied inheritance for offers from the landlord (RentOfferBase) - the basic offer contains some of the fields, more detailed offers, for example, the apartment offer - contains qualifying 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 of rental offers, we create the heirs: Apartment Offers and Room Offers
    image
    With obvious simplicity, this approach allows you 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 encapsulation from the world of OOP that is familiar to many, when using ORM, we also encapsulate the physical model of data storage. 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 structure of the classes, it is not entirely clear how the tables containing the data of offers from the lessor will look like, and they can look in three different ways, depending on the value of the attribute indicating the inheritance scheme:

    Classtable
    It is used by default, and creates a table for each class, starting from the root of the hierarchy
    Table schema for the ClassTable inheritance model

    Singletable
    One table for all hierarchy classes
    Table schema for SingleTable inheritance model

    ConcreteTable
    According to the table for each non-abstract class
    Table schema for 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. Moreover, since Since we do not write queries in SQL, then all queries will be automatically translated to use the appropriate inheritance scheme. Those. a report on rental proposals / apartments / rooms written in 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) can automatically generate forms according to the model. But we do not have enough static forms, let's revive it, add dynamics. In our system, we introduced such a concept as a form event handler - a class that implements a handler interface with an indication of which fields are associated with events. By changing data on the client, data is sent to the server, deserialization, processing of the .net object, serialization, sending data to the client.

    For example, we change the Cost on the form immediately, on the fly, the Percentage is recalculated. And vice versa. And here is how succinctly it looks in the code:
    
    /// 
    /// Обработчик изменения поля Price и ComissionPercent
    /// 
    [OnFieldChange("Price", "ComissionPercent")]
    public class RentalPriceFormEvent : RentOfferFormEventsBase
    {
        public override void OnFieldChange(RentOfferBase item)
        {
            if (item.ComissionPercent != decimal.Zero)
            {
                item.Comission = item.Price * 0.01m * item.ComissionPercent;
            }
        }
    }

    This is the event of calculating the commission on interest and price, the logic is very simple, but we can write any code on .net here. If necessary, execute a query 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 heirs, but what if we have several entities with a price / commission? Do you write the same code every time?
    Select the interface
    /// 
    /// С комиссией
    /// 
    public interface IWithComission
    {
        /// Стоимость
        decimal Price { get; set; }
        /// Комиссия
        decimal Comission { get; set; }
        /// %
        decimal ComissionPercent { get; set; }
    }
    
    and rewrite the event as
    /// 
    /// Обработчик изменения поля RentalPrice и ComissionPercent
    /// 
    /// Тип сущности
    [OnFieldChange("Price", "ComissionPercent")]
    public class RentalPriceFormEvent : RentOfferFormEventsBase
    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. Moreover, if you need to make changes to the logic of calculating interest, you need to do this in a single place, in all other places everything will be applied automatically. For example, create an entity for an application for the purchase of an apartment.
    This approach can significantly reduce the amount of code and provide convenient product support.

    Entity Events

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

    /// 
    /// Событие для установки краткого наименования заявки
    /// 
    [FireOn(EntityEventAction.Updated)]
    public class CheckStatus : IEntityEvent
        where TEntity : EntityBase, IWithStatus
    {
        /// 
        /// Операция контроля
        /// 
        /// Элемент сущности с измененными полями
        public void Execute(TEntity item)
        {
            if (item.Status.Name == "Закрыт" && !Roles.IsUserInRole("admin"))
            {
                throw new ErrorException("Запрещено изменение сущностей на статусе 'Закрыт'!");
            }
        }
        /// 
        /// Текущее действие выполняемое над элементом сущности
        /// 
        public EntityEventAction CurrentAction { get; set; }
    }


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

    Code separation

    Some approaches use RichDomainModel, but we have Anemic
    , which means that there is practically no business logic in the entity class. (There are Form / Entity Events / Filters, etc. for this).
    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, warn when choosing the wrong address.
    To do this, we can write a form event that will be applied to the external component.

    Filters

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

    For example, to limit the visibility of irrelevant applications, the following filtering expression from code is used for the manager:
    public static Expression> FilterOffers() 
        where TOffer : RentOfferBase
    {
        return a => a.Creator.SysName == SecurityHelper.CurrentLogin || a.Status.Name == "Актуально";
    }


    This is a simple filter used to restrict access rights only to your applications , or to applications with the status of “Actual.”
    This filter is now not explicitly tied to any entity, the generic parameter only says that you can use it for RentOfferBase and any of its the heirs. For whom it will be really applied will be determined later, at the time of application setup.

    We can also set filtering of one form field depending on another
    [FilterFor(typeof(RentOfferBase), "Metro")]
    public static Expression> MetroFilter([Source("Line")]MetroLine line)
    {
        return a => line == null || a.MetroLine == line;
    }


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

    Making system changes


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

    It must be said here that it is not just ORM that matters, but the CodeFirst ideology. In the previous version of our system, we also used ORM - Linq2SQL. The Database-first approach was used, the database was stored in the form of a “master database” and update scripts. The typical error encountered in this approach is that the .net class code does not match the database. To solve the problem, we wrote our own database structure validators.

    What do we get in CodeFirst:
    • Rapid development of entities - we write .net class rarely thinking about the database
    • A convenient way to store the database in the version control system - we only store the class code
    • Validation of business logic at compile time


    But what about updates?

    Migration

    Imagine that we are preparing an update that the customer installs on his database. Simple migrations are fully automatic. Those. if we made safe changes to the model, then ORM itself will migrate the database to the new version.
    Safe changes. these are changes that do not delete data from the database, for example:
    • Create a new field
    • Create a new entity
    • Increased accuracy / field length

    Of course, these actions are not enough when developing serious applications, what should be done when renaming a field / entity?
    1. Применить рефакторинг переименования (мы используем ReSharper). При этом все использования этого поля в нашем коде переименовываются.
      В том числе переименовываются использования в Фильтрах, Событиях Формы, Событиях Сущности. Сложность может доставлять только встречающиеся в виде текста имена полей в Атрибутах, но если наименование поля достаточно уникально — то проблем не будет. При этом после переименования можно запустить компиляцию и убедиться что поля в атрибутах переименованы правильно.
    2. Добавить hint переименования. Hint — подсказка для ORM что же делать с БД при наличии различий между схемой построенной по классам и реальной схемой в SQL. hint переименования поля выглядит примерно так:
      public class RenameFieldUpgrader : ModelUpgraderBase
      {
          public override Version Version { get { return new Version("3.5.0.8764"); } }
          public override void AddUpgradeHints(ISet hints)
          {
              hints.Add(new RenameFieldHint(typeof(RentOfferBase), "OldName", "NewName"));
          }
      }

    By similar hints, we can point to renaming an entity, deleting a field / entity. If there is such a hint, at the next start, ORM will automatically apply renaming refactoring for the database and rename the field with saving data.

    Summary


    As a result of using ORM we got:
    • Convenient and fast development of both a data model and business logic
    • Convenient, simple and reliable application support
    • Take advantage of OOP at all stages
    • Code reuse (excluded copy-paste)
    • Code validation at compile time
    • Concise, clear business logic code

    Also popular now: