search form, zapytanie linq.

0

Witam,

piszę sklep internetowy i napotkałem pewien problem. Na stronie głównej mam formularz search, po wpisaniu nazwy produktu, daję szukaj i z pomocą AJAX-u podmienia mi część kodu widoku produktów z pełnej listy na wyselekcjonowane przez search. Tu jest ok.

Problem zaczyna się po wybraniu którejś z kategorii, tam też mam widok z produktami o CategoryID takim jak wybrana kategoria. Nasuwa się pytanie jak sformułować zapytanie LINQ tak aby pobierało ono produkty tylko i wyłącznie ze wskazanym CategoryID. Na ten czas URL po wejściu w kategorię wygląda tak:

http://localhost:60804/Products/ProductCategory?CategoryID=6

A po wyszukaniu poprzez formularz jakiegośproduktu zmienia się na:

http://localhost:60804/Products/ProductCategory?searchQuery=Pierwszy+produkt+testowy

i wychodzi mi error log:

The parameters dictionary contains a null entry for parameter 'CategoryID' of non-nullable type 'System.Int32' for method 'System.Web.Mvc.ActionResult ProductCategory(Int32)' in 'NESTshop.DAL.ProductsController'. An optional parameter must be a reference type, a nullable type, or be declared as an optional parameter.
Parameter name: parameters

Zarzucę kodu troszkę:

ProductController (kontrolka odpowiedzialna między innymi za wyświetlanie listy produktów o danym CategoryID):

public ActionResult ProductCategory(int CategoryID)
        {
            var products = db.Product.Where(x => x.CategoryID == CategoryID).ToList();
            return View(products);
        }

A tak naskrobałem na HomeController

public ActionResult ProductsList(string searchQuery = null, int? CategoryID=null)
        {
            var products = productRepo.GetProduct().Where(p => (searchQuery == null || p.ProductTitle.ToLower().Contains(searchQuery.ToLower()) && !p.IsHidden));

            if (Request.IsAjaxRequest())
            {
                return PartialView(products);
            }

            return PartialView(products);

i działa bez zarzutu. Czy mógłby ktoś podpowiedzieć jak to napisać?

2

No to działa, czy nie działa?
"The parameters dictionary contains a null entry for parameter 'CategoryID' of non-nullable type 'System.Int32' for method 'System.Web.Mvc.ActionResult ProductCategory(Int32)' in 'NESTshop.DAL.ProductsController'. An optional parameter must be a reference type, a nullable type, or be declared as an optional parameter." - to oznacza, że jeśli akcję wywołujesz nie podając któregoś z parametrów, to taki parametr musi być nullable albo mieć wartość domyślną. W miarę dobrze to zinterpretowałeś w ProductsList(string searchQuery = null, int? CategoryID=null), tylko z technicznego punktu widzenia niepotrzebnie dałeś tam wartości domyślne. Z technicznego niepotrzebnie, ale z punktu widzenia czytelności zrobiłeś bardzo dobrze.
Jak to zrobić dalej to kwestia "biznesowa". Jeśli chcesz umożliwić łączenie sposobów wyszukiwania, czyli wyszukiwać w danej kategorii po słowie kluczowym, to musisz zrobić taki kombajn jak w ProductList.
Widziałbym to tak:

var products = productRepo.GetProduct().Where(p => !p.IsHidden);
if (categoryId != null)
    products = products.Where(p => p.CategoryId == categoryId);
if (!string.IsNullOrWhitespace(searchQuery))
    products = products.Where(p => p.ProducTitle.Contains(searchQuery));

Dlaczego tak, a nie wszystko w jednym linq? Musisz wziąć pod uwagę wydajność tego, co dzieje się po stronie samej bazy danych.
Wyszukiwanie po stringu z like jest dość wolne, a jeśli na początku like'a znajduje się %, to jest to chyba najwolniejszy z możliwych sposobów wyszukiwania w bazie (gwoli wyjaśnienia - Contains jest tłumaczone na '%szukana wartość%'). Ty do tego dokładasz jeszcze tolower - wolniej już się nie da. Upewnij się, że masz bazę danych CI (Case Insensitive, to jest chyba default, w opozycji do CS), wtedy nie potrzebujesz ToLower. Unikaj funkcji w zapytaniu, bo zwykle uniemożliwiają serwerowi sql użycie indeksów.
W moim kodzie wyszukiwanie po searchQuery zostanie zrealizowane tylko, jeśli searchQuery jest niepuste. Do sql nie trafi nic związanego z contains. Tymczasem w Twojej wersji (searchQuery == null || p.ProductTitle.ToLower().Contains(searchQuery.ToLower()) trafi; optymalizator zapytań MSSQL działa niezbyt przewidywalnie i jest dość prawdopodobne, że baza danych spędzi ileś czasu na szukaniu produktów o tytułach zawierających null.

Poza tym proponuję Ci zapoznać się z sql profilerem - to takie narzędzie, które umożliwi wyciąganie zapytań, które idą do bazy danych - oraz z planem zapytań (query plan możliwy do obejrzenia w management studio po zaznaczeniu odpowiedniego guziczka i wykonaniu zapytania przeklejonego z profilera). Naucz się odczytywać koszt zapytania oraz chociaż zgrubnie rozumieć plan zapytania, dzięki temu będziesz miał szansę nie zabić serwera gdy pojawi się trochę więcej danych niż kilkaset wierszy.

1

Nota bene: co to za potworek:

            if (Request.IsAjaxRequest())
            {
                return PartialView(products);
            }
 
            return PartialView(products);

Parafrazując: jeśli będą jajka, to kup parówki. W przeciwnym wypadku kup parówki ;-)

0

^^ co ja tam natworzyłem tego nie wiedzą najstarsi Majowie :D, poprawiłem już. Jeden problem jeszcze został:

if (!string.IsNullOrWhitespace(searchQuery))

sypie błędem, że: 'string' does not contain a definition for 'IsNullOrWhitespace', ale mam nadzieję, że podołam sobie z tym :)

0

Zmien target na .net 4.0 albo wyżej. Jesli nie mozesz, to napisz sobie taka extension metode sam:)

0

Szefie, podłączyłem Twój silnik pod swój kod i placek:

The model item passed into the dictionary is of type 'System.Linq.Enumerable+WhereListIterator`1[shop.Models.Product]', but this dictionary requires a model item of type 'System.Collections.Generic.List`1[shop.Models.Product]'.
public ActionResult ProductCategory(int CategoryID, string searchQuery = null)
        {
            var products = proRepo.GetProduct().Where(p => !p.IsHidden);
            if (CategoryID != null)
                products = products.Where(p => p.CategoryID == CategoryID);
            if (!string.IsNullOrWhiteSpace(searchQuery))
                products = products.Where(p => p.ProductTitle.Contains(searchQuery));
            return View(products);
        }

no chyba, że coś nie tak powyżej. W ProductList zmieniłem elegancko na Twój sposób zapytań linq i działa bez zarzutu.

0

return View(products.ToList()); albo w view zmień wymagany model z List<> na IEnumerable<>.
Poczytaj o LINQ, IList<T>, ICollection<T>, IEnumerable<T>, IQueryable<T> itp, bo błądzisz...

0

ProductCategory(int CategoryID, string searchQuery = null)
...
if (CategoryID != null)

Twoim zdaniem kiedy categoryId może być null?

0

W takim razie nie potrzebny ten if, skoro CategoryID jest zawsze wartością. Poprawiłem według Twoich zaleceń i pięknie działa :) o to mi się rozchodziło... Jeszcze sporo nauki przede mną widzę. LINQ w ogóle dopiero zaczynam wdrażać. Tak jak mówisz, muszę poczytać i przećwiczyć go w praktyce. Dzięki za cierpliwość do mnie.

Jeszcze takie pytanie ogólne, jakieś źródło wiedzy o LINQ w wersji papierowej polecasz?

1 użytkowników online, w tym zalogowanych: 0, gości: 1