Monthly Archives: June 2011

Tag g:select w Grails

Ostatnio miałem z Grailsami cokolwiek niefajną sytuację, a poszło o tag g:select. Zasadniczo nie wiem, co zawiniło – moje luźne podejście, brak dostatecznego przetestowania, czy mało przejrzysta dokumentacja. Pewnie wszystko po trochu.

Zacznijmy jednak od początku. Jak można się łatwo domyślić w GSP użycie tagu g:select ma docelowo doprowadzić do wyrenderowania znacznika <select>. Póki co wszystko jasne. Znaczącym ułatwieniem jest również fakt, iż znacznik ten może posiadać atrybut multiple, który pozwala użytkownikowi na wybranie nie jednego, ale kilkunastu elementów z listy. W moim przypadku były to kategorie – choć to rzecz bez znaczenia, grunt, iż etykietami były łańcuchy znaków, zaś przekazywanymi wartościami identyfikatory liczbowe (konkretnie wartości klucza głównego w bazie danych). Przyjmijmy taką oto sytuację:

<g:select name="kategorie" from="${kategorieList}" value="${wybranePoprzednioID}"
 optionKey="id" multiple="true" />

Na liście kategorii znajduje się kilkanaście obiektów, zaś identyfikatory wybrane z listy (po np. wadliwej walidacji i ponownym wyrenderowaniu formularza) znajdują się w zmiennej/kolekcji o nazwie ${wybranePoprzednioID}. Po stronie kontrolera działał taki oto pseudokod:

if( kategorie ) {
    kategorie.each {
       zapiszIDDoBazy( it )
    }
}

Kiedy wybierano na liście tylko jeden element wszystko było OK. Co więcej – kiedy na liście wybierano kilka elementów również wszystko było OK. Problem zaczął się w momencie, gdy aplikacja wylądowała na serwerze preprodukcyjnym. W czym problem? Otóż po wybraniu jednego elementu z listy okazywało się, że po ponownym wyświetleniu formularza (po udanym zapisie do bazy danych) zaznaczona kategoria nie pokrywała się z tą, którą wybrano przed zapisaniem. Ot zagwozdka. Przy wybieraniu dwóch lub więcej elementów – działało poprawnie. Szybki debug i oto co się okazało.

Kategorie w bazie danych znalazły się ‘ręcznie’. To znaczy, że zostały wprowadzone grubym klientem bezpośrednio do bazy danych i oczywiście sekwencja przydzieliła ładne identyfikatory startując od numeru 1 (z krokiem też o 1). Zatem mieliśmy bodajże 7 kategorii, spośród których można było wybierać, każda posiadająca identyfikator mniejszy niż 10. Wraz z przejściem na preprodukcję do pracy wzięli się nie tylko testerzy (ci przetestowali aplikację i wszystko było OK, nawet po dodaniu kategorii), ale również docelowi użytkownicy. A ci z kolei pododawali o wiele więcej kategorii do wyboru i tym samym identyfikatory rekordów zaczęły składać się z 2 cyfr. Co z tego wynika?

Otóż Groovy w konstrukcji each przechodzi po kolejnych elementach obiektu, na rzecz którego został wywołany. W przypadku kolekcji oznacza to przejście po wszystkich elementach, zaś w przypadku łańcuchów tekstowych – po każdym znaku w tymże łańcuchu! Przy wybraniu kilkunastu elementów wartość parametru była kolekcją – zatem iteracja szła dobrze. Przy zaznaczeniu kategorii ‘początkowych’, czyli takich, które miały identyfikatory mniejsze niż 10 również wszystko było OK. Problemem było wybranie 1 elementu o identyfikatorze np. 14Grails traktował wówczas parametr jako łańcuch tekstowy i wykonanie takiego kodu:

if( kategorie ) {
 kategorie.each {
println it
 }
}

Dla identyfikatora 14 dawało na wyjściu:


1
4

Czyli jako dwie oddzielne wartości. Widzicie już problem? W bazie danych istniały kategorie o takich identyfikatorach (1 oraz 4) zatem przy zapisie nie było mowy o wyrzuceniu wyjątku czy błędach – wszystko się zgadzało, kategorie istniały to Grails grzecznie je zapisał. Problem był taki, że to dane wejściowe nie były do końca tymi, na które liczyliśmy.  Czyli zamiast jednej kategorii o identyfikatorze 14 mieliśmy dwie kategorie o identyfikatorach oraz 4.  Typowy błąd logiczny – godzinka na debugowaniu w plecy.  Stąd ten wpis i mam nadzieję, że dzięki niemu zdarzy się uniknąć komuś takiej sytuacji 😉

Natywne zapytania SQL w Groovy i Grails

Grails jeśli chodzi o zabawy z bazą danych potrafi być bardzo wdzięczny. Niestety czasami potrzebujemy odpytać bazę w sposób, do którego magii Grails po prostu nie da się użyć. Tutaj z pomocą przychodzi sam Groovy.

Załóżmy, że potrzebujemy zapytania sumującego szereg wartości z tabeli. Posłużę się znów przykładem tabeli z wpisami na blogu. Przyjmijmy, że w wierszach trzymamy informacje o odsłonach danego wpisu, ale z podziałem na np. dni. Czyli mamy dzisiejsze odsłony, odsłony z wczoraj i odsłony od początku istnienia danego wpisu. Użycie Grailsów w tym przypadku nie bardzo się powiedzie zatem kod napiszemy w Groovym, korzystając z dobrodziejstw klasy groovy.sql.Sql.


 def dataSource

 String sqlAllCommand = "SELECT SUM(show_count_all) AS sca, SUM(show_count_yesterday) AS scy, SUM(show_count_today) AS sct FROM posts";
 groovy.sql.Sql sql = new groovy.sql.Sql( dataSource )

 sql.eachRow( sqlAllCommand, { row ->
			println row.sca + " " + row.scy + " " + row.sct
		} )

 sql.close()

Jak widać rzecz jest dość prosta. Jedyną rzeczą o której należy pamiętać jest skorzystanie z dependency injection dla otrzymania obiektu dataSource. Problemem niestety jest to, iż bezpośrednie posługiwanie się beanem dataSource może w perspektywie powodować wyczerpywanie się puli wątków na serwerze lub komunikatem w stylu dataSource is closed (pomimo teoretycznego zamykania połączenia odpowiednią metodą). Poszukując w internecie rozwiązania natknąłem się na rozwiązanie bazujące na Hibernate. Oto ono:

def sessionFactory

String sqlAllCommand = "SELECT SUM(show_count_all) AS sca, SUM(show_count_yesterday) AS scy, SUM(show_count_today) AS sct FROM posts";
groovy.sql.Sql sql = new groovy.sql.Sql( sessionFactory.currentSession.connection() )

 sql.eachRow( sqlAllCommand, { row ->
		println row.sca + " " + row.scy + " " + row.sct
		} )

W tym przypadku pozwalamy by to Hibernate grzecznie użyczył nam obiektu sesji, a po zakończeniu pracy bez jakiegokolwiek udziału z naszej strony zajmuje się połączeniem. Błędy dotyczące ilości połączeń znikają.

Kilkukrotne wykorzystanie kryteriów szukania w Grailsach

Podobnie jak w poprzednim wpisie pozostajemy dziś w obrębie współpracy Grails z Hibernate. Choć może by być bardziej konkretnym – z mechanizmem domknięć w Groovym.

Bardzo często występującą sytuacją jest stosowanie paginatorów do prezentacji wyników wyszukiwania lub po prostu wylistowania wszystkich danych z bazy. Ma to na celu ułatwienie nawigacji użytkownikowi, ale też odciążenie bazy i łącza (nie przesyłamy na raz olbrzymich zbiorów wyników). Grailsy posiadają bardzo praktyczny tag GSP, który generuje poprawne menu paginacji oraz obsługuje co trzeba. Warunkiem koniecznym dla poprawnego działania tego mechanizmu są:

  • skonfigurowanie paginatora – czyli podanie podstawowych danych jak zakres wyświetlanych stron, bieżącą stronę i tym podobne informacje
  • wyciągnięcie danych – wyniki, które mamy zaprezentować musimy w jakiś sposób otrzymać (lub po prostu strzelić po nie do bazy)
  • podać łączną ilość wierszy w zbiorze wynikowym – i tym właśnie się teraz zajmiemy

Posiłkując się przykładem z poprzedniego wpisu (artykuły preentowane na blogu) – załóżmy, iż łączna ilość tychże wynosi 166. W panelu zarządzania blogiem, albo też na głównej stronie prezentowanie listy ich wszystkich mija się z celem. Załóżmy zatem, iż potrzebujemy do wyświetlenia tylko 10 rekordów. Otrzymanie ich z bazy danych jest dość proste (w przypadku Grails wręcz banalne – wystarczy odpowiednio manipulować metodami listującymi). Jednakże wypadałoby otrzymać informację o magicznej liczbie 166 (łączna ilość wierszy). Tutaj ponownie Grails załatwia za nas całą robotę – metoda klasy domenowej o wdzięcznej nazwie count zwraca łączną liczbę rekordów. Co jednak zrobimy w przypadku jakiejkolwiek formy filtrowania wyników?

Załóżmy, że prezentowane posty możemy filtrować w zależności od ich statusu. Posty mogą być opublikowane, równie dobrze mogą być postami prywatnymi (do ich oglądania potrzebne są specjalne uprawnienia) – możliwych pomysłów jest wiele. Moglibyśmy zastosować dynamicznych finderów Grails – czyli wywołań metod na kształt:

def postList = Post.findByStatusLike("opublikowany")

Rozwiązanie to sprawdza się w przypadku prostych list. Jeżeli wpadniemy na pomysł by naszą listę filtrować na podstawie kilku różnych kryteriów (i do tego używając ich jednocześnie), możemy wpaść w kłopoty budując w naszym kontrolerze/serwisie rozbudowane konstrukcje warunków. Raczej nie tędy droga.

Odwołamy się zatem po raz kolejny do Hibernate wraz z jego mechanizmem kryteriów. Naturalną konsekwencją byłoby utworzenie obiektu kryteriów i następnie zaaplikować go do wywołania metod listujących oraz zliczających rekordy. Rzecz jest osiągalna – poniższy listing powie więcej niż tysiąc słów:


// Dwa 'kontenery' na kryteria
def listCriteria = Post.createCriteria()
def listCountCriteria = Post.createCriteria()

// Domyslne wartosci na podstawowe parametry paginatora
if( params.offset == null ) {
	params['offset'] = 0
}

if( params.max == null ) {
	params['max'] = 30
}

if( params.order == null ) {
	params['order'] = 'asc'
}

if( params.portalXX == null ) {
	params['portalXX'] = 'all'
}

def commonListCriteria = {

	// Wszystkie kryteria, ktorych zamierzamy uzyc do
	// wyciagniecia rekordow (okreslony status, daty)

   maxResults(params.max as Integer)
   firstResult(params.offset as Integer)

}

// Zwrocenie wynikow oraz zapytanie listujace
def postList = listCriteria.list(commonListCriteria)
def postCount = listCountCriteria.count(commonListCriteria)

Prezentowany kod wydaje się być niemożliwie prosty, jednakże by dojść do takiej jego postaci straciłem chyba ze 3 godziny. Jak widać ‘bazowe’ kryterium określa również takie kryteria jak maksymalna ilość rekordów na stronie oraz przesunięcie (offset), od którego należy wyciągnąć rekordy. Równie dobrze zwrócone posty mogą reprezentować 10tą stronę z łącznego zbioru wynikowego (przy params.offset = 10). Jednocześnie zastosowanie tych samych kryteriów dla metody zliczającej dalej zwróci poprawną łączną liczbę wierszy dla danych kryteriów (pomijając wartości paginacyjne).

Sortowanie po wyrażeniu SQL w Grails

Ostatnio mam okazję produkcyjnie pobawić się Grailsami. Co cieszy – zwłaszcza po niedawnych przygodach z prehistorycznym kodem, który prezentował ogromniaste kolce i strzykał na kilometr śmierdzącą śliną. Jednakże jak zwykle coś delikatnie było nie tak – stąd ten wpis.

Na pierwszy rzut oka sprawa jest dość prosta. Chcemy posortować zwracane z bazy danych rekordy za pomocą pewnego wyrażenia. Załóżmy, że mamy tabelę z listą postów na blogu. Tabelka zawiera tytuł, zajawkę, datę i wszystkie inne obowiązkowe w tym przypadku dane. Każdy rekord zawiera też informację o ilości odsłon danego postu, a także o ilości kliknięć na link do niego prowadzący z głównej strony bloga. Sprawa jest prosta:

SELECT * FROM posts 

Idźmy dalej. Może byłoby fajnie wyciągnąć posty o największym CTR (kliki do emisji)? Ok, lecimy:

SELECT * FROM posts ORDER BY main_page_click_count / emission_count DESC

Od strony logiki oraz kodu SQL sprawa jest niesamowicie prosta. Niestety w przypadku Grailsowego DSLa problem okazał się dość niebagatelny. Standardowe metody listujące ( list, findAll ) niestety nie przyjmują dla wyciągania/sortowania/grupowania wyrażeń – otrzymywałem błąd, iż dana właściwość nie istnieje w klasie domenowej. Próby stworzenia własności typu transient (nie zapisywanej w bazie danych) jako wyniku działania pewnej funkcji (w tym przypadku dzielenia dwóch innych własności) również nie zakończyła się powodzeniem.

Tutaj z pomocą przyszedł ‘spodni’ mechanizm frameworku, czyli konkretnie Hibernate. W tym ORMie mamy coś takiego jak kryteria. Sprawa zaczęła wyglądać na łatwiejszą, niestety, tylko wyglądać. Próba użycia restrykcji do klauzuli sortującej zakończyła się niepowodzeniem. Całe szczęście znalazłem w necie wpis o dokładnie tym samym zagadnieniu. Kod przeze mnie prezentowany bazuje na zamieszczonym przez autora ww. wpisu.

Musimy utworzyć oddzielną klasę, która będzie fizycznie odpowiedzialna za dodanie kawałka kodu SQL do naszego wynikowego zapytania. U mnie wygląda ona tak:

package com.wordpress.chlebik.hibernate.extension;

import org.hibernate.criterion.Order;
import org.hibernate.criterion.CriteriaQuery;
import org.hibernate.Criteria;
import org.hibernate.HibernateException;


/**
 * Class extending Hibernate order by functionality to deal with
 * SQL-expression in order by  
 */
public class SortBySql extends Order {

    private static final long serialVersionUID = -6698545180750378282L;
    private String sqlFormula;
 
    /**
     * Constructor for Order.
     * @param sqlFormula an SQL formula that will be appended to the resulting SQL query
     */
    protected SortBySql(String sqlFormula) {
        super(sqlFormula, true);
        this.sqlFormula = sqlFormula;
    }
 
    public String toString() {
        return sqlFormula;
    }
 
    public String toSqlString(Criteria criteria, CriteriaQuery criteriaQuery) throws HibernateException {
        return sqlFormula;
    }
 
    /**
     * Custom order
     *
     * @param sqlFormula an SQL formula that will be appended to the resulting SQL query
     * @return Order
     */
    public static Order sqlFormula(String sqlFormula) {
        return new SortBySql(sqlFormula);
    }   
    
}

Klasę tę oraz metody wywołujemy w miejscu użycia kryteriów Hibernate. Dzięki sile domknięć kryteria można stworzyć oraz wywoływać w ten sposób:

 // To jest stworzenie obiektu kryteriow dla klasy domenowej 'Post'
 def listCriteria = Post.createCriteria()

 // To domkniecie, ktore przekazemy jako parametr do metody listujacej kryteriow
 def sortBySqlCriteria = {
           getInstance().addOrder( SortBySql.sqlFormula(" (main_page_click_count / emission_count) DESC ") )
 }
       
 // Wywolanie metod listujacych posty dla zadeklarowanych kryteriow          
def postList = listCriteria.list(sortBySqlCriteria) 

I w ten oto piękny sposób uzyskaliśmy zamierzony efekt. Zainteresowanych odsyłam do ww. wpisu. W tym przypadku (chodzi o CTR) należy również zwrócić uwagę na możliwy błąd dzielenia przez zero! Trzeba go obsłużyć na poziomie samej bazy danych – w moim przypadku było to Oracle i posiłkowałem się rozwiązaniami zaprezentowanymi w tym poście. Jednakże ostatecznie sprawa zakończyła się ustaleniem domyślnej wartości emisji nawiększą niż 0 i tym samym konieczność obsługi tego problemu można było pominąć.