Showing posts with label criteria. Show all posts
Showing posts with label criteria. Show all posts

Jan 6, 2011

Episerver CMS 6 search

Had to make some additional staff in EPiServer search functionality. I had to add ability to filter pages by category, page type, and keyword in content. Firstly I have tried to do it with DataFactory.Instance.FindPagesWithCriteria method. But the thing is that I couldn’t  find a way to filter data by search keyword.

I knew that SearchDataSource does this somehow and wanted to reuse that logic. But when I looked at it in reflector, I found a method 100 lines long doing some really fancy staff to make select by keywords. So the best way to make search that I need is to do it through SearchDataSource.

Fortunately it provides public property called Criteria. It is a criteria that are going to be passed for underlying DataFactory.FindPagesWithCriteria call.

So the search that I needed could be implemented in the this way. Aspx part:

<EPiServer:SearchDataSource ID="uiSearchDataSource" runat="server" EnableVisibleInMenu="false"
                            PageLink="<%# PageReference.StartPage %>">
    <SelectParameters>
        <asp:QueryStringParameter Name="SearchQuery" QueryStringField="search" DefaultValue="" />
    </SelectParameters>
</EPiServer:SearchDataSource>

And in code behind:

var pageTypeCategory = new PropertyCriteriaControl(new PropertyCriteria
                            {
                                Condition = CompareCondition.Equal,
                                Name = "PageTypeID",
                                Type = PropertyDataType.PageType,
                                Value = PageType.Load("Article").ID.ToString(),
                                Required = true
                            });

var pageTypeCategory1 = new PropertyCriteriaControl(new PropertyCriteria
                            {
                                Condition = CompareCondition.Equal,
                                Name = "PageCategory",
                                Type = PropertyDataType.Category,
                                Value = Category.Find("category2").ID.ToString(),
                                Required = true
                            });

this.uiSearchDataSource.Criteria.Add(pageTypeCategory);
this.uiSearchDataSource.Criteria.Add(pageTypeCategory1);

Note that Name property of each criteria should be as shown in code above, otherwise it won’t work. Hope it helps someone!

Apr 15, 2010

Сложные запросы через ICriteria

Недавно пришлось сделать довольно таки интересный запрос с использованием NHibernate Criteria API. Думаю пример будет полезен в блоге. Итак рассмотрим следующую доменную модель:

1 

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

Допустим, что необходимо построить страницу поиска, на которой можно запросить пользователей по следующим критериям:

  1. Имя пользователя
  2. Количество заказов
  3. Общая сумма оплат по всем заказам

Для такого построения запроса я буду использовать следующий класс:

public class SearchCriteria
{
    public string UserName { get; set; }

    public int PageSize { get; set; }

    public int PageNumber { get; set; }

    public int? OrdersNumber { get; set;}

    public double? MinPayedMoney { get; set; }

    public double? MaxPayedMoney { get; set; }
}

Он содержит текущие условия поиска. Итак начнем. Самое простое что можно реализовать это поиск по имени пользователя:

Customer customerAlias = null;  // понадобится позже
var criteria = Global.CurrentSession.CreateCriteria(typeof(Customer), () => customerAlias);

if (!string.IsNullOrEmpty(searchCriteria.UserName))
{
    criteria.Add(SqlExpression.Like<Customer>(x => x.Name, searchCriteria.UserName, MatchMode.Anywhere));
}
criteria.SetMaxResults(searchCriteria.PageSize);
criteria.SetFirstResult(searchCriteria.PageSize * (searchCriteria.PageNumber - 1));

return criteria.List<Customer>();

Ничего интересного. Теперь добавим поиск по количеству товаров. Для этого необходимо будет использовать подзапрос. Делается это следующим образом:

if (searchCriteria.OrdersNumber.HasValue)
{
    ICriteria ordersCriteria = criteria.CreateCriteria<Customer>(x => x.Orders);

    DetachedCriteria computersCount = DetachedCriteria.For<CustomersOrder>();
    computersCount.SetProjection(Projections.RowCount());
    computersCount.Add<CustomersOrder>(x => x.Customer.Id == customerAlias.Id);

    ordersCriteria.Add(Subqueries.Eq(searchCriteria.OrdersNumber.Value, computersCount));
}

Для того, чтобы Nhibernate правильно выполнил join таблиц, мы используем customerAlias. В 6й строке мы указываем на то, что нас интересуют только заказы данного пользователя, а не общее количество заказов в таблице.

Подобным же образом необходимо сделать и запрос на общую сумму выплат:

DetachedCriteria payedAmout = DetachedCriteria.For<OrderLine>();
payedAmout.SetProjection(LambdaProjection.Sum<OrderLine>(x => x.TotalPrice));
payedAmout.CreateCriteria<OrderLine>(x => x.CustomersOrder)
    .Add<CustomersOrder>(x => x.Customer.Id == customerAlias.Id);

if (searchCriteria.MinPayedMoney.HasValue)
{
    criteria.Add(Subqueries.Lt(searchCriteria.MinPayedMoney, payedAmout));
}

if (searchCriteria.MaxPayedMoney.HasValue)
{
    criteria.Add(Subqueries.Gt(searchCriteria.MaxPayedMoney, payedAmout));
}

Собрав все воедино Nhibernate выполнит следующий SQL запрос:

SELECT top 20 this_.Id                as Id1_1_,
              this_.Name              as Name1_1_,
              customerso1_.Id         as Id3_0_,
              customerso1_.OrderDate  as OrderDate3_0_,
              customerso1_.CustomerId as CustomerId3_0_
FROM   Customers this_
       inner join CustomersOrders customerso1_
         on this_.Id = customerso1_.CustomerId
WHERE  this_.Name like '%test%' /* @p0 */
       and 2 /* @p1 */ = (SELECT count(* ) as y0_
                  FROM   CustomersOrders this_0_
                  WHERE  this_0_.CustomerId = this_.Id)
       and 100 /* @p2 */ < (SELECT sum(this_0_.TotalPrice) as y0_
                  FROM   OrderLines this_0_
                         inner join CustomersOrders customerso1_
                           on this_0_.CustomersOrderId = customerso1_.Id
                  WHERE  customerso1_.CustomerId = this_.Id)
       and 500 /* @p3 */ > (SELECT sum(this_0_.TotalPrice) as y0_
                  FROM   OrderLines this_0_
                         inner join CustomersOrders customerso1_
                           on this_0_.CustomersOrderId = customerso1_.Id
                  WHERE  customerso1_.CustomerId = this_.Id)

Обратите внимание на inner join в 7й строке. Если пользователь выполнил больше одного заказа, то в результате запроса SQL Server вернет столько строк, сколько заказов у пользователя. Чтобы избежать этого, необходимо добавить Distinct:

criteria.SetProjection(
                Projections.Distinct(
                    Projections.ProjectionList().Add(LambdaProjection.Property<Customer>(x => x.Id), "Id")
                                                .Add(LambdaProjection.Property<Customer>(x => x.Name), "Name")));

Теперь приведу весь код:

Customer customerAlias = null;  
var criteria = Global.CurrentSession.CreateCriteria(typeof(Customer), () => customerAlias);

if (!string.IsNullOrEmpty(searchCriteria.UserName))
{
    criteria.Add(SqlExpression.Like<Customer>(x => x.Name, searchCriteria.UserName, MatchMode.Anywhere));
}

if (searchCriteria.OrdersNumber.HasValue)
{
    ICriteria ordersCriteria = criteria.CreateCriteria<Customer>(x => x.Orders);

    DetachedCriteria computersCount = DetachedCriteria.For<CustomersOrder>();
    computersCount.SetProjection(Projections.RowCount());
    computersCount.Add<CustomersOrder>(x => x.Customer.Id == customerAlias.Id);

    ordersCriteria.Add(Subqueries.Eq(searchCriteria.OrdersNumber.Value, computersCount));
}

DetachedCriteria payedAmout = DetachedCriteria.For<OrderLine>();
payedAmout.SetProjection(LambdaProjection.Sum<OrderLine>(x => x.TotalPrice));
payedAmout.CreateCriteria<OrderLine>(x => x.CustomersOrder)
    .Add<CustomersOrder>(x => x.Customer.Id == customerAlias.Id);

if (searchCriteria.MinPayedMoney.HasValue)
{
    criteria.Add(Subqueries.Lt(searchCriteria.MinPayedMoney, payedAmout));
}

if (searchCriteria.MaxPayedMoney.HasValue)
{
    criteria.Add(Subqueries.Gt(searchCriteria.MaxPayedMoney, payedAmout));
}

criteria.SetProjection(
    Projections.Distinct(
        Projections.ProjectionList().Add(LambdaProjection.Property<Customer>(x => x.Id), "Id")
                                    .Add(LambdaProjection.Property<Customer>(x => x.Name), "Name")));

criteria.SetMaxResults(searchCriteria.PageSize);
criteria.SetFirstResult(searchCriteria.PageSize * (searchCriteria.PageNumber - 1));

criteria.SetResultTransformer(Transformers.AliasToBean<Customer>());

return criteria.List<Customer>();

Данный код формирует следующий SQL запрос:

SELECT distinct top 20 this_.Id   as y0_,
                       this_.Name as y1_
FROM   Customers this_
       inner join CustomersOrders customerso1_
         on this_.Id = customerso1_.CustomerId
WHERE  this_.Name like '%test%' /* @p0 */
       and 2 /* @p1 */ = (SELECT count(* ) as y0_
                  FROM   CustomersOrders this_0_
                  WHERE  this_0_.CustomerId = this_.Id)
       and 100 /* @p2 */ < (SELECT sum(this_0_.TotalPrice) as y0_
                  FROM   OrderLines this_0_
                         inner join CustomersOrders customerso1_
                           on this_0_.CustomersOrderId = customerso1_.Id
                  WHERE  customerso1_.CustomerId = this_.Id)
       and 500 /* @p3 */ > (SELECT sum(this_0_.TotalPrice) as y0_
                  FROM   OrderLines this_0_
                         inner join CustomersOrders customerso1_
                           on this_0_.CustomersOrderId = customerso1_.Id
                  WHERE  customerso1_.CustomerId = this_.Id)

Код выглядит как по мне страшновато. Но с другой стороны LINQ реализация врядли была бы проще. Все равно пришлось бы использовать синтаксис через extension методы и лямбда выражения.

Тут конечно можно еще кое что оптимизировать, но цель поста была показать как строить сложные запросы при помощи NHibernate. Надеюсь это поможет.

Nov 4, 2009

NHibernate для начинающих. Часть 4 Criteria queries

Линки на остальные статьи

Основным средством построения запросов в NHibernate является Criteria api (до тех пор, пока не реализуют полноценный Linq to Hibernate). Поэтому я опишу некоторые примеры запросов, которые можно строить с их использованием. Для этого воспользуемся моделью, описанной тут.

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

Получить все продукты, в названии которых содержится определенная подстрока (эти запросы наиболее часто встречаются на разного рода админках для того, чтобы можно было быстро найти необходимую запись в базе). Но как правило данные всегда необходимо получать с пейджингом. Данный запрос можно выполнить следующим образом:

ICriteria criteria = Global.CurrentSession.CreateCriteria(typeof(Product));

const string requiredSubstring = "prod";
criteria.Add(Restrictions.Like("Description", requiredSubstring, MatchMode.Anywhere));
criteria.SetMaxResults(10);
criteria.SetFirstResult(0);
criteria.AddOrder(NHibernate.Criterion.Order.Asc("Description"));

IList<Product> result = criteria.List<Product>();

Итак что здесь происходит:

  • 1 строка очевидно – создаем сам критерий.
  • 4 строка – в критерий добавляется условие, которое говорит о том, что объекты, которые войдут в результирующий рекорд сет в свойстве Description должны содержать requiredSubstring. Последний параметр говорит о том, где именно должна быть эта подстрока. Можно создать критерий, который говорит что название должно начинаться с указанного слова или заканчиваться на него. Прелесть этого запроса в том, что мы оперируем исключительно доменными объектами, мы вообще не заботимся о том, как NHibernate построит необходимый запрос и какие поля/таблицы затронет.
  • 5 строка – указываем, что результатов должно быть не больше 10.
  • 6 строка – выдавать результаты начиная с 0го. Т.е. например, у нас в базе есть 20 продуктов, из них 15 в названии содержит «prod». Значит данный запрос выведет 10 первых продуктов.
  • 7 строка – сортировка.

Далее рассмотрим следующий пример:

Найти все продукты, название которых содержит подстроку, и цена которых больше чем указанное число:

const string requiredSubstring = "prod";

const double requiredPrice = 5.0;
criteria.Add(new Conjunction()
  .Add(Restrictions.Like("Description", requiredSubstring, MatchMode.Anywhere)) 
  .Add(Restrictions.Gt("Price", requiredPrice))
      );

Это первый вариант записи, но мне он кажется довольно слабо читаемым, есть альтернативный:

criteria.Add(Restrictions.Like("Description", requiredSubstring, MatchMode.Anywhere) &&
    Restrictions.Gt("Price", requiredPrice)
             );

Как Вы видите между двумя условиями стоит обычный оператор &&. Так же можно использовать и || где это необходимо (например если бы надо было найти товар у которого подстрока содержится либо в названии либо в описании).

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

ICriteria criteria = Global.CurrentSession.CreateCriteria(typeof(Product));
const string requiredSubstring = "prod";
criteria.Add(Restrictions.Like("Description", requiredSubstring, MatchMode.Anywhere));
criteria.SetProjection(Projections.RowCount());
int result = criteria.UniqueResult<int>();

Теперь посмотрим на следующий запрос. Найти все продукты, цена которых выше стредней. Для этого необходимо воспользоваться подзапросом, который будет вычислять среднюю цену продуктов. Итак:

DetachedCriteria avgCritegia = DetachedCriteria.For<Product>();
avgCritegia.SetProjection(Projections.Avg("Price"));

ICriteria criteria = Global.CurrentSession.CreateCriteria(typeof(Product));
criteria.Add(Subqueries.PropertyGt("Price", avgCritegia));

IList<Product> result = criteria.List<Product>();

NHibernate выполнит следующий запрос:

SELECT this_.Id as Id0_0_, this_.Description as Descript2_0_0_, this_.Name as Name0_0_, this_.Price as Price0_0_
FROM Products this_ 
WHERE this_.Price > (SELECT avg(cast(this_0_.Price as DOUBLE PRECISION)) as y0_ FROM Products this_0_)

Теперь посмотрим на запрос к объектам с коллекциями. Допустим надо найти все продукты, которые заказывал покупатель с определенной строкой в имени:

ICriteria criteria = Global.CurrentSession.CreateCriteria(typeof(Product));

const string requiredCustomerName = "cust";
criteria.CreateCriteria("Orders")
 .Add(Restrictions.Like("Customer", requiredCustomerName, MatchMode.Anywhere));

IList<Product> result = criteria.List<Product>();

NHibnerate выполнит следующий запрос:

SELECT this_.Id as Id0_1_,
 this_.Description as Descript2_0_1_,
 this_.Name as Name0_1_,
 this_.Price as Price0_1_,
 order1_.Id as Id3_0_,
 order1_.NumberOfItems as NumberOf2_3_0_,
 order1_.Customer as Customer3_0_,
 order1_.ProductId as ProductId3_0_
FROM Products this_ inner join Orders order1_ on this_.Id=order1_.ProductId
WHERE order1_.Customer like @p0
@p0=N'%cust%'

Естественно что в .net 3.5 уже очень не хотелось бы использовать названия свойств в виде строк. Nз-за этого приходится постоянно лазить в исходник самого объекта и смотреть там точное название необходимого ствойства. Чтобы избежать этого существует QueryOver API. Они позволяют переписать все выше упомянутые запросы, но в строго типизированном виде.