Всемогущий, Google, найди мне чего-нибудь!

четверг, 9 декабря 2010 г.

Table-per-Type (ActiveRecord)

Работая над последним проектом, возникла проблема разделения абстрактных и конкретных сущностей. Вырву кусок ТЗ из контекста: необходимо была возможность легкой расширяемости служб инкассации. Например, существует на данный момент 3 вида инкассации: инкассация Сбербанка, внутренняя и фельдьегерская (вроде бы, инкассация НБУ, во всяком случае государственная) инкассации. Задача была в безболезненном добавлении новых служб.

В теории необходимо указать абстрактный класс, который будет содержать общие для всех видов инкассации свойства и методы, а в конкретных моделях указать специфические. Например, у каждой службы инкассации собственная формула расчета стоимости перевозки наличности, у какой-то ограничены дистанция и сумма перевозимой наличности и т.д.



Итак, изначально для построения сущностей была взята ОРМ ActiveRecord. Посему сперва напишем наш абстрактный класс, который будет содержать общие для всех служб инкассации методы и свойства. Листинг кода некоторых классов выкладывать не буду, потому что из названия можно легко догадаться о его примерном назначении (в целях экономии места комментарии и некоторые методы были опущены):

[JoinedBase]
[ActiveRecord("ENCASHMENT_SERVICE", Where = "Enabled = 1")]
public abstract class IEncashmentService : ActiveRecordBase
{
 #region Properties

 [PrimaryKey(Generator = PrimaryKeyType.Increment, Column = "ID")]
 public int Id { get; protected set; }

 [Property("NAME")]
 public string Name { get; protected set; }

 [Property("ENABLED")]
 public bool Enabled { get; protected set; }

 [Property("AGREEMENT")]
 public string Agreement { get; protected set; }

 [HasMany(ColumnKey = "SERVICE_ID", Lazy = false)]
 public ISet<EncashmentRoute> Routes { get; protected set; }

 [HasMany(ColumnKey = "SERVICE_ID", Lazy = false)]
 public ISet<EncashmentServiceParameter> Parameters { get; protected set; }

 [HasMany(ColumnKey = "SERVICE_ID", Lazy = true)]
 internal ISet<Deal> Deals { get; set; }

 #endregion

 #region Methods

 public abstract IEnumerable<PotentialDeal> Create(CashRequest buyer, CashRequest seller);

 #endregion
}

В данном листинге ключ к статье - атрибут "JoinedBase". Имеено он определяет, что данный класс будет абстрактным родителем для всех служб инкассации.

Теперь необходимо описать конкретные службы инкассации. В данной статье напишу пример "Внутренней инкассации", а остальные делаются по аналогии. Итак, листинг кода внутренней службы инкассации будет выглядеть следующим образом (комментарии также были опущены):

[EncashmentServiceCode("Internal")]
[ActiveRecord("ENCASHMENT_SERVICE_INTERNAL")]
public class InternalEncashmentService : IEncashmentService
{
 #region Properties

 [JoinedKey("SERVICE_ID")]
 protected internal int ServiceId { get; set; }

 #endregion
 
 #region Methods

 public override IEnumerable<PotentialDeal> Create(CashRequest buyer, CashRequest seller)
 {
  /* Функциональность метода была опущена, т.к. для статьи он неважен. */
 }

#endregion
}

В последнем листинге нас интересует атрибут свойства ServiceId - JoinedKey. Именно он показывает о том, что данный класс является "прикрепленным" наследником от класса IEncashmentService.

Заполните базу данными о службах инкассации. Теперь, чтобы проверить правильно ли вы все сделали, добавьте проект юнит-тестов и в одном из тестов напишите следующее:

IEncashmentService[] encashmentServices = 
                    ActiveRecordBase<IEncashmentService>.FindAll();
Assert.IsTrue(encashmentServices.Length > 0);

Учтите, что связка между абстрактной и конкретной моделью осуществляется через уникальный идентификатор службы инкассации. Если nHibernate, пробегая по "абстрактной" таблице не найдет ни в одной из "конкретной" таблицы, то произойдет исключение, так что аккуратнее. =)

Также учтите, что таблицы необходимо заводить как для абстрактной модели, так и для каждой конкретной.

Будут вопросы - пишите.

Комментариев нет: