22 listopada 2007

Bieżący kontekst Spring'a w aplikacji JSF

Wyobraźmy sobie, że mamy aplikację Spring + JSF (choć łatwo uogólnić na inne technologie WWW). W pewnej klasie potrzebujemy ziarna zarządzanego Spring, ale nie możemy go wstrzyknąć (ang. injection) poprzez konfigurację, gdyż cyklem życia tej klasy nie zarządza Spring. Przykładem niech będzie komponent JSF. To nie Spring tworzy instancje tych komponentów, tylko JSF, więc nie możemy wstrzyknąć naszej zależności. Poniżej fragment kodu pokazujący jak pobrać bieżący kontekst Spring’a (utworzony przez ContextLoaderListener zdefiniowany w web.xml) i z niego ziarno zarządzane. Oto on:


import javax.faces.context.FacesContext;
import javax.servlet.ServletContext;

import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.support.WebApplicationContextUtils;

ServletContext servletContext = (ServletContext)FacesContext.
getCurrentInstance().getExternalContext().getContext();

WebApplicationContext applicationContext = WebApplicationContextUtils.
getWebApplicationContext(servletContext);

Object springBean = applicationContext.getBean("beanName");

9 listopada 2007

Hibernate Search, czyli wyszukiwanie pełnotekstowe encji Hibernate

Zacznijmy od początku, czyli co to jest wyszukiwanie pełnotekstowe? Wyobraźmy sobie, że implementujemy funkcjonalność wyszukiwania produktów w sklepie internetowym. Nie chodzi jednak o jakieś formularze gdzie podajemy wartości dla ściśle określonych atrybutów tylko o wyszukanie na podstawie opisu tekstowego produktu z użyciem słów kluczowych. Chcemy więc mieć taki mały Google który wyszukuje wśród naszych obiektów, a mówiąc ściślej encji (ang. entity) Hibernate. Istnieją co najmniej dwie rozsądne metody implementacji takiej funkcjonalności, tzn. ja znam dwie: Hibernate Search oraz użycie odpowiedniego indeksu bazy danych (FULLTEXT INDEX). W niniejszym artykule pokażę, jak zbudować prostą aplikację na bazie Hibernate Search. Za punkt wyjścia przyjmę aplikację, którą przedstawiłem w artykule "Aplikacja Hibernate z adnotacjami i Spring'iem", ale de facto nada się każda aplikacja Hibernate z odwzorowaniem obiektowo-relacyjnym zdefiniowanym w formie adnotacji. Zanim jednak przejdę do czynu dwa słowa o tej technologii. Otóż Hibernate Search to nic innego jak integracja Apache Lucene z Hibernate Core w ten sposób, aby operacje na encjach były odwzorowane na odpowiednie operacje na indeksie Lucene. Dodatkowo, efektem wyszukiwania w indeksie Lucene nie jest – dajmy na to – lista identyfikatorów, lecz lista encji zarządzanych Hibernate. Indeks właśnie jest najważniejszym elementem technologii Lucene. Zawiera on informacje o dokumentach Lucene i umożliwia sprawne wyszukiwanie tych pasujących do naszego zapytania. O dokumentach tych możemy myśleć jak o obiektach. Podstawowe operacje API Lucene to dodanie dokumentu do indeksu, usunięcie go i modyfikacja. To są właśnie operacje wywoływane automatycznie przez Hibernate Search ilekroć encje – które zdefiniowaliśmy jako indeksowane – są zapisywane, usuwane bądź modyfikowane. To rzekłszy przechodzę do implementacji.

Zacznijmy od pobrania biblioteki Hibernate Search (ja użyłem wersji 3.0.0 GA) i dodania jej .jar’ów (hibernate-search.jar i lucene-core-2.2.0.jar) do projektu. Kolejnym krokiem jest konfiguracja indeksu Lucene. Rzecz jest trywialna i sprowadza się do określenia – w formie właściwości (ang. property) Hibernate – klasy implementującej dostawcę indeksu oraz kolejnego parametru wymaganego dla dostawcy użytego w naszej aplikacji. Ja używam Hibernate w połączeniu ze Spring’iem a więc modyfikuję ziarno zarządzane Spring’a definiujące fabrykę sesji Hibernate. Powinno ono wyglądać następująco (patrz artykuł "Aplikacja Hibernate z adnotacjami i Spring'iem"):

<bean id="hibernateSessionFactory"
class="org.springframework.orm.hibernate3.annotation.AnnotationSessionFactoryBean">

<property name="dataSource" ref="hibernateDataSource" />
<property name="schemaUpdate" value="true" />
<property name="hibernateProperties">
<value>
hibernate.dialect = org.hibernate.dialect.MySQLDialect
hibernate.search.default.directory_provider =
org.hibernate.search.store.FSDirectoryProvider

hibernate.search.default.indexBase = c:/index
</value>
</property>
<property name="annotatedClasses">
<list>
<value>entity.Product</value>
</list>
</property>
</bean>

Jak można by się domyślać wartość ‘c:/index’ to katalog w którym będą przechowywane pliki stanowiące indeks. Weźmy teraz jedną z naszych encji Hibernate i za pomocą kilku adnotacji sprawmy by była indeksowana. Poniżej przykład dla klasy produktu:


@Entity @Indexed
public class Product {

@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
@DocumentId
private Long productId;

@Field(index = Index.TOKENIZED, store = Store.NO)
private String name;

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public Long getProductId() {
return productId;
}

public void setProductId(Long productId) {
this.productId = productId;
}
}

Wszystkie adnotacje Hibernate Search pochodzą z pakietu org.hibernate.search.annotations. Pozostałe to adnotacje JPA (wykorzystywane także przez Hibernate). Adnotacja @Indexed sprawia, że encja będzie indeksowana. Tak jak @Id określa identyfikator encji tak @DocumentId określa identyfikator dokumentu w indeksie. Bodaj zawsze powinna to być ta sama właściwość klasy, ale nie musi być. Adnotacja @Field określa właściwości klasy, które będą indeksowane. Nie jest tak jak w przypadku JPA, że domyślnie indeksowane a w przypadku JPA utrwalane (ang. persisted) są wszystkie właściwości. Atrybut index = Index.TOKENIZED adnotacji określa, że indeksowane będą poszczególne słowa a nie cała wartość. Właśnie tego chcemy dla właściwości tekstowych, ale do indeksu możemy włączyć także właściwości będące np. datą aby umożliwić filtrowanie a wtedy nie chcemy aby wartość była w jakikolwiek sposób rozbijana. Atrybut store = Store.NO powoduje, że wartość sama w sobie nie będzie przechowywana w indeksie. W pewnych przypadkach przechowywanie wartości ma sens, ale jest to dyskusja poza zakresem tego artykułu. Stwórzmy teraz klasę DAO z metodą searchProducts(String searchText), która zwróci listę encji zarządzanych Hibernate spełniających zadane kryterium wyszukiwania. Oto przykładowy kod źródłowy:


public class ProductDAOBean extends HibernateDaoSupport implements ProductDAO {

public void saveProduct(Product product) {
getHibernateTemplate().saveOrUpdate(product);
}

@SuppressWarnings("unchecked")
public List<Product> searchProducts(String searchText) {
List<Product> products = null;

Session session = getSession();
try {
FullTextSession fullTextSession = Search.createFullTextSession(session);

MultiFieldQueryParser queryParser =
new MultiFieldQueryParser(new String[] { "name" }, new StandardAnalyzer() );

FullTextQuery fullTextQuery fullTextSession =
.createFullTextQuery(queryParser.parse(searchText), Product.class);

products = fullTextQuery.list();

} catch (HibernateException hibernateExc) {
throw convertHibernateAccessException(hibernateExc);
} catch (ParseException parseExc) {
throw new DataRetrievalFailureException("Lucene exception", parseExc);
} finally {
releaseSession(session);
}

return products;
}
}

Metoda saveProduct(Product product) będzie potrzebna, aby zasiedlić bazę danych oraz indeks Lucene danymi testowymi. Nie możemy zrobić tego inaczej (np. poprzez SQL INSERT) gdyż wtedy nasze obiekty nie zostałyby zindeksowane. I jeszcze klasa testu JUnit 4 wraz z metodami do instalacji danych testowych:

public class ProductDAOBeanTest {

private ProductDAOBean productDAOBean;

@Before
public void initialize() {
ApplicationContext context = new ClassPathXmlApplicationContext(
new String[] { "config/dao.xml" });

productDAOBean = (ProductDAOBean) context.getBean("productDAO");
}

@Test
public void testSearchProducts() {
List<Product> products = productDAOBean.searchProducts("wiertarka bosch");

for(Product product : products)
System.out.println(product.getName());
}

@Test
public void installData() {
productDAOBean.saveProduct(newProduct("Wiertarka Bosch GSB 13RE"));
productDAOBean.saveProduct(newProduct("Wiertarka Bosch PSB 500RE"));
productDAOBean.saveProduct(newProduct("Szlifierka Bosch GWS 1000"));
productDAOBean.saveProduct(newProduct("Strug Bosch GHO 26-82"));
productDAOBean.saveProduct(newProduct("Wiertarka Makita HP 1620"));
productDAOBean.saveProduct(newProduct("Szlifierka Makita 9565H"));
productDAOBean.saveProduct(newProduct("Szlifierka Skill 9470"));
}

private Product newProduct(String prodName) {
Product product = new Product();

product.setName(prodName);

return product;
}
}

Metoda searchProducts(String searchText) akceptuje zapytania zgodne ze składnią Lucene Query Language i jest to sporo więcej niż tylko lista słów. Zachęcam do lektury dokumentacji tego języka (jest krótka i przyjemna) i eksperymentów.

1 listopada 2007

Aplikacja Hibernate z adnotacjami i Spring'iem

W wyniku długotrwałej ewolucji, z chęci dotrzymania kroku, ale i potrzeby zachowania zgodności wstecznej dzisiejszy Hibernate stał się technologią o wielu obliczach. Z jednej strony możemy używać Hibernate jako implementacji JPA, z drugiej strony ciągle żywe jest jego tradycyjne API. Używając Hibernate w sposób, nazwijmy to tradycyjny (tj. nie jako JPA) możemy powierzyć konfigurację szkieletowi (ang. framework) Spring lub wykonać ją samodzielnie, z wykorzystaniem plików konfiguracyjnych hibernate.cfg.xml lub hibernate.properties. Generalnie, wszystko sprowadza się do zbudowania obiektu implementującego interfejs org.hibernate.SessionFactory, który jest sercem technologii Hibernate. Jeśli nie używamy do tego celu Spring’a to pierwszym krokiem jest zbudowanie obiektu klasy org.hibernate.cfg.Configuration (albo AnnotationConfiguration). Możemy to zrobić wczytując właśnie plik konfiguracyjny, ale możemy też zbudować go w sposób programowy. Na podstawie tego obiektu tworzymy obiekt fabryki sesji (implementujący org.hibernate.SessionFactory). Jeśli budowę obiektu fabryki sesji powierzymy Spring’owi, to konfigurację wykonujemy definiując odpowiednio ziarno zarządzane (ang. managed bean) kontekstu Spring’a. Dodatkowo, oprócz konfiguracji obiektu fabryki sesji definiujemy odwzorowanie obiektowo-relacyjne dla naszych encji (ang. entity). Odwzorowanie to możemy wykonać zasadniczo na jeden z dwu sposobów; z użyciem plików XML lub z użyciem adnotacji. W dzisiejszym artykule opiszę, jak używać Hibernate konfigurowany przez Spring z odwzorowaniem zdefiniowanym w postaci adnotacji.

Zacznijmy od utworzenia zwykłego (tj. nie www) projektu Java (w Eclipse jest to New > Project > Java Project). Projekt nazwijmy 'Hibernate-Spring'. Teraz pobierzmy biblioteki Hibernate z działu 'Download' portalu http://www.hibernate.org/. Potrzebne będą 'Hibernate Core' (ja użyłem wersji 3.2.4.sp1) oraz 'Hibernate Annotations' (użyłem wersji 3.3.0.GA). Pobrane archiwa trzeba rozpakować a biblioteki dodać do aplikacji. Jeśli używamy któregoś z klonów Eclipse to możemy to zrobić definiując zestaw .jar'ów jako bibliotekę użytkownika. W tym celu wybierzmy opcję Window > Preferences, gałąź Java > Build Path > User Libraries. Następnie kliknijmy guzik New i podajmy nazwę biblioteki, np. 'Hibernate'. Teraz używając guzika 'Add JARs' dodajmy do nowo zdefiniowanej biblioteki 'Hibernate' wymagane pliki .jar (to jakie są wymagane pokazuje niżej). Analogicznie jak z bibliotekami dla Hibernate musimy zrobić z bibliotekami dla Spring'a. Ze strony http://www.springframework.org/download pobieramy więc 'Spring Framework' i dodajemy do projektu (ja mam wersję 2.1 M3). Jeśli używamy Eclipse'a możemy zdefiniować bibliotekę użytkownika o nazwie 'Spring'. Potrzebujemy jeszcze sterownika JDBC odpowiedniego dla naszej bazy danych, ja używam MySQL i zdefiniowałem sobie bibliotekę użytkownika 'MySQL-JDBC'. To, jakie biblioteki są wystarczające dla naszego przykładu widać na poniższej ilustracji. Jest to widok definiowania bibliotek użytkownika:


Aby tak zdefiniowane biblioteki użytkownika dodać do projektu, klikamy na nim prawym guzikiem myszy i wybieramy Build Path > Add Libraries > User Library, klikamy Next, zaznaczamy nasze biblioteki i klikamy Finish.

Jesteśmy gotowi, by zacząć w końcu tworzyć naszą aplikację. Zaimplementujemy klasę DAO (dao.ProductDAOBean i interfejs dao.ProductDAO) dla encji produktu (entity.Product), która będzie miała proste metody do zapisu i wyszukania z użyciem identyfikatora. Oto ich kod źródłowy:

public interface ProductDAO {
public void saveProduct(Product product);

public Product findProduct(Long productId);
}

public class ProductDAOBean extends HibernateDaoSupport implements ProductDAO {

public void saveProduct(Product product) {
getHibernateTemplate().saveOrUpdate(product);
}

public Product findProduct(Long productId) {
return (Product) getHibernateTemplate().get(Product.class, productId);
}
}

Klasa DAO dziedziczy z klasy pomocniczej HibernateDaoSupport dostarczanej przez Spring. Ułatwia ona znacznie programowanie z użyciem Hibernate, zwalnia między innymi z koniczności samodzielnego zarządzania zasobami, tj. obiektami sesji oraz zapewnia spójną strukturę wyjątków. Poniżej kod naszej encji:

@Entity
public class Product {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long productId;

private String name;

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public Long getProductId() {
return productId;
}

public void setProductId(Long productId) {
this.productId = productId;
}
}

Proszę zauważyć, że mimo iż używamy tradycyjnego API Hibernate’a, odwzorowanie klasy na relację definiujemy z użyciem adnotacji JPA. Naturalnie w bardziej skomplikowanych przypadkach będziemy chcieli sięgać po rozszerzenia specyficzne dla Hibernate. A teraz najważniejsze, czyli plik kontekstu Spring, który stanowi de facto konfigurację Hibernate, oto on:

<?xml version="1.0" encoding="UTF-8"?>

<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-2.0.xsd">

<bean id="hibernateDataSource"
class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">

<property name="driverClassName" value="com.mysql.jdbc.Driver" />
<property name="url" value="jdbc:mysql://localhost:3306/hibernate" />
<property name="username" value="root" />
<property name="password" value="passwd" />
</bean>

<bean id="hibernateSessionFactory"
class="org.springframework.orm.hibernate3.annotation.AnnotationSessionFactoryBean">

<property name="dataSource" ref="hibernateDataSource" />
<property name="schemaUpdate" value="true" />
<property name="hibernateProperties">
<value>
hibernate.dialect = org.hibernate.dialect.MySQLDialect
</value>
</property>
<property name="annotatedClasses">
<list>
<value>entity.Product</value>
</list>
</property>
</bean>

<bean id="productDAO" class="dao.ProductDAOBean">
<property name="sessionFactory" ref="hibernateSessionFactory" />
</bean>

</beans>

Charakterystyczne jest tutaj użycie klasy AnnotationSessionFactoryBean jako obiektu implementującego interfejs org.hibernate.SessionFactory. Właśnie tej klasy należy użyć, aby móc definiować odwzorowanie obiektowo-relacyjne w formie adnotacji. Na koniec przygotujemy jeszcze klasę testu JUnit 4. Naturalnie do projektu należy dodać bibliotekę JUnit w wersji co najmniej 4. Oto owa klasa, przy założeniu, że plik definiujący kontekst Spring’a nazwaliśmy 'dao.xml' i umieściliśmy go w pakiecie 'config':

public class ProductDAOBeanTest {
private ProductDAOBean productDAOBean;

@Before
public void initialize() {
ApplicationContext context = new ClassPathXmlApplicationContext(
new String[] {"config/dao.xml"});

productDAOBean = (ProductDAOBean)context.getBean("productDAO");
}

@Test
public void testSaveProduct() {
Product product = new Product();

product.setName("Super produkt");

productDAOBean.saveProduct(product);

product = productDAOBean.findProduct(product.getProductId());

Assert.assertEquals("Super produkt", product.getName());
}
}

To by było na tyle. Aby przekonać się, że rzeczywiście działa możemy jeszcze zerknąć do bazy danych. Dla jasności, poniżej widok na strukturę projektu: