Pokazywanie postów oznaczonych etykietą JSF. Pokaż wszystkie posty
Pokazywanie postów oznaczonych etykietą JSF. Pokaż wszystkie posty

poniedziałek, 30 stycznia 2012

Fantomowe tabelki w JSF

W niniejszym poście przyjrzymy się pewnej anomalii występującej w JSF, której skutki potencjalnie mogą być katastrofalne.
Anomalia została dobrze rozpoznana dosyć dawno, jednak jak pokazuje moje doświadczenie w pracy z zespołami korzystającymi z JSF, nie jest ona uświadomiona.
Dlatego dla tych z czytelników, którzy używają JSF i nie spotkali się ze zjawiskiem "klikania w nieodpowiednie wiersze tabelki" lektura posta jest obowiązkowa:)

Problematyczny scenariusz:
1. Użytkownik A wyświetla na stronie tabelkę z listą rekordów (Encje JPA lub DTO)
Na ekranie dla każdego rekordu mamy możliwość jego edycji/usunięcia poprzez Postback.

2. Podczas gdy użytkownik A delektuje się widokiem zaokrąglonych rogów na naszej stronie, użytkownik B (w tak zwanym międzyczasie) dokonuje w bazie zmiany danych, które są prezentowane na ekranie użytkownika A.
Może być to usunięcie danych lub edycja takich atrybutów, które wpłyną na ilość lub kolejność danych zwracanych przez zapytanie, które używa ekran użytkownika A.

3. Użytkownik A, po zaspokojeniu swych potrzeb estetycznych, klika przykładowo w pierwszy wiersz tabelki w celu usunięcia/modyfikacji rekordu.

4. Ku zdziwieniu użytkownika A, system poddał usunięciu/edycji zupełnie inny rekord niż zamierzony.

Zjawisko to pozwoliłem sobie nazwać Fantomową tabelką - ot jako żarcik techniczny będący paralelą (trudne słowo) do Anomalii Transakcji.

Przykład kodu

Managed Bean:
@ManagedBean()
public class UsersControler{ 
 
 @ManagedProperty("#{userFinder}")
 private UserFinder userFinder;
 
 @ManagedProperty("#{userManagement}")
 private UserManagement userManagement;
 
        //Bean o zasięgu sesji pamiętający nasze kryteria wyszukiwania
 @ManagedProperty("#{usersSearchCriteria}")
 private UsersSearchCriteria usersSearchCriteria;

 private List<User> users;
 
 private User selected;
 
 
 @PostConstruct
 public void search(){  
  users = userFinder.findUsers(usersSearchCriteria.getFirstname(), usersSearchCriteria.getLastname(), null, null);  
 } 

 public void remove(){
  userManagement.deleteUser(selected.getId());
  //search(); - zbędne dla zasięgu Request, konieczne dla View aby odświeżyć
 }
 
 public void remove2(Long id){ 
  userManagement.deleteUser(id);
  //search(); - zbędne dla zasięgu Request, konieczne dla View aby odświeżyć
 }   
}

Powyższy ManagedBean ma domyślny zasięg Request (nie chcemy przecież obciążać stanu sesji listą użytkowników).

Nasz bean ma wstrzyknięte 2 obiekty z warstwy aplikacji: UserFinder (wyszukujący użytkowników), UserManagement (operujący na użytkownikach - w przykładzie usuwamy użytkowników, ale problem jest ogólny - generalnie chodzi o jakąkolwiek modyfikację, która zmieni resultat działania UserFinder.findUsers )

Zwróćcie uwagę, iż kryteria wyszukiwania z formularza nie przechowywane w kontrolerze (który ma zasięg Request) a we wstrzykniętym Modelu o zasięgu Sesji.



    
     
     
      imie:  nazwisko: 
       
       
       
           
       
     
   
    
     ID
     #{_u.id}
    
 
    
    
     
      
      
     
                         
    
         
                
    

Widok jest bardzo prosty: tabelka iteruje po kolekcji użytkowników. Dla każdego z nich wyświetla ID (aby organoleptycznie zaobserwować problem) oraz umożliwia usunięcie (w ogólności modyfikację stanu bazy mającą wpływ na listę) użytkownika. Technicznie mamy tutaj dwa podejścia do przekazywania "klikniętego" użytkownika na Serwer: poprzez f:setPropertyActionListener oraz dzięki wywołaniu metody z parametrem.

To czy przekazujemy ID użytkownika czy obiekt klasy User nie ma znaczenia dla eksperymentu.

Dlaczego tak się dzieje

Załóżmy, że podczas pierwszego renderowania strony do pierwszego wiersza tabelki był "podpięty" pierwszy wiersz z wynikowej listy - użytkownik o id = 1.
Załóżmy, że później - po zmianie stanu bazy - pierwszy użytkownik na liście wynikowej pobranej z bazy ma id = 2.

Silnik JSF podczas postback odtwarza drzewo komponentów graficznych. Następnie "rozsmarowuje" na tym drzewie model. Tak więc podczas postback do pierwszego wiersza tabeli może podpiąć użytkownika o id = 2.

Model zdarzeń w JSF (zgodnie z nazwą Java Server Faces) odbywa się w kontekście serwera, czyli silnik ma informację o numerze klikniętego wiersza w tabelce (nie o id klikniętego usera). Dalej na podstawie klikniętego wiersza, zbiera z tego wiersza model i ten model traktuje jako "kliknięty". Jak widać - zakłada optymistycznie, że tak jest:)

Jeżeli przy postbacku model pobrany z bazy będzie inny - trudno... :P


Dlaczego często nie widać problemu

Programiści JSF często z powodu kłopotów z ogarnięciem złożoności zasięgów życia ManagedBeanów decydują się na rozwiązania "bo działa" i rozszerzają zasięg życia niemal wszystkich ich do Sesji. W przypadku zasięgu sesji nie odtwarzamy modelu danych na podstawie bazy lecz na podstawie ViewState zatem problem z klikaniem w fantomy jest po prostu ukryty - nie występuje. Czyli przy okazji, jako skutego uboczny niechlujstwa, zabezpieczamy się przez błędem fantomowych danych:)

Jeżeli zależy nam na zasobach lub świeżości danych, wówczas musimy wysilić się na rezygnację z Sesji gdzie tylko jest to możliwe i zaczyna się zabawa...

Rozwiązania

1. Zasięg View/Session
Przechowywanie list w Sesji to zwykle słaby pomysł, ale możemy zdecydować się na ich przechowywanie w zasięgu View, który trwa dopóki znajdujemy się na tej samej stronie (GET zrywa ten zasięg).
@ManagedBean()
@ViewScoped
public class UsersControler{

Można pokusić się o wydzielenie samego modelu prezentacji do osobnego ManagedBeana o zasięgu View tak aby nie przechowywać tam całego kontrolera, który jest zbędny i powinien żyć w Request. Wówczas wstrzykujemy model do kontrolera, napełniamy go w kontrolerze, po czym kontroler może już "umrzeć".


2. Ukryte Pole

Jeżeli nie możemy pozwolić sobie na przechowywanie stanu widoku, wówczas pewnym rozwiązaniem, jest rezygnacja z mechanizmów JSF do wskazywania klikniętego wiersza. Nasze buttony powinny przy pomocy JS ustawiać ukryte pole formularza na wartość id modelu wiersza i submitować to pole na managedbeana.


//=====================================

Po raz kolejny mamy do czynienia z sytuacją, gdzie próba ukrycia złożoności esencjalnej skutkuje wyciekiem jeszcze większej ilości złożoności przypadkowej.
"...życia nie oszukasz."

piątek, 12 sierpnia 2011

Wygeneruj sobie PDFa - Facelets (JSF) + CSS3 + Flying Saucer

Dzisiejszy post będzie nietypowy: dużo kodu i kilka speszyl-haków:)

Pokażę jak szybko i raczej bezboleśnie generować PDFy na podstawie HTMLa + CSS3 z wykorzystaniem szablonów w wydaniu Facelets.

Założenia

Oczywiście do generowania PDF możemy podejść na wiele sposobów. Możemy np. użyć natywnego api lub nawet zastosować Open Office postawionego w trybie servera (bez GUI) i korzystać ze wszelkich narzędzi do tworzenia dokumentu, aby na końcu "zapisać jako" pdf.

Natomiast w tym poście zakładam, że chcemy rozwiązać sprawę najprościej jak się da, bez potrzeby studiowania autystycznej dokumentacji.

Generujemy PDF

Na początek potrzebujemy biblioteki: http://code.google.com/p/flying-saucer (w paczce mamy ITexta i xercesa). Flying saucer to wspaniałe narzędzie, które przykrywa iTexta wygodnym api: na wejściu podajemy urla do strony, a na strumieniu wyjściowym dostajemy PDFa. Strona może odwoływać się do CSS3, w którym definiujemy nagłówki, stopki, numerowanie stron, podział stron. Wszystko zostanie kolejno opisane w dalszej części.

Pdfy będziemy zwracać z następującego servletu:
@SuppressWarnings("serial")
@WebServlet("/programy_szkolen_pdf/*")
public class PdfServlet extends HttpServlet {
	
	private static final String FONTS_PACKAGE = "fonts";
	private static final String FONTS_DIR = "/" + FONTS_PACKAGE + "/";
	
	protected void doGet(HttpServletRequest request,
			HttpServletResponse response) throws ServletException, IOException {				
		
		response.setContentType("application/pdf");
		OutputStream os = response.getOutputStream();
		
		String url = getUrl(request);

		try {
			ITextRenderer renderer = initRenderer();
			
			renderer.setDocument(url);
			renderer.layout();
			
			renderer.createPDF(os);
		} catch (Exception ex){
			throw new ServletException(ex);
		}
		
		response.flushBuffer();

	}

	private String getUrl(HttpServletRequest request) {
		String pdfServletPath = request.getServletPath();
		
		String cp = request.getContextPath();
		String program = request.getRequestURI();
		
		program = program.replace(cp + pdfServletPath, "");
		program = program.substring(0, program.length() - 4); // cut .pdf
				
		String url = "http://localhost:8080" + cp + program + ".xhtm?pdf=true";
		
		return url;
	}
}

Jak widać API latającego spodka jest proste:
- posługujemy się obiektem rendererm który posiada stan
- ustawiamy mu URL do strony, którą chcemy skonwertować na PDF
- wołamy metodę układającą pdf
- wynik kierujemy do strumienia odpowiedzi servletu.

I to generalnie byłoby wszystko:)

Zostało kilka szczegółów...
Metoda getUrl w moim przypadku robi pewne oszustwo operując na ścieżkach

Użytkownik woła servlet:
/programy_szkolen_pdf/szkolenie-java-start.pdf
btw: zauważcie jak zamapowany jest servlet: @WebServlet("/programy_szkolen_pdf/*")

A servlet wskazuje latającemu spodkowi adres:
http://localhost:8080/strona/szkolenie-java-start.xhtm?pdf=true

Zakładam, że zasoby do renderowania znajdują się na tej samej maszynie, stąd localhost.
Do czego służy parametr pdf dowiemy się za chwilę:)


Mamy jeszcze nieco smutnych szczegółów technicznych w naszym latającym spodku.

Renderera trzeba zainicjować - metoda initRenderer.

Jeżeli w PDF chcemy używać polskich (lub jakichkolwiek innych barbarzyńskich znaków), to musimy zarejestrować w rendereze fonty, których używamy w CSS (Arial, Verdana, cokolwiek). Służy do tego metoda renderer.getFontResolver().addFont
Ja napisałem prosty automat, metodę findFonts(), który automatycznie wyszukuje plików z fontami w *pakiecie* zdefiniowanym w stałej FONTS_PACKAGE. Chodzi o to aby uniknąć konfigurowania ścieżek. Po prostu skanujemy pakiet z class path.
Zatem do odpowiedniego pakietu należy sobie wkopiować (np z systemu) fonty. Oczywiście katalog powinien być niedostępny z sieci aby nie być posądzonym o dystrybucję komercyjnych fontów - jeżeli takowych używacie.

	
	private ITextRenderer initRenderer() throws IOException, DocumentException{
		List fonts = findFonts();
		
		ITextRenderer renderer = new ITextRenderer();
		
		for (String font : fonts) {
			renderer.getFontResolver().addFont(FONTS_DIR + font,
					BaseFont.IDENTITY_H, BaseFont.EMBEDDED);
		}			
		
		return renderer;
	}

	private List findFonts() throws IOException {
		Enumeration resources = getClass().getClassLoader().getResources(FONTS_PACKAGE);

		URL resource = resources.nextElement();
		File directory = new File(resource.getFile());

		List fonts = new ArrayList();
		if (!directory.exists()) {
			return fonts;
		}
		File[] files = directory.listFiles();
		for (File file : files) {			
			fonts.add(file.getName());
		}
		return fonts;
	}


Szablony

Dlaczego Facelets i JSF 2?

Do tej pory różnego rodzaju strony tworzyłem w PHP z wykorzystaniem Smarty. Po kilku doświadczeniach uznałem to za... powiedzmy niewygodne.

Chcę być dobrze zrozumiany - Smarty są ok dla pewnej klasy problemów: np wówczas gdy mamy kontent w bazie danych (jakiś CMS), pobieramy kontent jako wiersze i rozsmarowujemy po po "dziurkach" w szablonie. Wówczas imperatywne API Smarty (w postaci obiektu, na którym wołamy metody definiujące zmienne szablonu) jest jak najbardziej ok.

Natomiast ja w klasach problemów typu strona firmowa, preferuję przechowywać kontent (strzępy konwentu) w plikach XHTML. W takim wypadku praca z imperatywnym API to po prostu pomyłka. Jako "weteran" aplikacji webowych czuję się jak ryba w wodzie poruszając się w kodzie. Baza i CMS jedynie mnie ogranicza.

Dlatego tworząc nową wersję strony formowej zdecydowałem się na coś... delikatnie mówiąc... zastawiającego. Użycie JSF - standardu prezentacji enterprise (która ma swoich fanów jak i anty-fantów) do stworzenia strony www :O

Okazało się, że jeżeli nie używać formularzy (pomijamy większość faz cyklu obsługi żądania), nie używać ManagedBeanów, a jedynie składania kawałków XHTMla to okazuje się, bo jest to bardzo produktywne narzędzie.

Przykład:
strona http://bottega.com.pl/szkolenie-jsf-2
wygląda jak każdy widzi.

Jest wyraźnie podzielona na kontent merytoryczny i ozdobniki w postaci nagłówka, stopki i paneli bocznych.

A tak wygląda jej kod:

	
	
		
			
	

	Java Enterprise Edition

	JSF 2
	
		Java Server Faces 2

	Projektanci, programiści

	2 dni

	50% wykłady / 50% warsztaty

			
	
		

Program został skontrowany tak aby przedstawić zagadnienia w kontekście konkretnych problemów, które ilustrują praktyczne wykorzystanie każdej z funkcjonalności JSF. Program szkolenia zawiera rozszerzenie o najlepsze praktyki projektowe i architektoniczne. Tematyka szkolenia obejmuje wszystkie cechy frameworka JSF 2.0 istotne z punktu widzenia developera aplikacji.


Znaczniki ui:definie definiują wartości zmiennych, które będą używane w szablonie.
Wartości to czysty kod XHTML, który jest na bieżąco validowany przez edytor.

Stronę możemy zawołać na 3 sposoby:
- podając jej url: http://bottega.com.pl/szkolenie-jsf-2
- renderując ją do pdf: http://bottega.com.pl/programy_szkolen_pdf/szkolenie-jsf-2.pdf (wówczas nasz servlet przekaże adres http://bottega.com.pl/szkolenie-jsf-2.xhtm?pdf=true) do Flying Saucera
- zagnieżdżając w pływającej ramce na facebooku podając adres http://bottega.com.pl/szkolenie-jsf-2.xhtm?fb=true

W każdym z tych przypadków oczekujemy innego wyglądu.

Zatem znacznik ui:composition wskazuje szablon: template="/layout/training_template.xhtml"

Plik ten układa w odpowiednich miejscach parametry szkolenia. Szablon ten odpowiada jedynie za fragment strony (układ info o szkoleniu), ale nie zawiera np. nagłówka, stopki, ani bocznych boxów.



		Szkolenie: 
		

	
		

Informacje ogólne


Jak widać, szablon ten definiuje 2 zmienne: title i body. Wartości tych zmiennych zawierają w sobie XHTMLa oraz wartości zmiennych z dekorowane pliku - znacznik ui:insert.

Warunkowe renderowanie

Pomarańczowy przycisk "Zapytaj o szkolenie" jest niepożądany w przypadku wersji PDF i facebook, dlatego został warunkowo renderowany, przy pomocy panelGroup i atrybutu rendered:



Bean UrlChecker będzie opisany już za chwilę.


Wybór szablonu docelowego

Szablon szkolenia, nie jest szablonem "ostatecznym". Jest on dekorowane znowuż przez szablon wybierający szablon główny: template="/layout/dispatching_template.xhtml"

Jest to warstwa pośrednia pozwalająca w jednym miejscu przechowywać kod, który potencjalnie ulegnie zmianie. Tak oto wygląda dispatching_template.xhtml:


	


Jak widać nie dodaje żądnej wizualizacji a jedynie deleguje dekorowanie do kolejnego szablonu. Z tym, że nie mamy tutaj konkretnej ścieżki a jedynie wyrażenie wyliczane w Javie (tak w normalnym języku programowania a nie w jakimś pokracznym języku opartym o znaczniki):

@ManagedBean
public class UrlChecker {

	public String getTemplateName(){
		if (isPdf())
			return "template_pdf";
		
		if (isFacebook())
			return "template_facebook";
		
		return "template";
	}
	
	public boolean isStandard(){
		return ! (isPdf() || isFacebook());
	}
	
	public boolean isPdf(){		
		return "true".equals(getParameter("pdf"));
	}
	
	public boolean isFacebook(){		
		return "true".equals(getParameter("fb"));
	}
	
	private String getParameter(String name){
		ExternalContext externalContext = FacesContext.getCurrentInstance().getExternalContext();				
		return externalContext.getRequestParameterMap().get(name);		
	}
	
	
}

Zatem jeżeli zawołamy naszą stronę w ten sposób:
http://bottega.com.pl/szkolenie-jsf-2.xhtm
to w wyniku otrzymamy zawartość dekorowaną szablonem template_pl, który zawiera banner, stopę i boczne boxy

jeżeli natomiast w ten sposób:
http://bottega.com.pl/szkolenie-jsf-2.xhtm?pdf=true
to szablon dekorujący template_pdf_pl doda nagłówek i stopkę dla dokumnetu

a takie wywołanie
http://bottega.com.pl/szkolenie-jsf-2.xhtm?fb=true skutkuje zastosowaniem niemal niewidocznego szablonu dla facebooka.


Podsumowanie dekorowania


Mamy zatem następującą hierarchię treści:

szablon: szablon www | szablon pdf | szablon facebook
szablon: dispatcher
szablon: training
strona: konkretne szkolenie

Czyli na najwyższym poziomie mamy trzy perspektywy (szablony) spojrzenia na dane merytoryczne zawarte w plikach opisujących szkolenie.

Inne strony nie będące programem szkoleń po prostu pomijają szablon training_template i są dekorowane wprost szablonem dispatcher template


Układ dokumentu PDF w HTML i CSS3

Składanie stron w HTML jest dosyć łatwe. Wręcz banalne w porównaniu z natywnym API lub autyzmem Open Office.

Szablon pdf to zwykły XHTML:
<head>


</head>

<body>
  
!!! TUTAJ trzy warstwy opisane poniżej !!!
<</body>

Style po komentarzy "Specific Elements" są *kluczowe*.


Wewnątrz pierwszej warstwy mamy kolejno kolejno:

nagłówek:


stopkę:


zawartość:

Program szkolenia




Na zakończenie bonus dla wytrwałych

Jak wołać w JSF 2 metody z parametrem? Zapewniam, że może się przydać w bardziej wyrafinowanych szablonach:)

Tak robimy to w Seam od 5 lat:




Natomiast w czystym JSF trzeba wykonać kilka prostych kroków aby uzyskać możliwość stosowania powyższej składni:
http://ocpsoft.com/java/jsf2-java/jsf2-how-to-add-the-magic-of-el-el2-to-jsf/

//====================================

Mam nadzieję, że udało mi się pokazać, że w Javie można efektywnie a przy tym czysto i elegancko zbudować stronę.

Przy innej okazji pokażę jak Filtry Servletów w dosłownie kilku linijkach biją na głowę autystyczne htaccess:)

A Smarki?... Już nigdy więcej:)

środa, 21 kwietnia 2010

Google Application Engine



Dziś będzie o rzetelności.

WYGLĄDA OBIECUJĄCO

Chmura Google - GAE ogólnie prezentuje się świetnie. Za darmo (do pewnych limitów) dostajemy platformę, która jest w stanie udźwignąć tysiące transakcji na sekundę oraz przechować dla nas terabajty danych w nierelacyjnej bazie BigTable. Do tego dostęp do wielu usług google takich jak bezpieczeństwo, składowanie obrazków, pobieranie zawartości stron poprzez ichni cache, potężny memcache itp.

Programować możemy sobie w Pythonie albo w Javie (okrojonej wersji, bez części klas standardowych i z zakazem korzystania z pewnych bibliotek).

Ogólne założenie jest takie: programiści (i konsultanci;) w większości nie potrafią konfigurować skalowalnych środowisk ani pisać skalowalnych aplikacji, dlatego dostarczmy im platformę, która zajmie się tym problemem w sposób transparentny. Czyli mamy podejście odwrotne do Java EE. Zajmij się problemem biznesowym a chmura zajmie się skalowaniem. Kierunek wydaje się słuszny.

Zatem zaczynamy zabawę...
Na dzień dobry krótka, oficjalna prezentacja z głównej strony jak to fajnie i szybko klepie się aplikacje i odpala je w kosmos: http://www.youtube.com/watch?v=P3GT4-m_6RQ
Trzeba przyznać, że plugin do Eclipse jest wzorowy: prosty w obsłudze (3 sugestywne buttony), działa oraz zapewnia wszystko:
- lokalny server emulujący chmurę i jej zasoby (bazę)
- narzędzie automatycznie "wzbogacające" bytecode klas encji na potrzeby ichniej "implementacji" JPA
- narzędzie wystrzeliwujące aplikację w kosmos
- prosty kreator, który generuje cały ten kosmodrom
(nie odważyłem się zmawenizować wygenerowanego projektu, bo to zwykle tydzień w plecy a na koniec może się okazać, że tracimy hotdeploy)



WTF?

Ale, ale... akcja filmu rozkręca się z czasem...
Jedna z największych firm programistycznych na ziemi pokazuje jak tworzyć kod w... Java Server Pages. Patrzcie uważnie i uczcie się: logika i dostęp do danych w... scriptletach (sic!).

Oczywiście duzi chłopcy wiedzą, ze scriptlety są depricated. A nóż widelec tą scenę zobaczy jakieś małe dziecko i będzie później tak programować jak dorośnie?!? Później znowu przeczytamy w gazecie o tragedii jaka rozegrała się w jakimś spokojnym projekcie. Gdzie byli rodzice?!?

Hmmm dziwne, dziwne... no ale nie czepiajmy się szczegółów, to przecież tylko taki przykład. Przecież zawsze możemy zaprojektować sobie eleganckie warstwy i całość oprzeć o sprawdzony framework.



BOLESNE OGRANICZENIA

Niestety tak jak wspomniałem na początku JVM ma ograniczenia i nie wszystko działa. Seam co prawda daje się uruchomić po zastosowaniu 15 speszyl haków, ale wszyscy twierdzą, że nie warto ryzykować. Na szczęście stary, dobry Spring oficjalnie działa. Wielu dzielnych blogerów publikuje hello worldy i tutoriale jak to poskładać. Wygląda na to, że jedni kopiują od drugich;)

Ba, nawet 2 książki o GAE preferują Springa i pokazują jak go skonfigurować. Hmmm ciekawe dlaczego omawiają tylko servlecik Spring MVC i dyplomatycznie nie poruszają bardziej zaawansowanych zagdanień...?

Niestety, jednak nie działa do końca!
Przykładowo gdy użyjemy technik Aspektowych i przeplatania w czasie uruchomienia to mamy dziwne błędy ClassNotFound. Lokalnie działa, a na chmurze już nie.
Co implikuje, że możemy zapomnieć o aspektowej, eleganckiej obsłudze bezpieczeństwa i transakcji.
Więc po co nam Spring? Tylko do wstrzykiwania? To bez sensu.



UWAGA NA PERSYSTENCJĘ

Nieważne, bez Springa można żyć, idźmy dalej... Nierelacyjna baza BigTable została przez Google przykryta interfejsem JDO albo JPA (do wyboru) więc czujemy się jak w domu.
Nie do końca... specyfikacja ma szereg ograniczeń wynikających z architektury rozwiązania BigTable - chodzi o rozpraszanie struktur danych na różne maszyny po stronie chmury.

Drobny szczególik wspomniany jedynie w dokumentacji jest taki, ze agregaty encji są grupowane w rodziny. Przykładowo jeżeli mamy encję Zamówienie, która ma w sobie listę ZamówionychProduktów to jest to rodzina. Co z tego wynika?

Encja może należeć jedynie do jednej rodziny.
Implikacja: Jeżeli mamy encje Zamówienie i Faktura oraz encję Kleint, który musi być podpięty do Zamówienia jak i Faktury to niestety nie ma takiej możliwości. Dziecko może mieć jednego rodzica. Oczywiście można to obejść i w Zamówieniu jak i Fakturze używać obiektów Kluczy do Klienta. Ale wówczas tracimy łatwość programowania i uzależniamy się od API Chmury.

Trzeba pamiętać, że podejście do modelowania danych w nierelacyjnej bazie wymaga innego "nastawienia" - taka natura rozwiązania. Jeżeli nierelacyjnie to może obiektowo? Jednak nie do końca - no to jak?

Osobiście preferuję takie podejście, że Zamówienie czy Faktura ma owszem klucz do Klienta ale ma również niepersystentne pole Klient, które jest ustawiane w niej osobnym zapytaniem (nie jest to duży koszt w bazie BigTable, która jest tak na prawdę wielowymiarową HashMapą). Tego typu szczegóły można hermetyzować z obiektach Repository, które zwracają dane "poskładane" tak aby było wygodnie. OK, jeden problem z głowy.


Transakcja może obejmować tylko operacje na jednej rodzinie.
Czyli jeżeli logika aplikacji chce zapisać zarówno rodzinkę Zamówienie jak i Faktura to będą to 2 osobne transakcje. A co jeżeli w tak zwanym międzyczasie zdarzy się coś przykrego? Ręczna rzeźba i sprzątanie:)
Poza tym, jak wspomniałem, aspektowe transakcje Springa nie działają, więc brudzimy kod biznesowy kodem API transakcji JPA.


Datanucleus - "implementacja" JPA
Oprócz tego, że jest okrojona to dodatkowo zawiera szereg błędów - nie działają nawet niektóre proste przykłady z tutoriali JPA. Na oficjalnej grupie wisi wiele pytań - bez odpowiedzi.



GWÓŹDŹ

Prezentacja
Z frameworków prezentacji testowałem JSF.
1.2 (z Seam) działa.
2.0 działa po zastosowaniu jednego prostego speszyl haka.

Działa w sensie, że się uruchamia. W 2.0 występują pewne problemy z wstrzykiwaniem ManagedBeanów. RichFaces ani IceFaces nie mają wersji dla chmury. Oficjalnie Primefaces częściowo działa. Znowu nie jest to rzetelna i informacja - AJAX ogólnie się sypie.

No i można zapomnieć o uruchamianiu metod z parametrem przy pomocy nowego silnika wyrażeń (el-api w wersji 2.2). Chmura odmawia współpracy z nowymi zdobyczami.

//==============================

Biorąc pod uwagę powyższe ograniczenia:
- brak wygodnego frameworka (w tym wsparcia dla AOP)
- uboga funkcjonalność ORM wymagająca wielu ręcznych operacji
- mentalny model programowania sprzed o ok 10 lat
Mamy znaczny spadek produktywności.

Domyślamy się skąd te JSP, scriptlety i statyczne metody w filmie promocyjnym? Czyżby tylko to działało?
Szkoda tylko, że community i blogosfera zatrzymuje się na poziomie Hello World i nie weryfikuje nieco bardziej dogłębnie i rzetelnie.

Na zakończenie dodam, że zupełnie inne założenia przy budowie chmury przyjął Amazon. Dostajemy sprzęt i system operacyjny. Nikt nie narzuca nam dodatkowych bytów zbudowanych nad sprzętem i nie wchodzi nam z butami w nasz wypracowany latami i sprawdzony model programowania. I co najważniejsze - wszystko działa:)

niedziela, 29 listopada 2009

Seam == JSF + EJB ?

WSTĘP

Framework Seam opiera się na bardzo silnym mechanizmie - bijekcji.

Bijekcja to nowa jakość w technice Dependency Injection, która pozwala na budowanie bardziej naturalnych konstrukcji Inversion on Control.

/**
 * Na marginesie dodam, że DI i IoC to nie jest to samo.
 * Wyjaśnienie różnic w tym przydługim poście:
 * Wprowadzenie do wstrzykiwania zależności i Springa zarazem
 */

Bijekcja w skrócie przedstawia się następująco:
- podczas wywołania metody
- następuje wstrzyknięcie zależności do obiektu, którego metoda jest wołana (pola adnotowane @In)
- metoda jest wykonana
- następuje wystrzyknięcie obiektów do kontekstu Seam (pola adnotowane @Out)
- następuje czyszczenie (nulllowanie) wstrzykniętych zależności

Od strony technicznej odbywa się to w Seam dzięki specjalnym interceptorom. Przechwytują one wywołania metod wszystkich komponentów i wokół tych wywołań dodają całą opisaną powyżej magię.

Koncepcja jest jak już napisałem piękna, ponieważ pozwala na budowanie dużo bardziej naturalnych konstrukcji. Wstrzyknięcia następują per wywołanie metody a nie jedynie raz, po stworzeniu komponentu - zaspawane na wieki wieków (albo przynajmniej do restartu servera z powodu zwisu aplikacji;)
Dodatkowo niemal bez ograniczeń możemy wstrzykiwać w siebie komponenty o różnych zasięgach - nie ma ograniczenia szerszy zasięg w węższy. Dlatego, że wstrzyknięcie następuje jedynie na czas wywołania metody i nie będziemy mieli nigdy problemu z nieświeżą instancją komponentu.


PROBLEM

Twórcy frameworka poszli jeszcze dalej...
Umożliwiają używanie Sesyjnych EJB wprost w JSF - bez potrzeby pośredniej warstwy Managed Beanów (lub ich specyficznej odmiany Backing Beanów). Czyli komponenty Seam, które są jednocześnie komponentami EJB będą widoczne w kontekście JSF.
Managed Beany są podobno zbędą warstwą, która tylko przeszkadza. Możliwość wołania EJB z JSF jest rzekomo tryumfem rozumu na platformą korporacyjną.

Hmmm w bardzo prostych systemach klasy "Przeglądarka Bazy Danych" rzeczywiście można by się obyć bez MB, ale w niniejszym poście przedstawię do jakich kuriozalnych konstruktów dochodzi gdy w nietrywialnych przypadkach wiążemy JSF wprost z EJB.

Na wstępie zaznaczam, że na potrzeby niniejszych rozważań pomijamy aspekty warstwowej architektury, elementarnych zasad projektowania mówiących o kohezji klasy i temu podobnych staromodnych ograniczeniach. Skupiamy się na "produktywnym" kodowaniu na wyścigi rodem z najlepszych tutoriali i książek.

Zaczynamy.
Przykład prosty, klasyczny ekran prezentujący listę czegoś.
Wymagania:
Po ordynarnym wejściu na stronę przez GET chcemy aby lista wyświetlała wszystkie dane.
Widok ma pozwalać również na wyszukanie czegoś po jakimś atrybucie - czyli wpisanie szukanego słowa w pole tekstowe i naciśnięcie buttonu z zaokrąglonymi rogami "Szukaj" (POST gwoli ścisłości).




Na początek implementujemy pierwsze wymaganie: po wejściu na stronę widzimy listę wszystkiego...

Strzępek kodu widoku:

<h:dataTable value="#{listaCzegos}" var="_cos">
<h:column>#{_cos.nazwa}</h:column>
...
</h:dataTable>


Stateless Session Bean, który dostarcza danych dla widoku:


@Stateless
@Name("cosProwajder")
public class CosProwajderBean implements CosProwajderLocal{
@Out
private List listaCzegos;

@Factory("listaCzegos")
public void initListaCzegos(){
listaCzegos = //pobranie danych
}
}


Co się tutaj dzieje: JSF rząda komponentu listaCzegos. Seam widzi, że nie istnieje on w kontekście, ale na szczęście znalazł ochotnika, który go sfabrykuje - metodę otagowaną adnotacją @Factory("listaCzegos"). Metoda zostaje wywołana, metoda ustawia pole prywatne, a ponieważ pole jest adnotowane @Out to po chwili jest wystrzykiwane do kontekstu. Dzięki temu JSF "widzi" listę i może ją już teraz spokojnie renderować w tabelce.

Kod Session Beana mógłby równie dobrze wyglądać tak:

@Stateless
@Name("cosProwajder")
public class CosProwajderBean implements CosProwajderLocal{

@Factory("listaCzegos")
public List initListaCzegos(){
listaCzegos = //pobranie danych
return listaCzegos;
}
}


Ale już śpieszę wyjaśnić skąd poprzednia konstrukcja. Mam już w zamyśle spełnienie drugie wymagania - funkcjonalności wyszukiwania. Zatem na widoku pojawi się pole tekstowe i button:


<h:form>
<h:inputText value="#{searchFilter.name}" />
<h:commandButton action="#{cosProwajder.search}" />
<h:form>


Nasz Session Bean dostanie metodę search, która wyszuka dane na podstawie wstrzykniętych kryteriów, następnie wynik ustawi w prywatnym polu, z którego to po chwili wartość zostanie wystrzyknięta do kontekstu Seam, skąd JSF będzie ją widział.


@Stateless
@Name("cosProwajder")
public class CosProwajderBean implements CosProwajderLocal{
@Out
private List listaCzegos;

@In
private SearchCriteria searchCriteria;

@Factory("listaCzegos")
public void initListaCzegos(){
listaCzegos = //pobranie danych
}

public void search(){
listaCzegos = //pobranie danych na podstawie searchCriteria
}
}


Jeszcze tylko dla ścisłości komponent przechowujący kryteria wyszukiwania. Zasięg PAGE aby kryteria były widoczne po przeładowaniu strony:

@Name("searchCriteria")
@Scope(PAGE)
public class SearchCriteria implements Serializable{
private String name;
//getter i setter
}



Pytanie: co jest nie tak z poniższym kodem?

@Stateless
@Name("cosProwajder")
public class CosProwajderBean implements CosProwajderLocal{
@Out
private List listaCzegos;

@In
private SearchCriteria searchCriteria;

@Factory("listaCzegos")
public void initListaCzegos(){
listaCzegos = //pobranie danych
}

public void search(){
listaCzegos = //pobranie danych na podstawie searchCriteria
}
}


Dla ułatwienia wyrzucę linijki odwracające uwagę i zaznaczę kluczowy element:
(To przecież nie jest egzamin na certyfikat - chcemy się tu dowiedzieć czegoś pożytecznego)


@Stateless //<<-------------
public class CosProwajderBean implements CosProwajderLocal{
private List listaCzegos;

private SearchCriteria searchCriteria;

public void search(){
listaCzegos = //pobranie danych na podstawie searchCriteria
}
}


Właśnie!
Niby mamy bezstanowy komponent, ale korzystamy z niego w stanowy sposób!

Wyobraźmy sobie, że nasz wspaniały komponent biznesowy jest tak genialny, że chcemy go wykorzystać jeszcze gdzieś poza JSF, np wywołać zdalnie. Musimy wówczas:
1. ustawić kryteria wyszukiwania (zakładając, że mamy setter)
2. odpalić metodę search(), która zmieni stan - tu jest ta nieszczęsna stanowość
3. odebrać wynik przez getter

Czyli JSF wymusza na nas styl "strzelania z muszkietu": załaduj i wypal.

Czy dałoby się wykorzystać mimo wszystko ten super-kod poza Seam, np przykrywając go fasadą:


@Stateless
public class CosProwajderFacadeBean implements CosProwajderFacadeRemote{
@Ejb
private CosProwajderLocal cosProwajder;
public List search(SearchCriteria searchCriteria){
cosProwajder.setSearchCriteria(searchCriteria);
cosProwajder.search();
return cosProwajder.getListaCzegos();
}
}

Niestety NIE, ponieważ nie mamy gwarancji, że kontener JEE przy każdym z 3 wywołań bezstanowego komponentu zaserwuje nam tą samą instancję!


Dlaczego ten bezsensowny kod działa w ogóle w Seam? Tak jak napisałem na wstępie - interceptory Seam. Jeden z nich przechwytuje wołanie metody search na komponencie, wstrzykuje kryteria, wykonuje metodę, wystrzykuje wynik. Ponieważ wstrzykiwanie i wystrzykiwanie nie są wywołaniem metod biznesowych bezstanowego komponentu to interceptor cały czas operuje na tej samej instancji.


ROZWIĄZANIE

Łatwo można rozwiązać problem sensowności kodu zmieniając bean bezstanowy na stanowy. Jednak wciąż mamy problem z bezsensownością logiczną. Dlaczego jakiś komponent będący de facto wrapperem dla procedury ma być stanowy?

Owszem w pewnych sytuacjach stanowość może mieć sens, np: z przyczyn wydajnościowych komponent stanowy trzyma wynik jako jakiś kursor po stronie bazy. Klient Stanowego Komponentu Sesyjnego przegląda listę wynikową po kawałku. Tę argumentację zaliczam.

Innym usprawiedliwieniem może być chęć wybrania (kliknięcia) wiersza - z technicznych powodów musi wówczas przechować listę. Innym jeszcze usprawiedliwieniem może być naiwna paginacja, która w naiwnych paginatorach działa na danych sesyjnych pobranych w całości z bazy.
Czy jednak sensowne jest dopasowywanie API komponentów biznesowych do takich szczegółów technicznych jakiś frameworków prezentacji?
Poza tym wciąż będziemy mieli kuriozalne korzystanie z niego w fasadzie - w stylu "strzelania z muszkietu": załaduj i wypal.


PRAWDZIWE ROZWIĄZANIE

Aby nasze komponenty biznesowe miały sensowny interfejs w nietrywialnych przypadkach musimy ponieść ten niesamowity trud wprowadzenia warstwy jakiś Managed Beanów - w Seam zwanych Akcjami.

Są to zwykłe POJOs, które mają API w stylu "strzelania z muszkietu" a logikę biznesową delegują do EJB:


@Name("cosControler")
public class CosControler{
@In //EJB
private CosProwajderLocal cosProwajder;

@Out
private List listaCzegos;

@In
private SearchCriteria searchCriteria;

@Factory("listaCzegos")
public void initListaCzegos(){
//używamy pustych kryteriów (w celu optymalizacji można by wynieść je do singeltona)
listaCzegos = cosProwajder.search(new SearchCriteria());
}

public void search(){
//Wywolanie EJB
listaCzegos = cosProwajder.search(searchCriteria);
}
}


Jest to również doskonałe miejsce do wstrzyknięcia np kontekstu FacesMessages, kontekstów Seam, parametrów Request, bindowanie UIComponent i innych zależności typowych dla technikaliów frameworków. W tej warstwie możemy sobie na to śmiało pozwolić i dzięki niej nie musimy brudzić EJB zależnościami od Seam i JSF.

//==========================

Sama możliwość wołania Sesyjnych EJB z JSF jest oczywiście bardzo wygodnym ficzerem i warto czasem z niej korzystać. Ale tylko wówczas gdy ma to sens i jest racjonalnie uzasadnione.

Tworzenie komponentu biznesowego, który jest "zbrukany" stylem i zależnościami pewnych technologii powoduje, że nie jest to już ani komponent ani biznesowy. Komponent - czyli pewna reużywalna część; biznesowy - czyli zajmujący się jedynie logiką biznesową.