Strona główna > Java > Klejenie stringów na przykładzie JPA

Klejenie stringów na przykładzie JPA

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 Tags: ,
  1. Hej
    10/06/2010 o 23:39

    Też eksperymentowałem w ten sposób z budowaniem EJB QL. Gdy poszedłem tropem z generowaniem nazw parametrów, odkryłem, że trochę zmniejsza się ilość pisania ale:
    – ograniczają się dostępne opcje rozwoju QueryAssemblera
    – bardzo intensywnie rośnie złożoność jego implementacji.
    W końcu nie mogłem dodać kolejnego feature’a który był mi konieczny i wyrzuciłem całe rozwiązanie.

    Wróciłem do pomysłu dzięki Twojej implementacji. Jest to rozwiązanie proste, zawierające b. dużą ilość zalet i nie nakładające ograniczeń.

    Dzięki

    PS
    Zapomniałeś zareklamować fluent interace Twojego QueryAssembler’a.

    • 14/06/2010 o 20:54

      Tak, masz racje z fluent interface. Z samego kodu nawet wynikało że tworzyłem w tym kierunku – return this.
      Tak więc ostatni końcowy przykład można zapisać także tak:

      Query query = new QueryAssembelr("SELECT u FROM User u")
      .addStringCondition("u.name like :", "name", name)
      .addObjectCondition("u.type =:", "type", type)
      .addObjectCondition("u.registerDateFrom >:", "registerDateFrom", registerDateFrom)
      .addObjectCondition("u.registerDateTo <:", "registerDateTo", registerDateTo)
      .preperQuery(em);

      List results = query.getResultList();

  1. No trackbacks yet.

Skomentuj

Wprowadź swoje dane lub kliknij jedną z tych ikon, aby się zalogować:

Logo WordPress.com

Komentujesz korzystając z konta WordPress.com. Log Out / Zmień )

Zdjęcie z Twittera

Komentujesz korzystając z konta Twitter. Log Out / Zmień )

Facebook photo

Komentujesz korzystając z konta Facebook. Log Out / Zmień )

Google+ photo

Komentujesz korzystając z konta Google+. Log Out / Zmień )

Connecting to %s

%d bloggers like this: