Как стать автором
Обновить

Микро-ORM одним классом

Время на прочтение4 мин
Количество просмотров9.6K
Здравствуйте, уважаемые хабравчане!

Хочу рассказать о собственной велонадстройке над ADO.NET.
Так как в большинстве своих проектов работу с данными я реализую в хранимых процедурах, идея создания надстройки появилась из-за необходимости чтения «сложных» результатов, например выборки из мастер-таблицы и нескольких подчиненных, а затем заполнения моделей этими данными.

Если кому-нибудь интересно, прошу под кат


Маппинг осуществляется за счет генерации IL кода для инициализации нужного типа из DataRecord и сохранения его в статическом словаре, что устраняет необходимость в повторной генерации кода инициализации для данного типа вне зависимости от инстанции самого DataManager'а, но с учетом вызываемой хранимой процедуры.

Примеры использования:

Создадим класс наследник, реализующий создание подключения к БД:
class MSSqlDataManager : DataManager 
{
    public MSSqlDataManager() : base(new SqlConnection("ConnectionString here")) { }
}


Выборка простого набора данных:
public class Product
{
        public int Id { get; set; }
        public string Name { get; set; }
        public string Description { get; set; }
        public int? Price { get; set; }
}
...
using (var dm = new MSSqlDataManager())
{
    List<Product> res = dm.Procedure("Test").GetList<Product>();
}

где хранимка Test выбирает данные, например, такого вида:
SELECT p.Id, p.Name, p.[Description], p.Price
    FROM dbo.Product p

Чтение данных из
хранимки,
SELECT 
		p.Id
		, p.Name, 
		, p.[Description]
		, p.Price
		, StorageId = s.Id
		, StorageName = s.Name
    FROM dbo.Product p 
    INNER JOIN dbo.Storages s ON s.Id = p.StorageId
    WHERE p.Id = @Id;
    
    SELECT 
		c.Id
		, c.Body
		, c.WriteDate
		, UserId = u.Id
		, UserName = u.Name
		, UserLocationId = l.Id
		, UserLocationName = l.Name
                          , c.ProductId
    FROM dbo.Comments c 
    INNER JOIN dbo.Users u ON u.Id = c.UserId
    INNER JOIN dbo.Locations l ON l.Id = u.LocationId 
    WHERE c.ProductId = @Id;


выбирающей одну главную запись и несколько подчиненных:
Product res = dm.Procedure("Test").AddParams(new { id = 10 }).Get<Product, ProductComment>(p => p.Comments);

где структура классов моделей
такова
public class UserLocation
    {
        public int Id { get; set; }
        public string Name { get; set; }
    }

    public class UserModel
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public UserLocation Location { get; set; }

        public UserModel()
        {
            this.Location = new UserLocation();
        }
    }
    
    public class ProductComment
    {
        public int Id { get; set; }
        public string Body { get; set; }
        public DateTime WriteDate { get; set; }
        public UserModel User { get; set; }
        public int ProductId { get; set; }

        public ProductComment()
        {
            this.User = new UserModel();
        }
    }

    public class ProductStorage
    {
        public int Id { get; set; }
        public string Name { get; set; }
    }

    public class Product
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public string Description { get; set; }
        public int? Price { get; set; }
        public ProductStorage Storage { get; set; }
        public List<ProductComment> Comments { get; set; }

        public Product()
        {
            this.Storage = new ProductStorage();
            this.Comments = new List<ProductComment>();
        }
    }


Обратите внимание — кроме основных свойств класса инициализируются свойства и вложенных классов-свойств. Для этого необходимо в выборке присваивать именам столбцов правильное наименование, отражающее вложенность — например для UserLocationName позволяет мапперу найти в объекте, для которого маппинг производится (типа ProductComment в данном случае), свойство User, в нем Location и уже нужное нам Name.

Дальше — больше. Получение нескольких главных записей с подчиненными:
List<Product> res = dm.Procedure("Test")
      .GetList<Product, ProductComment>(
        (parents, detail)=>parents.First(p => p.Id == detail.ProductId).Comments
      );

Всего я объявил по четыре перегруженных метода для получения записей один ко многим и многие ко многим, позволяющих чтение до четырех наборов подчиненных записей. В случае если подчиненных наборов больше (что довольно редко встречается), можно добавить еще перегрузок, или воспользоваться другим методом:
List<Product> res = dm.Procedure("Test")
  .GetList<Product>(
    (dr, parents) => { 
        parents.Where(p=>p.Id == (int)dr["ProductId"]).First().Comments
            .Add(dm.Create<ProductComment>(dr)); 
    },
    (dr, parents) => {  },
    ...
);

Ну а если модель данных не подходит под вышеуказанные шаблоны, можно воспользоваться методом Raw — он принимает лямбду, в которой доступен IDataReader, которым можно пользоваться по ситуации.
dm.Procedure("Test")
  .Raw(dr =>
  {
    while (dr.Read())
    {
      ...
    }
});

Конечно же, присутствуют методы получения скалярного значения и старый-добрый Execute.
Добавление параметров для хранимки осуществляется вызовом AddParams
dm.AddParams(new { id = 10, name = "stringparam", writeDate = DateTime.Now }) ...

Так же реализован метод для передачи табличных параметров (Table-Valued Parameters) — конечно же он работает только для MS SQL Server начиная с 2008 версии.
dm.AddEnumerableParam("Details",
                    Enumerable.Range(1, 10)
                        .Select(e => new {id = e, name = string.Concat("Name", e.ToString())})
                    );


По производительности совсем немного отстаем от Dapper, особенно при певром вызове инициализатора объекта.
В планах — реализовать поддержку IQueryable-результатов с передачей параметров в хранимку, что очень пригодилось бы в ApiController'aх AspNet MVC.

Если кому то будет интересен данный велосипед, код библиотеки доступен на github.
Спасибо за внимание!
Теги:
Хабы:
Всего голосов 18: ↑17 и ↓1+16
Комментарии5

Публикации

Работа

.NET разработчик
46 вакансий

Ближайшие события