Archiwum

Archive for Maj 2010

Klejenie stringów na przykładzie JPA

26/05/2010 2 Komentarze

Jakiś czas temu miałem okazje pracować z JPA. Nie mam z tym dużego doświadczenia więc nie będę się tu rozwodził o niuansach tej technologii. Opisze tylko jak można się zmierzyć z niektórymi problemami jakie powstają przy użyciu tego. Będzie to dotyczyć użycia JPQL do generowania wielowarunkowych zapytań. Może być to wykorzystywane przy prostych wyszukiwarkach jakie spotyka się po stronie GUI.
W takim przypadku mamy zwykle zdefiniowany filtr(specyfikacje), w którym typowe kryteria to: wyszukiwanie w tekście (klienta po imieniu), następnie może być zakres (czas rejestracji klienta) oraz jeszcze wyszukiwanie po jakiejś kategorii (typ klienta), dalej mogą iść jeszcze inne warunki. Tak więc w skrajnym przypadku dawać to może dość pokaźne zapytanie, przykład (choć niewielki):

Query query = em.createQuery(
"SELECT u FROM User u WHERE u.name like :name and u.type=:type and u.registerDate > :registerDateFrom and u.registerDate < :registerDateTo");
query.setParameter("name", name);
query.setParameter("type", type);
query.setParameter("registerDateFrom", registerDateFrom);
query.setParameter("registerDateTo", registerDateTo);
List results = query.getResultList();

Głównym wyzwaniem jest tu odpowiednie sklejenie stringa reprezentującego zapytanie, na bazie którego zostanie stworzony obiekt Query i dostarczenie do niego parametrów. Ponieważ użytkownik na GUI nie musi wypełniać wszystkich kryteriów, to w zależności od tego co dostarczy trzeba złożyć odpowiednie zapytanie, a potem dostarczyć właściwe parametry. Różnie można sobie z tym radzić, ja spotkałem się z poniższym podejściem:

	    
	    String queryStart = "SELECT u FROM User u";
        StringBuffer whereStrBuf = new StringBuffer();
        Map<String, Object> params = new HashMap<String, Object>();

		// name 
		if (isNotEmpty(name)) {
			addConditionsPrefix(whereStrBuf);
			whereStrBuf.append("u.name like :name");
			params.put("name", "%"+name+"%");              
		}
		// type
		if (type != null) {
			addConditionsPrefix(whereStrBuf);
			whereStrBuf.append("u.type =:type");
			params.put("type", type);      
		}
		// registerDateFrom
		if (registerDateFrom != null) {
			addConditionsPrefix(whereStrBuf);
			whereStrBuf.append("u.registerDateFrom >:registerDateFrom");
			params.put("registerDateFrom", registerDateFrom);               
		}
		// registerDateTo 
		if (registerDateTo != null) {
			addConditionsPrefix(whereStrBuf);
			whereStrBuf.append("u.registerDateTo <:registerDateTo");
			params.put("registerDateTo", registerDateTo);               
		}
            
        String queryStr = new StringBuilder().append(queryStart).append(whereStrBuf).toString();     
        
        Query query = em.createQuery(queryStr);
        for (Map.Entry<String, Object> entry : params.entrySet()) {
            query.setParameter(entry.getKey(), entry.getValue());
        }
		List results = query.getResultList();

Brakujące metody to:

	private void addConditionsPrefix(StringBuffer whereStrBuf) {
		if (whereStrBuf.length() == 0)
			whereStrBuf.append(" where ");
		else
			whereStrBuf.append(" and ");
	}

	public boolean isNotEmpty(String value) {
		return (value != null && value.trim().length() > 0);
	}

Jest to poprawne rozwiązanie i przy wielokrotnym stosowaniu takiego podejścia można się w nim poruszać sprawnie, wiedząc co gdzie jest. Dla mnie problematyczne było tu przeplatanie się kodu z dwóch różnych dziedzin. Kod typowo biznesowy, związany z samym zapytaniem (o co pytamy i po jakich kryteriach), występował razem z kodem technicznym, odpowiedzialnym za proces tworzenia zapytania.
Po pewnych przekształceniach udało mi się doprowadzić do odseparowania tego i kod wyglądał tak:

	QueryAssembelr qa = new QueryAssembelr("SELECT u FROM User u");
	// name
	qa.addStringCondition("u.name like :", "name", name);
	// type
	qa.addObjectCondition("u.type =:", "type", type);
	// registerDateFrom
	qa.addObjectCondition("u.registerDateFrom >:", "registerDateFrom", registerDateFrom);
	// registerDateTo
	qa.addObjectCondition("u.registerDateTo <:", "registerDateTo", registerDateTo);

	Query query = qa.preperQuery(em);
	List results = query.getResultList();

Teraz sama treść zapytania jest bardziej zwarta i widać od razu o co chodzi. Techniczna część została schowana do pomocniczej klasy:

public class QueryAssembelr {

	StringBuffer whereStrBuf = new StringBuffer();
	HashMap<String, Object> params = new HashMap<String, Object>();

	String queryStart;

	public QueryAssembelr(String queryStart) {
		this.queryStart = queryStart;
	}

	public QueryAssembelr addStringCondition(String condition, String key, String value) {
		if (isNotEmpty(value)) {
			addCondition(condition, key, '%' + value + '%');
		}
		return this;
	}

	public QueryAssembelr addObjectCondition(String condition, String key, Object value) {
		if (value != null) {
			addCondition(condition, key, value);
		}
		return this;
	}

	public Query preperQuery(EntityManager em) {
		String queryStr = new StringBuilder(queryStart).append(whereStrBuf).toString();

		Query query = em.createQuery(queryStr);
		for (Map.Entry<String, Object> entry : params.entrySet()) {
			query.setParameter(entry.getKey(), entry.getValue());
		}
		return query;
	}
	
	private void addCondition(String condition, String key, Object value) {
		addConditionsPrefix(whereStrBuf);
		whereStrBuf.append(condition).append(key);
		params.put(key, value);
	}

}

Przedstawione tu rozwiązanie nie jest może najbardziej efektywne (w szczególności można byłoby się pokusić o wyeliminowanie potrzeby przekazywania dwóch stringów), ale sądzie że oddaje kierunek w którym można pójść. Cześć techniczna jest schowana do jednej klasy, tak więc ewentualne globalne zmiany w wielu zapytaniach mogą być wprowadzane łatwiej, np: ograniczenie liczby zwracanych wyników, czy jak w tym przypadku wyszukiwanie po tekście zawiera znak ‚%’.

Szukając informacji do tego postu trafiłem na ten trochę wiekowy artykuł. Jest tu przedstawienie obejścia się z tym problemem w inny sposób poprzez Criteria API, dostępne szerzej teraz także od specyfikacji JPA 2.0. Nie będę porównywał tego rozwiązania w stosunku do JPQL. Sądzę, że można stworzyć podobną implementacje QueryAssembelr dla tego drugiego podejścia (choć w tym wypadku ograniczałoby się to tylko do schowania if_ów). Użycie wersji z JPQL jest jednak bardziej przejrzyste ponieważ tworzone zapytanie jest lepiej widoczne.

Kategorie:Java Tagi: ,

Trik z ładowniem klas

15/05/2010 Dodaj komentarz

Wprowadzenie
Ostatnio udało mi się zrobić ciekawy użytek z tego jak java ładuje klasy w trakcie życia aplikacji. Proces ten nawet zwięźle jest opisany tutaj. Dotychczas mogłem przypominać sobie o tym kiedy musiałem walczyć z tym. Na przykład kiedy na serwerze aplikacyjnym zaplątały się dwie takie same biblioteki, ale w różnych wersjach. Dobrze było kiedy coś takiego objawiało się wyjątkiem, w gorszym wydaniu mogło otrzymywać się informacje o tym, że coś nie działa tak jak powinno i kombinuj co jest nie tak, zwykłe „u mnie działa” nie pomagało.

Opis problemu
Jednak znalazły się okoliczności kiedy można wykorzystać te „specyficzne” właściwości aby ułatwić sobie życie. Miałem problem z wykorzystaniem zewnętrznej biblioteki. W trakcie działania aplikacja sypała warning_ami. No i z faktu pojawiania się tego mogłem wiedzieć tylko tyle że coś tą zewnętrzną bibliotekę boli, ale co jest przyczyną tego to już nie bardzo.

Dochodzenie do rozwiązania
Więc na początek google i szukanie czy komuś coś takiego już się objawiło. W idealnym świecie powinien być to post o potędze internetu i poprawnym poszukiwaniu informacji w nim, ale tą drogą nie wiele znalazłem. Dowiedziałem się, że inni też spotykają się z czymś takim, a jedyne znalezione rozwiązanie tego problemu, to odpowiednia zabawa z poziomem logowania i zamiatanie tego pod dywan 😉
No, ale można się oprzeć na idei wolnego oprogramowania. Jako, że zewnętrzna biblioteka miała dostępne źródła, to pobrało i podłączyło się je pod projekt w eclipsie. Tak więc teraz znalazłem tą linijkę, w zewnętrznym kodzie, gdzie występował warning. Uruchamiając aplikację w trybu debug mogłem widzieć więcej co jest nie tak.
I to w zasadzie wystarczało do rozwiązania konkretnego przypadku. Jednak ja takich przypadków miałem wiele i rozwiązanie każdego z nich wymagało zmiany w różnych miejscach. Tak więc praca w trybie debug wyglądała by męcząco, gdybym chciał rozwiązać każdy przypadek w taki sposób. Jak na razie dowiedziałem się o klasie występujących problemów, teraz potrzebowałem tylko pozyskiwania zwięzłych informacji w każdym przypadku. Idealnie byłoby gdyby warning_i, od których to wszystko się zaczęło, niosły te informacje. Jednak budowanie ze źródeł feralnego jara z rozszerzonymi informacjami w logach wydawało mi się przesadą, dlatego postanowiłem użyć triku z ładowaniem klas.
Miałem źródła zewnętrznej klasy, w której chciałem rozszerzyć logowanie. Więc w ramach kodu projektu nad którym pracowałem stworzyłem odpowiedni pakiet i w nim kopie tej klasy ze zmianami jakie chciałem. Dzięki temu teraz kiedy uruchamiałem aplikacje „moja zewnętrzna” klasa była ładowana przed oryginałem i mój kod był wykonywany dostarczając mi dodatkowych informacji, które musiałbym pozyskiwać w czasochłonnym trybie debug :).

Wnioski
Co z tego można wyciągnąć na przyszłość?:

  • Czasem to co nam uprzykrza życie, w innych przypadkach może ułatwiać;
  • Jest to rozwiązanie, które polegało na dodaniu tymczasowego nieprodukcyjnego kodu aby ułatwić sobie pracę. Robiąc podmianę klas jako stałe rozwiązanie, zastanowiłbym się dobrze czy na pewno nie mogę zrobić tego samego w bardziej czysty sposób, tak aby nie zaszkodzić samemu sobie w przyszłości.

Jeszcze na koniec o samej istocie rozwiązywanego tu problemu. Był on związany z logowanie, ostatnio ukazała się seria ciekawych artykułów związanych z tym zagadnieniem, a opisany przypadek pasuje zwłaszcza do tego.

Kategorie:Java Tagi: ,

Początek

12/05/2010 Dodaj komentarz

Witam to mój pierwszy post, pierwszego blogu. W zamierzeniu ma być to miejsce w którym mógłbym gromadzić jakieś większe przemyślenia, czy też małe spostrzeżenia na temat swojej zawodowej pracy. A że (jeszcze) głównie zajmuje się programowaniem w języku zwanym Java, to nazwa bloga jest tak jaka jest. Nie znaczy to, że planuje skupić się na tym i płodzić same posty dotyczące tego języka. Raczej jest to punkt wyjścia i czas pokaże czy treść będzie zgodna z nazwą, czy może nazwa będzie tylko przypominać o tym co autor chciał pisać;).

W głównym zamierzeniu prowadzenie tego bloga ma być pomocą w nauce jasnego formułowania myśli na piśmie. Także próbą refleksji, spojrzeniem z oddali na to co robię zawodowo, może z czasem to co tu będę umieszczał okaże się interesujące dla kogoś innego.

Kategorie:Inne Tagi: