При этом в будующем расчитывается , добавить систему рейтингов, где будет по определённым формулам, высчитывается показатель человека, компании, города, как собственный, так и зависящий от связанных с ним рейтингов. Таким образом сотрудником, должно быть не всё равно на рейтинг всей организации, а организации на сотрудников.
Рисунок 2.3 – Жалоба и теги
При создании таблицы жалоб я выбрал не самую масштабированную, однако более удобную в некоторых случаях схему. С одной стороны жалоба по своей сути должна позволять присоединяться к любому Entity. Если первичный ключ элемента, на который жалуются, был бы числовым, как это обычно и бывает, это приводило бы к конфликтам. Для этого приходилось бы вводить дополнительное поле с указанием таблицы. Ещё один вариант, был бы создание единой таблицы для всех общих элементов (Personal, Company) и специальные поля как Address для Company или LastName для Personal , скрыты в наследуемых таблицах. Тогда можно было бы использовать числовое Id. В случае Guid его можно использовать без надобности в общей таблицы, так как все id уникальные. В данном случае, потребовалось ссылаться жалобой на несколько элементов. Это можно было бы решить дополнительной таблицей, не ограничивая себя количеством ссылок на жалобные объекты. Но в данном случае потребовалось также возможность использовать ForeignKey, который позволял в ORM иметь дополнительные связи и с тем создавать сложные запросы.
Tag обычная реализация web 2.0 параллельных неиерархических ярлыков. Суть связок основано на идеи категоризации основанной на некоем списке названий почти не связанных друг с другом. Таким образом, любое множество тегов, может прикрепляться к множеству элементов. Здесь они используются для указания типов жалоб: на обслуживание и качество, серьозные, юридически выйгрышные дела или менее важные.
Платформенным уровнем доступа к базе есть встроенный в .net 3.5 – Linq to Sql. Linq to Sql – это one-to-one table mapper, тоесть работает он достаточно просто. Подключившись к серверу Sql, он получает метаданные структуры таблиц, на их основе создаёт xml файл с расширением dbml. Это промежуточный файл, содержащий дополнительную абстракцию, где можно производить например переименование таблиц. Затем существуют утилиты генераторы, создающие на основании dbml, cs классы с аттрибутами. Одним из классов, есть расширение класса DataContext, который относительно ADO.NET 2.0, являеться чем-то близким к симбиозу DataSet и DataAdapter. Он загружает данные, в форме сгенерированных классов абстракций таблиц, измененяя любые из них, мы производим трассировку изменений, что используеться при сохранении в автоматическом режиме. Также контекст отвечает за транзакции, логирование действий, конкурентные запросы.
Относительно особенностей веб-приложений: контекст не имеет смысла хранить дольше одного реквеста, наоборот это может приводить к неожиданным ошибка. Тоже и с хранением состояния. По сути, дилема заключаеться в том, какое количестве записей можно было бы провести в виде одной операции с базой. Большее количество значительно эффективней, против меньшего количества одновременных операций. Зато соответственно меньше разрозненных записей, меньше проблем с устаревшими, не добавленными данными, которых будут ожидать другие пользователи и правильностью последовательности ввода зависимых записей.
Опытным путём на данный момент я пришёл к выводу что оптимальным путём управления базой есть следующие несколько принципов:
1. Использовать HttpContext.Current.Context для хранения текущего контекста.
2. В Ctx создаётся singletone Model, через который идёт доступ к базе всем приложением. Описывается на Рисунок 2.4
3. Вместо стандартной схемы Рисунок 2.5, используется Рисунок 2.6.
4. Бизнес логика прописывается как методы partial классов от объектов.
public static MedData Model { get { if (HttpContext.Current.Items["Data_Context_Complaint"] == null) { HttpContext.Current.Items["Data_Context_Complaint"] = new MedData(); } return (MedData)HttpContext.Current.Items["Data_Context_Complaint"]; } } |
Рисунок 2.4 – Singletone доступа к базе
using(var ctx = new DataContext()) { User user = new User { Name = name } ctx.Users.InsertOnSubmit(user); ctx.Users.SubmitChanges(); } |
Рисунок 2.5 – Вариант использования контекста
User user = new User { Name = name } Ctx.Model.Users.InsertOnSubmit(user); Ctx.Model.Users.SubmitChanges(); |
Рисунок 2.6 – Вариант использования контекста, с Singletone (Рисунок. 2.4)
В большинстве часто используемых entity в базе данных, есть определённые сервисные поля, которые независимы от других полей и содержат похожую информацию. DateCreated, DateModified, UserCreated, UserModified, UrlName – могут не вводиться при каждом добавлении в бизнесс обьектах, они могут быть сгенерированы.
Перехват удобней всего производить в override версии SubmitChanges. Там можно перед произведением добавления или изменений, получать все обьекты и производить над ними операции.
namespace Yabeda.Med.Data { public partial class MedData { public override void SubmitChanges(System.Data.Linq.ConflictMode failureMode) { foreach(object ent in this.GetChangeSet().Inserts) { AutoValues auto = new AutoValues(ent); auto.AutoUrlName(); auto.AutoDateCreated(); auto.AutoDateModified(); auto.AutoUserCreated(); auto.AutoUserModified(); } foreach (object ent in this.GetChangeSet().Updates) { AutoValues auto = new AutoValues(ent); auto.AutoDateModified(); auto.AutoUserModified(); } base.SubmitChanges(failureMode); } } } |
Рисунок 2.7 – Переопределение SubmitChanges, для автозаполнения
Как видно, для Inserts, вставляються Created и UrlName значений, при Update, только Modified поля. AutoValues – это специальный класс принимающий обьект как конструктор, и производит с ним заполнения полей. Политика заполнения представляют собой осторожный подход. Поле вставляеться только в случае его существования и пустого значения внутри.
public object Entity { get; set; } public Type EntityType { get; set; } public AutoValues(object ent) { Entity = ent; EntityType = ent.GetType(); } |
private bool CheckIfValueExists(string property) { var name = EntityType.GetProperty(property); if (name == null) return true; object value = name.GetValue(Entity, null); if (value is Guid) { return ((Guid)value) != Guid.Empty; } return value != null; } |
Рисунок 2.8 – Проверка заполненности Reflection свойства
Данные функции широко используют Reflection механизмы. Для ValueType таких как Guid, производяться специальные проверки на пустоту значения.
private void AutoDefaultPut(string propertyName, object value) { var user = EntityType.GetProperty(propertyName); if (user == null) return; user.SetValue(Entity, value, null); } |
Рисунок 2.9 – Установка значения в Reflection свойство
AutoDefaultPut – проверяет указанное свойство на существование и заполняет его. Метод используеться AutoDate и AutoUser методами.
public void AutoDateModified() { AutoDefaultPut("DateModified", DateTime.UtcNow); } public void AutoUserCreated() { if (CheckIfValueExists("UserCreated")) return; AutoDefaultPut("UserCreated", Data.Ctx.User.UserId); } |
Рисунок 2.10 – Пример автозаполнения Id пользователя и DateTime.
Поле с датой заполняеться текущим временем в формате универсальном формате UTC. User заполняеться текущим UserId. В случае с Url происходят дополнительные проверки и конвертация через транслит метод.
public void AutoUrlName() { var name = EntityType.GetProperty("DisplayName"); var url = EntityType.GetProperty("UrlName"); if (url == null || name == null) return; string nameValue = name.GetValue(Entity, null) as string; string urlValue = url.GetValue(Entity, null) as string; if (nameValue != null && urlValue == null) { url.SetValue(Entity, nameValue.Translit(), null); } } |
Рисунок 2.11 – Заполнение Url, из расчёта на другое свойство
Это специальный созданный для данной системы транслит, удобный для внедрения в Url. Транслит сделан в виде Extension метода [extension метод в C# и статья из моего блога].
Так как множество данных в результате передаёться на клиент в форме json, то вполне не лишним может оказаться дополнительный уровень абстракции в виде набора классов для сериализации. По стилю кодированию они заканчиваются на Json и находяться среди моделей. Их задача есть передавать на клиент только нужную информацию без лишних зацикленных элементов.
В целом это обычные пустые классы, как пример снизу.
namespace Yabeda.Med.Mvc.Models.Company { public class ShortJson { public string name { get; set; } public string url { get; set; } public Guid id { get; set; } } } |
Рисунок 2.12 – Пример промежуточной структуры для модели данных
Они могут также содержать в одном из конструкторов аргументом класс Company из ORM модели для клонирования данных.
public ShortJson(Data.Company comp) { id = comp.CompanyId; name = comp.LongName; } |
Рисунок 2.13 – Конструктор заполнения структуры ShortJson
Конструктор как метод заполнения выбран не случайно, он удобен при создании объектов в Linq методе Select.
Company.All().Select(s => new Models.Company.ShortJson(s)); |
Рисунок 2.14 – Пример заполнение ShortJson
Кроме того, оказалось проблемой воспользоваться механизмом UrlRouting не внутри View, а внутри Action Controller. Для этого пришлось создавать специальную заглушку.
namespace Yabeda.Med.Mvc.Controllers { public class BaseController<C> : Controller where C : Controller { …. private UrlHelper _url; public UrlHelper Url { get { if (_url == null) { _url = new UrlHelper(this.ControllerContext.RequestContext); } return _url; } } protected override void OnActionExecuting(ActionExecutingContext filterContext) { Ctx.Url = Url; } … } } |
Рисунок 2.15 – Загрузка Ctx.Url
Таким образом в каждом Action контроллеров, которые наследуются от BaseController, есть возможность использования Ctx.Url. А в конструкторе CompanyJson создавать целый url.
url = Ctx.Url.Action("Show", "Company", new { name = comp.UrlName}); |
Рисунок 2.16 – Доступ к Ctx.Url
Любой пользователь, производящий запрос к странице, не имеющий в запросе cookies аутентификации, является для системы анонимом.