Третья статья про сравнение различных ORM для .NET, ранее успели ознакомиться с настройкой и работой следующих ORM:
В этой статье ознакомимся в Castle ActiveRecord. Как понятно из названия, ORM реализует паттерн ActiveRecord, то есть вся работа с объектом ведётся через его методы и статические методы класса, к которому относится объект. Построена ORM на основе Nhibernate 3-й версии, параметры отображения свойств класса на запись в БД задаются с помощью атрибутов класса и свойств.
Пара слов в напоминание способа изучения работы фреймворка: есть веб-сайт условного сервисного центра по ремонту различной техники, работающего на ASP.NET MVC 4, база данных - MS SQL 2008, структура базы данных состоит из 6 таблиц, описывающих главным образом свойства заявки на ремонт техники.
Структура таблиц:
Загрузить Castle ActiveRecord можно на GitHub: https://github.com/castleproject/ActiveRecord
Настройка
Первым делом убедимся что наш web.config содержит строку подключения
<connectionStrings>
<addname="ServiceCenter"connectionString="Server=localhost\mssql2k8r2;Database=ServiceCenter;Integrated Security=SSPI"providerName="System.Data.SqlClient"/>
</connectionStrings>
Далее, так как мы работаем в веб-приложении, добавим запись, избавляющую нас от ручного создания сессии на каждый запрос к базе данных
<system.web>
...
<httpModules>
<addname="ar.sessionscope"type="Castle.ActiveRecord.Framework.SessionScopeWebModule, Castle.ActiveRecord.Web" />
</httpModules>
...
</system.web>
Следующая задача - настройка самого фреймворка. Есть несколько способов настройки: через XML-Файл или жестко забить настройки в код программы, рассмотрим оба варианта.
Через XML-файл
<?xmlversion="1.0"encoding="utf-8" ?>
<activerecord
isWeb="true"
isDebug="false"
pluralizeTableNames="false">
<configdatabase="MsSqlServer2008"connectionStringName="ServiceCenter">
</config>
<config>
<addkey="connection.driver_class" value="NHibernate.Driver.SqlClientDriver" />
<addkey="dialect" value="NHibernate.Dialect.MsSql2008Dialect" />
<addkey="connection.provider" value="NHibernate.Connection.DriverConnectionProvider" />
<addkey="proxyfactory.factory_class" value="NHibernate.ByteCode.Castle.ProxyFactoryFactory,NHibernate.ByteCode.Castle" />
<!-- Use only one of the two attributes below -->
<!--<add key="connection.connection_string" value="connection string" />-->
<addkey="connection.connection_string_name"value="ServiceCenter" />
</config>
</activerecord>
Загружается XML-конфиг следующим образом
XmlConfigurationSource source = new XmlConfigurationSource(System.IO.Path.Combine(System.IO.Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().CodeBase), "source.xml"));
source.DefaultFlushType = DefaultFlushType.Auto;
source.VerifyModelsAgainstDBSchema = true;
ActiveRecordStarter.Initialize(source, typeof(DeviceType), typeof(Employee), typeof(Manufacturer), typeof(Position), typeof(Request), typeof(Status));
В коде программы
Dictionary<string, string> properties = new Dictionary<string, string>();
properties.Add("connection.driver_class", "NHibernate.Driver.SqlClientDriver");
properties.Add("dialect", "NHibernate.Dialect.MsSql2000Dialect");
properties.Add("connection.provider", "NHibernate.Connection.DriverConnectionProvider");
properties.Add("proxyfactory.factory_class", "NHibernate.ByteCode.Castle.ProxyFactoryFactory,NHibernate.ByteCode.Castle");
properties.Add("connection.connection_string", System.Configuration.ConfigurationManager.ConnectionStrings["ServiceCenter"].ConnectionString);
InPlaceConfigurationSource source = new InPlaceConfigurationSource();
source.DefaultFlushType = DefaultFlushType.Auto;
source.IsRunningInWebApp = true;
source.PluralizeTableNames = false;
source.VerifyModelsAgainstDBSchema = true;
source.Add(typeof(ActiveRecordBase), properties);
ActiveRecordStarter.Initialize(source, typeof(DeviceType), typeof(Employee), typeof(Manufacturer), typeof(Position), typeof(Request), typeof(Status));
Чтобы загрузить конфигурацию создадим класс DataService, добавим в него статический метод Initialize(), запишем в него выбранный способ загрузки конфига, а в файл Global.asax основного проекта добавим вызов этого метода.
DataService.cs
#region Using
using System;
using System.Collections;
using System.Collections.Generic;
using System.Data;
using System.Data.SqlClient;
using System.Linq;
using Castle.ActiveRecord;
using Castle.ActiveRecord.Framework;
using Castle.ActiveRecord.Framework.Config;
#endregion
public static class DataService
{
public static void Initialize()
{
//Dictionary<string, string> properties = new Dictionary<string, string>();
//properties.Add("connection.driver_class", "NHibernate.Driver.SqlClientDriver");
//properties.Add("dialect", "NHibernate.Dialect.MsSql2000Dialect");
//properties.Add("connection.provider", "NHibernate.Connection.DriverConnectionProvider");
//properties.Add("proxyfactory.factory_class", "NHibernate.ByteCode.Castle.ProxyFactoryFactory,NHibernate.ByteCode.Castle");
//properties.Add("connection.connection_string", System.Configuration.ConfigurationManager.ConnectionStrings["ServiceCenter"].ConnectionString);
//InPlaceConfigurationSource source = new InPlaceConfigurationSource();
//source.DefaultFlushType = DefaultFlushType.Auto;
//source.IsRunningInWebApp = true;
//source.PluralizeTableNames = false;
//source.VerifyModelsAgainstDBSchema = true;
//source.Add(typeof(ActiveRecordBase), properties);
XmlConfigurationSource source = new XmlConfigurationSource(System.IO.Path.Combine(System.IO.Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().CodeBase), "source.xml"));
source.DefaultFlushType = DefaultFlushType.Auto;
source.VerifyModelsAgainstDBSchema = true;
ActiveRecordStarter.Initialize(source, typeof(DeviceType), typeof(Employee), typeof(Manufacturer), typeof(Position), typeof(Request), typeof(Status));
}
}
Global.asax.cs
public class MvcApplication : System.Web.HttpApplication
{
protected void Application_Start()
{
...
ServiceCenter.Data.CastleActiveRecord.DataService.Initialize();
}
}
Классы
Базовый класс ActiveRecordBase, от которого наследуются наши классы, предоставляет основные методы для работы с БД, остаётся добавить только пару недостающих нам методов.
Для примера рассмотрим пару рабочих классов для связи с БД
Сотрудник (Employee)
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using Castle.ActiveRecord;
///<summary>
/// Сотрудник
///</summary>
[ActiveRecord("Employee", Schema = "dbo", UseAutoImport = false),
JoinedTable("Position", Column = "PositionId", Fetch = FetchEnum.Unspecified),
DisplayName("Сотрудник"),
DisplayColumn("FullName")]
public class Employee : ActiveRecordBase
{
#region Свойства
///<summary>
/// Ключ
///</summary>
[Key, PrimaryKey(PrimaryKeyType.Native, Column = "EmployeeId"), DisplayName("Id")]
public int Id { get; set; }
///<summary>
/// Имя
///</summary>
[Property(NotNull = true, Length = 50, ColumnType = "String"),
DisplayName("Имя"),
DisplayFormat(NullDisplayText = "Наименование не может быть пустым"),
Required(ErrorMessage = "Имя не может быть пустым"),
DataType(DataType.Text),
StringLength(50, ErrorMessage = "Максимум 50 символов")]
public string Name { get; set; }
///<summary>
/// Фамилия
///</summary>
[Property(NotNull = false, Length = 50, ColumnType = "String"),
DisplayName("Фамилия"),
DataType(DataType.Text),
StringLength(50, ErrorMessage = "Максимум 50 символов")]
public string SurName { get; set; }
///<summary>
/// Отчество
///</summary>
[Property(NotNull = false, Length = 50, ColumnType = "String"),
DisplayName("Отчество"),
DataType(DataType.Text),
StringLength(50, ErrorMessage = "Максимум 50 символов")]
public string MidName { get; set; }
///<summary>
/// Должность
///</summary>
[DisplayName("Должность"),
BelongsTo(Column = "PositionId", NotFoundBehaviour = NotFoundBehaviour.Exception, NotNull = true, Type = typeof(Position))]
public Position Position { get; set; }
///<summary>
/// Полное имя
///</summary>
[DisplayName("Ф.И.О.")]
public string FullName
{
get
{
string fullName = this.Name;
if (!string.IsNullOrEmpty(this.SurName) && !string.IsNullOrEmpty(this.MidName))
fullName = string.Format("{0} {1}.{2}.", this.SurName, this.Name[0], this.MidName[0]);
else if (!string.IsNullOrEmpty(this.SurName))
fullName = string.Format("{0} {1}", this.SurName, this.Name);
return fullName;
}
}
///<summary>
/// Заявки
///</summary>
[HasMany(MapType = typeof(Request), ColumnKey = "EmployeeId", Lazy = true, ExtraLazy = true), DisplayName("Заявки")]
public IList<Request> Requests { get; set; }
#endregion
#region Общие методы
///<summary>
/// Возвращает все записи
///</summary>
public static List<Employee> FindAll()
{
//using (new Castle.ActiveRecord.TransactionScope(TransactionMode.New))
return new List<Employee>((Employee[])ActiveRecordMediator.FindAll(typeof(Employee)));
}
///<summary>
/// Возвращает запись по её Id
///</summary>
///<param name="id">Id записи в БД</param>
public static Employee Find(int id)
{
return ActiveRecordMediator<Employee>.FindByPrimaryKey(id, false);
}
#endregion
}
Заявка (Request)
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using Castle.ActiveRecord;
///<summary>
/// Заявка
///</summary>
[ActiveRecord("Request", Schema = "dbo", UseAutoImport = false), DisplayName("Заявка")]
public class Request : ActiveRecordBase
{
#region Свойства
///<summary>
/// Ключ
///</summary>
[Key, PrimaryKey(PrimaryKeyType.Native, Column = "RequestId"), DisplayName("Id")]
public int Id { get; set; }
///<summary>
/// Ответственный/исполнитель
///</summary>
[DisplayName("Ответственный/исполнитель"),
BelongsTo(Column = "EmployeeId", NotFoundBehaviour = NotFoundBehaviour.Exception, NotNull = true, Type = typeof(Employee))]
public Employee Employee { get; set; }
///<summary>
/// Дата приёма
///</summary>
[Property(NotNull = true),
DisplayName("Дата приёма"),
DisplayFormat(ApplyFormatInEditMode = true, DataFormatString = "dd.MM.yyyy", NullDisplayText = "Дата приёма не может быть пустой"),
DataType(DataType.Date)]
public DateTime DateIn { get; set; }
///<summary>
/// Дата выдачи
///</summary>
[Property(NotNull = false),
DisplayName("Дата выдачи"),
DisplayFormat(ApplyFormatInEditMode = true, DataFormatString = "dd.MM.yyyy", ConvertEmptyStringToNull = true),
DataType(DataType.Date)]
public DateTime? DateOut { get; set; }
///<summary>
/// Тип устройства
///</summary>
[DisplayName("Тип устройства"),
BelongsTo(Column = "DeviceTypeId", NotFoundBehaviour = NotFoundBehaviour.Exception, NotNull = true, Type = typeof(DeviceType))]
public DeviceType DeviceType { get; set; }
///<summary>
/// Производитель
///</summary>
[DisplayName("Производитель"),
BelongsTo(Column = "ManufacturerId", NotFoundBehaviour = NotFoundBehaviour.Exception, NotNull = true, Type = typeof(Manufacturer))]
public Manufacturer Manufacturer { get; set; }
///<summary>
/// Модель
///</summary>
[Property(NotNull = true, Length = 50, ColumnType = "String"),
DisplayName("Модель"),
DisplayFormat(NullDisplayText = "Название модели не может быть пустым"),
Required(ErrorMessage = "Название модели не может быть пустым"),
DataType(DataType.Text)]
public string Model { get; set; }
///<summary>
/// Описание неисправности
///</summary>
[Property(NotNull = true, Length = 250, ColumnType = "String"),
DisplayName("Описание неисправности"),
DisplayFormat(NullDisplayText = "Описание не может быть пустым"),
Required(ErrorMessage = "Описание не может быть пустым"),
DataType(DataType.MultilineText),
StringLength(250, ErrorMessage = "Максимум 250 символов")]
public string Description { get; set; }
///<summary>
/// Ф.И.О. клиента
///</summary>
[Property(NotNull = true, Length = 150, ColumnType = "String"),
DisplayName("Ф.И.О. клиента"),
DisplayFormat(NullDisplayText = "Ф.И.О. клиента не может быть пустым"),
Required(ErrorMessage = "Ф.И.О. клиента не может быть пустым"),
DataType(DataType.Text),
StringLength(150, ErrorMessage = "Максимум 150 символов")]
public string ClientName { get; set; }
///<summary>
/// Телефон клиента
///</summary>
[Property(NotNull = false, Length = 50, ColumnType = "String"),
DisplayName("Телефон клиента"),
DataType(DataType.Text),
StringLength(50, ErrorMessage = "Максимум 50 символов")]
public string ClientPhone { get; set; }
///<summary>
/// Стоимость ремонта
///</summary>
[Property(NotNull = true),
DisplayName("Стоимость ремонта"),
Required(ErrorMessage = "Поле 'Стоимость ремонта' не может быть пустым"),
Range(0, 10000, ErrorMessage = "Введите значение от 0 до 10000")]
public int Cost { get; set; }
///<summary>
/// Статус заявки
///</summary>
[DisplayName("Статус заявки"),
BelongsTo(Column = "StatusId", NotFoundBehaviour = NotFoundBehaviour.Exception, NotNull = true, Type = typeof(Status))]
public Status Status { get; set; }
///<summary>
/// Комметарий исполнителя
///</summary>
[Property(NotNull = false, Length = 250, ColumnType = "String"),
DisplayName("Комметарий исполнителя"),
DataType(DataType.MultilineText),
StringLength(250, ErrorMessage = "Максимум 250 символов")]
public string Comment { get; set; }
#endregion
#region Общие методы
///<summary>
/// Возвращает все записи
///</summary>
public static List<Request> FindAll()
{
//using (new Castle.ActiveRecord.TransactionScope(TransactionMode.New))
return new List<Request>((Request[])ActiveRecordMediator.FindAll(typeof(Request)));
}
///<summary>
/// Возвращает запись по её Id
///</summary>
///<param name="id">Id записи в БД</param>
public static Request Find(int id)
{
return ActiveRecordMediator<Request>.FindByPrimaryKey(id, false);
}
#endregion
#region Overrides
///<summary>
/// Сохранить новый объект в БД
///</summary>
public override void Create()
{
this.DateIn = DateTime.Now;
base.Create();
}
///<summary>
/// Сохранить в БД
///</summary>
public override void Save()
{
if (this.Status.Id == 5 || this.Status.Id == 6)
this.DateOut = DateTime.Now;
else
this.DateOut = null;
base.Save();
}
#endregion
}
Описание использованных атрибутов:
ActiveRecord - задаёт таблицу для сопоставления
PrimaryKey - задаёт первичный ключ таблицы
Property - свойство
BelongsTo - задаёт связную таблицу по указанному ключу
HasMany - задаёт связь один-ко-многим
Контроллеры/представления
В общем ничего, что следовало бы описывать, тут нет. Для примера контроллер EmployeeController
#region Using
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web.Mvc;
using Data = ServiceCenter.Data.CastleActiveRecord;
#endregion
public class EmployeeController : Controller
{
#region Список
public ActionResult Index()
{
ViewBag.Title = "Сотрудники";
List<Data.Employee> employees = Data.Employee.FindAll().OrderBy(x => x.SurName).ThenBy(x => x.Name).ToList();
return View(employees);
}
#endregion
#region Создание
public ActionResult Create()
{
ViewBag.Title = "Сотрудники - добавление записи";
return View();
}
[HttpPost]
public ActionResult Create(Data.Employee employee, FormCollection collection)
{
ViewBag.Title = "Сотрудники - добавление записи";
int positionId = 0;
if (!Int32.TryParse(collection["PositionId"], out positionId))
ModelState.AddModelError("Position", "Выберите должность!");
if (!ModelState.IsValid)
return View(employee);
employee.Position = Data.Position.Find(positionId);
try
{
employee.Create();
return RedirectToAction("Index");
}
catch (Exception ex)
{
ModelState.AddModelError("", string.Format("При создании записи возникла ошибка:{0}{1}", Environment.NewLine, ex.Message));
return View(employee);
}
}
#endregion
#region Редактирование
public ActionResult Edit(int id)
{
ViewBag.Title = "Сотрудники - редактирование записи";
Data.Employee employee = Data.Employee.Find(id);
ViewBag.Message = employee.FullName;
return View(employee);
}
[HttpPost]
public ActionResult Edit(Data.Employee employee, FormCollection collection)
{
ViewBag.Title = "Сотрудники - редактирование записи";
ViewBag.Message = employee.FullName;
int positionId = 0;
if (!Int32.TryParse(collection["PositionId"], out positionId))
ModelState.AddModelError("Position", "Выберите должность!");
if (!ModelState.IsValid)
return View(employee);
employee.Position = Data.Position.Find(positionId);
try
{
employee.Save();
return RedirectToAction("Index");
}
catch (Exception ex)
{
ModelState.AddModelError("", string.Format("При редактировании записи возникла ошибка:{0}{1}", Environment.NewLine, ex.Message));
return View(employee);
}
}
#endregion
#region Удаление
public ActionResult Delete(int id)
{
Data.Employee employee = Data.Employee.Find(id);
employee.Delete();
return RedirectToAction("Index");
}
#endregion
#region DropDown
public ActionResult GetEmployeeDropDown(int selectedId)
{
ViewData.Model = Data.Employee.FindAll().OrderBy(x => x.SurName).ThenBy(x => x.Name).Select(x => new SelectListItem()
{
Text = x.FullName,
Value = x.Id.ToString(),
Selected = x.Id == selectedId
});
ViewData.ModelMetadata = new ModelMetadata(ModelMetadataProviders.Current, null, null, typeof(int), "EmployeeId")
{
NullDisplayText = "выберите сотрудника"
};
return View("DropDown");
}
#endregion
}
View практически не изменились с предыдущей статьи, для примера view редактирования сотрудника
@model ServiceCenter.Data.CastleActiveRecord.Employee
@using (Html.BeginForm()) {
@Html.ValidationSummary(true)
<fieldset>
@Html.HiddenFor(model => model.Id)
<div class="row uniform 50%">
<div class="2u 12u$(xsmall)">
@Html.LabelFor(model => model.SurName)
</div>
<div class="4u$ 12u$(xsmall)">
@Html.EditorFor(model => model.SurName)
</div>
<div class="6u$ 12u$(xsmall)">
@Html.ValidationMessageFor(model => model.SurName)
</div>
</div>
<div class="row uniform 50%">
<div class="2u 12u$(xsmall)">
@Html.LabelFor(model => model.Name)
</div>
<div class="4u$ 12u$(xsmall)">
@Html.EditorFor(model => model.Name)
</div>
<div class="6u$ 12u$(xsmall)">
@Html.ValidationMessageFor(model => model.Name)
</div>
</div>
<div class="row uniform 50%">
<div class="2u 12u$(xsmall)">
@Html.LabelFor(model => model.MidName)
</div>
<div class="4u$ 12u$(xsmall)">
@Html.EditorFor(model => model.MidName)
</div>
<div class="6u$ 12u$(xsmall)">
@Html.ValidationMessageFor(model => model.MidName)
</div>
</div>
<div class="row uniform 50%">
<div class="2u 12u$(xsmall)">
@Html.LabelFor(model => model.Position)
</div>
<div class="4u$ 12u$(xsmall)">
@{ Html.RenderAction("GetPositionDropDown", "Position", new { selectedId = Model.Position.Id }); }
</div>
<div class="6u$ 12u$(xsmall)">
@Html.ValidationMessageFor(model => model.Position)
</div>
</div>
<br />
<ul class="actions">
<li><input type="submit" value="Сохарнить" class="special" /></li>
<li><input type="reset" value="Сбросить" /></li>
</ul>
</fieldset>
}
<p>
@Html.ActionLink("Назад к списку сотрудников", "Index", null, new { @class = "button alt small" })
</p>
Во вложении находятся проект и база данных