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

В данной статье будут рассмотрены возможности представлений для устранения данных недостатков.

В первую очередь рассмотрим типизацию и гибкость выходных данных. В конце будет продемонстрирована возможность создания абстрактного движка для сортировки элементов через as.crud.

Строитель модели EntittyFramework способен автоматически сгенерировать сущности БД в виде прикладных объектов. Это правило справедливо как для таблиц, так и для их представлений. Эти объекты представляются как DbSet на прикладном уровне, которые поддерживают IQueryable, а значит, на прикладном уровне можно уточнить запрос к этим сущностям – фильтр, проекция полей, пагинация.

Рассмотрим произвольную Get-хранимку (детали опущены):

ALTER PROCEDURE [dbo].[GetCountMakeAndModel]
@modelPropertyId int,
@publishedStatusId int,
@parentCategoryId int
AS
BEGIN
WITH groupedMakeModel
AS
(
    SELECT SUM(COUNT(*)) OVER (PARTITION BY ads.categoryID) AS MakeCount,
        COUNT(*) as ModelCount,
        ads.categoryID AS CategoryId,
        category.name AS CategoryName,
        (CASE WHEN avModel.value IS NULL THEN pvModel.value ELSE avModel.value END) AS Model,
        image.path AS ImagePath
    FROM cm_advertisements ads
    INNER JOIN cm_categories category on category.id = ads.categoryID
    LEFT JOIN cm_advertisementValues avModel ON avModel.advertisementID = ads.id AND avModel.propertyID = @modelPropertyId
    LEFT JOIN cm_propertyValuesString pvModel ON pvModel.configurationID = ads.configurationID AND pvModel.propertyID = @modelPropertyId
    LEFT JOIN cm_images image on category.imageID = image.id
    WHERE ads.isReady = 1
        AND statusID = @publishedStatusId
        AND ads.isPart = 0
        AND category.parentID = @parentCategoryId
    GROUP BY ads.categoryID, category.name, CASE WHEN avModel.value IS NULL THEN pvModel.value ELSE avModel.value END, image.path
),
c
AS
(
    SELECT *, DENSE_RANK() OVER(ORDER BY MakeCount DESC) AS CategoryNumber
    FROM groupedMakeModel
)
SELECT MakeCount, CategoryId, CategoryName, ModelCount, Model, ImagePath
FROM c
WHERE CategoryNumber <= 15
ORDER BY MakeCount DESC, CategoryName, ModelCount DESC, Model;
END

Обычная выборка с кучей JOIN и фильтром, структура которого задается жестко, лишь параметры гибкие.

Как эта хранимая процедура типизируется на прикладном уровне? Вручную пишется ViewModel, структура которой должна четко соответствовать структуре результата хранимой процедуры.

 db.GetSQLDataTyped<MakeModelItem>("GetCountMakeAndModel", p, CommandType.StoredProcedure)

 private class MakeModelItem
        {
            public int MakeCount { get; set; }
            public int ModelCount { get; set; }
            public int CategoryId { get; set; }
            public string CategoryName { get; set; }
            public string Model { get; set; }
            public string ImagePath { get; set; }
        }

Вернемся к SQL коду и порассуждаем чисто в терминах БД. Почему для единичного SELECT-запроса делается хранимая процедура? Зачем ограничивать структуру фильтрации? Специально для подобного случая существуют представления SQL:

Представление выполняет функцию фильтра базовых таблиц, на которые оно ссылается. (c) msdn

 

Далее будет продемонстрировано аналогичное решение, но с помощью представлений.

IF OBJECT_ID('dbo.vw_psl_parcelServices', 'V') IS NOT NULL
    DROP VIEW dbo.vw_psl_parcelServices;
GO

CREATE VIEW dbo.vw_psl_parcelServices
AS
		SELECT	
				p.id						as 'ID'	
		,		p.name						as 'ParcelName'
		,		1							as 'IsIncoming'
		,		ost.code					as 'OrderStatusCode'
		,		ost.name					as 'OrderStatusName'
		-- HACK
		,		p.outgoingParcelID			as 'OutgoingParcelID' 
		,		pt.code						as 'ParcelType'
		,		p.parentID					as 'ParentID'		
		,		o.comment					as 'Comment'
		,		''							as 'GiftComment'
		,		vp.MainSrc					as 'MainSrc'
		,		s.*
		FROM	psl_incomingParcels p
		LEFT JOIN	vw_psl_incomingParcels vp
			ON vp.ID = p.id			
		LEFT JOIN	psl_service_incomingParcels	ps
			ON	ps.incomingParcelID		= p.id						
		LEFT JOIN	vw_psl_services s
			ON	ps.serviceID				= s.ServiceID
		LEFT JOIN	psl_parcelTypes	pt	
			ON pt.id						= p.parcelTypeID
		LEFT JOIN psl_expectedParcelOrders epo
			ON epo.id							= p.orderID
		LEFT JOIN psl_orders o
			ON epo.orderID							= o.id
		LEFT JOIN psl_orderStatuses	ost			
			ON ost.id							= o.orderStatusID

		
	UNION	ALL
		SELECT	
				p.id						as 'ID'	
		,		''							as 'ParcelName'
		,		0							as 'IsIncoming'
		,		ost.code					as 'OrderStatusCode'
		,		ost.name					as 'OrderStatusName'
		-- HACK
		,		NULL						as 'OutgoingParcelID' 
		,		'OutgoingParcel'			as 'ParcelType'
		,		NULL						as 'ParentID'
		,		o.comment
											as 'Comment'
		,		p.giftDesc					as 'GiftComment'
		,		''							as 'MainSrc'
		,		s.*
		FROM	psl_outgoingParcels p
		LEFT JOIN	psl_outgoingParcels vp
			ON vp.ID = p.id
		LEFT JOIN	psl_service_outgoingParcels	ps
			ON	ps.outgoingParcelID			= p.id						
		LEFT JOIN	vw_psl_services s
			ON	ps.serviceID				= s.ServiceID
		LEFT JOIN psl_orders o
			ON o.id			= p.orderID
		LEFT JOIN psl_orderStatuses	ost			
			ON ost.id						= o.orderStatusID
GO

-- test
select	* 
from	vw_psl_parcelServices
order by ID DESC;

В данном представлении, аналогично, лежит куча JOIN, плюс есть даже немного логики для определения отношения записи к определенному множеству – согласно бизнес-правилам. Более того, заданная структура автоматически импортируется в прикладной уровень в таком виде:

 public partial class vw_psl_parcelServices
    {
        public int ID { get; set; }
        public string ParcelName { get; set; }
        public int IsIncoming { get; set; }
        public string OrderStatusCode { get; set; }
        public string OrderStatusName { get; set; }
        public Nullable<int> OutgoingParcelID { get; set; }
        public string ParcelType { get; set; }
        public Nullable<int> ParentID { get; set; }
        public string Comment { get; set; }
        public string GiftComment { get; set; }
        public string MainSrc { get; set; }
        public Nullable<int> ServiceID { get; set; }
        public string ServiceName { get; set; }
        public string ServiceDesctiption { get; set; }
        public Nullable<decimal> ServiceMinPrice { get; set; }
        public Nullable<decimal> ServiceMaxPrice { get; set; }
        public Nullable<int> ServiceHavePrecisePrice { get; set; }
        public Nullable<decimal> ServicePrice { get; set; }
        public Nullable<int> ServiceTypeID { get; set; }
        public string ServiceTypeCode { get; set; }
        public string ServiceTypeName { get; set; }
        public Nullable<int> ParentServiceTypeID { get; set; }
        public string ParentServiceTypeCode { get; set; }
        public string ParentServiceTypeName { get; set; }
        public Nullable<int> ServiceCategoryID { get; set; }
        public string ServiceCategoryName { get; set; }
        public string ServiceCategoryCode { get; set; }
        public Nullable<int> ParentServiceCategoryID { get; set; }
        public string ParentServiceCategoryName { get; set; }
        public string ParentServiceCategoryCode { get; set; }
        public string ServiceSrc { get; set; }
        public string ServiceAlt { get; set; }
        public string ServiceTitle { get; set; }
    }

И использование представления выглядит так:

// общая логика
 public List<vw_psl_parcelServices> GetParcelServices(ParcelServiceFilter filter)
 {
 var res = repository.db.vw_psl_parcelServices.AsNoTracking().AsQueryable()
 .Where(x => x.OrderStatusCode != "canceled")
 .Where(x => x.OrderStatusCode != "cancelled")
 .Where(x => x.OrderStatusCode != "deleted");

 if (filter.Precise.IsFilled)
 {
 return res.Where(x => x.IsIncoming == (filter.Precise.IsIncoming.Value ? 1 : 0))
 .Where(x => x.ID == filter.Precise.Id)
 .ToList();
 }
 
 if (filter.OutgoingParcelID.HasValue)
 {
 res = res.Where(x => x.OutgoingParcelID == filter.OutgoingParcelID && x.IsIncoming == 1 && x.ParcelType == "product") ;
 }

 if (filter.IdList != null && filter.IdList.Length != 0)
 {
 res = res.Where(x => filter.IdList.Contains(x.ID));
 }

 return res.ToList();
 }

// Использование
 public List<vw_psl_parcelServices> GetParcelServices(ParcelServiceFilter filter)
 {
 var res = repository.db.vw_psl_parcelServices.AsNoTracking().AsQueryable()
 .Where(x => x.OrderStatusCode != "canceled")
 .Where(x => x.OrderStatusCode != "cancelled")
 .Where(x => x.OrderStatusCode != "deleted");

 if (filter.Precise.IsFilled)
 {
 return res.Where(x => x.IsIncoming == (filter.Precise.IsIncoming.Value ? 1 : 0))
 .Where(x => x.ID == filter.Precise.Id)
 .ToList();
 }
 
 if (filter.OutgoingParcelID.HasValue)
 {
 res = res.Where(x => x.OutgoingParcelID == filter.OutgoingParcelID && x.IsIncoming == 1 && x.ParcelType == "product") ;
 }

 if (filter.IdList != null && filter.IdList.Length != 0)
 {
 res = res.Where(x => filter.IdList.Contains(x.ID));
 }

 return res.ToList();
 }

 public List<vw_psl_parcelServices> GetIncomingParcelServices(params int[] id)
 {
 var extra = new ParcelServiceFilter();
 extra.IdList = id;
 return GetParcelServices(extra);
 }

 public List<vw_psl_parcelServices> GetIncomingParcelServices(int id, ParcelServiceFilter extra = null)
 {
 extra = extra ?? new ParcelServiceFilter();
 extra.Precise.Id = id;
 extra.Precise.IsIncoming = true;

 return GetParcelServices(extra);
 }

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

Абстрагирование

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

switch (paging.Sort)
{
// powered by regexps =)
        case "ID": orderedQuery = query.OrderBy(x => x.ID, paging.Direction); break;
	case "Cost": orderedQuery = query.OrderBy(x => x.Cost, paging.Direction); break;
	case "Size": orderedQuery = query.OrderBy(x => x.Size, paging.Direction); break;
	case "Name": orderedQuery = query.OrderBy(x => x.Name, paging.Direction); break;
// и еще N кейсов
}

В качестве входных параметров – поле для сортировки и направление. Это берется напрямую из аякс-запроса который присылает as.crud. Изящное решение напрашивается само собой – с помощью отражения определить по какому полю требуется сортировка и выполнить эту сортировку. Однако, это уже тема для движка представлений.

В следующих статьях будут рассмотрены примененные здесь практики написания представлений и проработка решения для абстрактного движка выборки представлений

Периодически мы проводим обучение и даем возможность стажировки на базе нашей платформы с управлением на SQL. Если вам это интересно, то пожалуйста посмотрите информацию об обучении/стажировке по SQL.