Archiwum

Posts Tagged ‘jpa’

Pisanie procedur składowanych w hibernate

20/01/2012 Dodaj komentarz

Taka koncepcja urodziła mi się dziś: pisanie procedur składowanych w JPA – tutaj zawężam do implementacji Hibernate, a z baza można wskazać Oracle, ale to bez znaczenia.
Nie chodzi o wywoływanie procedur składowanych poprzez JPA czy zwykłe JDBC. Nie chodzi tu także o pisanie w javie ciała procedury składowanej (co na Oraclu da się).
To o co mi chodzi to wprowadzenia pewnej magicznej transformacji, która z kodu javy operującym na encjach JPA wygeneruje odpowiedni kod procedury (w tym wypadku PL/SQL) i stworzy taki kod na bazie. Na javie mogłoby wyglądać to tak:

	@AsStoredProcedure
	public void addCats(Long ownerId, Date date){
		List<Cat> cats = em.createQuery(
		    "select cat from Cat as cat where cat.birthdate < ?1")
		    .setParameter(1, date, TemporalType.DATE)
		    .getResultList();
		Owner owner em.find(Owner.class, ownerId);
		owner.getCats().addAll(cats);
	}

Stojąca za tym idea mogłaby być taka: jeśli wskazana metoda (tutaj oznaczoną odpowiednią adnotacją) może być prze-konwertować poprawnie na odpowiedni ciąg instrukcji PL/SQL, to zebrać to w jedną procedurę składowaną i stworzyć takie coś na bazie. Następnie w miejsce samej metody wstawić odpowiednie proxy, które to będzie wołane zamiast oryginalnej procedury.

Za i przeciw
Dlaczego tak się bawić?
Główna kwestia to wydajność. Stosowanie JPA często i tak kończyło się na wybiegach w postaci procedur składowanych dla poprawienia wydajności. To jednak wprowadzało rozbicie kodu na dwa miejsca, o które trzeba dbać i uaktualniać wraz ze zmianami.
Oczywiści można z tym żyć. Jeśli nałoży się odpowiedni reżim dbania o kod, większość błędów się nie prześliźnie. Jednak ze swojego doświadczenia widzę że jeśli jakiś kod można prosto napisać w JPA to się tak robi. Łatwiej jest to zrobić niż stworzyć analogiczną procedurę na bazie (w końcu programujemy w javie, a nie w sql :)), a to nie zawsze prowadzi do wydajnego kodu (zwłaszcza kiedy realizuję on scenariusz: wyciągnięcia wielkiej kolekcji danych, prostych zmian na jej elementach, podłączenie wszystkiego do innej encji i na koniec utrwalenie całości).
Dlatego też zamarzyło mi się właśnie coś takiego, co sprawiłoby że pisanie prostych procedur byłoby przezroczyste.

Wadą tu jest wprowadzenie dodatkowej abstrakcji (ktoś w debugu mógłby się zastanawiać dlaczego ten kod się nie wykonuję), ale to można by załatwić wprowadzając przemyślane API.

Sposoby realizacji
Realizacja tego jest możliwa, w końcu w logach widać sql który jest faktycznie wykonywany. Więc wystarczy go wziąć, trochę sparsować, opakować w kod tworzący procedurę, zarejestrować to na bazię, i wygenerować odpowiedni zamiennik w postaci proxy, który to będzie wołany zamiast metody w javie.
Najbardziej problematyczne może być pisanie w kodzie tak aby było to zamieniane na odpowiedni kod, pewnie nie każde wyrażenie z PL/SLQ może być zrealizowane.

Co do wgrywania procedur składowanych to jest taki mechanizm opis tutaj, poprzez zwykłe executeUpdate.

Kategorie:Java Tagi: , , ,

Opakowanie wynik zapytani JPA do DTO – jeszcze raz

14/09/2011 Dodaj komentarz

W ostatnim wpisie, prawiłem jak to dobrze jest w niektórych przypadkach napisać trochę więcej kod, aby nie gmatwać się później. Tą większą ilością kodu było tworzenie nowej klasy typu DTO, do użycia w zapytaniach JPA. Gmatwaniną był bałagan powstający ze zbyt uporczywej re-używalności encji jako DTO.
Niedługo po tym zdarzył mi się przypadek, który wymagał zastosowania się do tej zasady (przy okazji wyszło że łatwo jest gadać jakie to „ma być”, a jak już ma się to zastosować, to wychodzi że nie jest to takie oczywiste i bezbolesne ;)).
Przechodząc do setna: realizacja tego dla przykładowych encji:

@Entity
@Table(name = "PARENT")
public class Parent {

	@Id
	@GeneratedValue
	@Column(name = "ID")
	private Long id;
	
	@Column(name = "NAME")
	private String name;
	
    @OneToOne(fetch=FetchType.LAZY, cascade={CascadeType.ALL})
    @JoinColumn(name="CHILDREN_ID")
	private Children children;
	
	// getters, setters
}
...
@Entity
@Table(name = "CHILDREN")
public class Children {

	@Id
	@GeneratedValue
	@Column(name = "ID")
	private Long id;
	
	@Column(name = "NAME")
	private String name;

	// getters, setters
}

sprowadza się to do takiego kodu:

		List<Object[]> tempList = entityManager
		.createQuery("select d.name, d.children.name from Parent d")
		.getResultList();
		List<Parent> returnList = null;
		if (tempList == null) {
			// to jest trochę defensywne zachowanie, 
			// niby specyfikacja nie wspomina o tym że może być null
			returnList = Collections.emptyList();
		} else {
			returnList = new ArrayList<Parent>();
			for (Object[] record : tempList) {
				Parent parent = new Parent();
				parent.setName((String)record[0]);
				parent.setChildren(new Children());
				parent.getChildren().setName((String)record[1]);
				returnList.add(parent);
			}
		}

Tutaj dla uproszczenia nie wprowadzałem już osobnego DTO. Widać tu jednak jak można konstruować zapytania tak aby pobierać to co się chce i żeby było to umieszczane w odpowiednich miejscach.
Jest oczywiście prostsza metoda, z delegacją cześć zadania do konstruktora, przykład:

		List<Parent> returnList = entityManager
		.createQuery("select new Parent(d.name, d.children.name) from Parent d")
		.getResultList();

Jednak w tym przypadku rozdzielamy informacje o tym „co” „gdzie” ma być umieszczone. To też było kłopotem dla mnie, opisywałem to dawniej; przedstawiałem tam także zarys sposobu jak chciałem to usprawnić. Jednak z czasem zebrana wiedza uleżała się i dzięki sposobowi który opisywałem jeszcze w innym poście wiem jak to zrobić. Co też opisze dalej.
Czytaj dalej…

Kategorie:Java Tagi: , ,

Jak to z błędem było: prezentowanie wyników wyszukiwania, a stosowanie if

05/09/2011 Dodaj komentarz

Ostatnio natknąłem się na pewien rodzaj błędu. Związany on jest z wyszukiwaniem; gdzie sytuacja jest prosta jak budowa cepa :): podanie kryteriów, wykonanie zapytania, i zaprezentowanie wyników użytkownikowi. Błąd ten jest o tyle złośliwy, ponieważ pojawia się tylko w części przypadków, dlatego też gdy już zostanie wprowadzony trudniejsze jest jego wykrycie.

Geneza jego jest następująca:
Na początek mamy typową wyszukiwarkę, schemat całego postępowania można przedstawić na poniższym obrazku.

Tak więc użytkownik definiuje kryteria wyszukiwania; co przekłada się następnie na odpowiednie zapytanie do bazy. Wynik, z wykonania zapytania, pakowany jest do obiektów (DTO), których to zbiór służy do wygenerowania prezentacji dla użytkownika.

Za pierwszym razem wszytko działa jak należy, ale klient życzy sobie pewnych modyfikacji. W prezentowanych wynikach, w niektórych przypadkach dla zwracanych rekordów wartość X jest nieprawidłowa (jest pusta). Związane jest to z brakiem odpowiednich danych w bazie. Braki te nie są zmartwieniem klienta, chodzi o prezentacje. Dlatego też, życzy on sobie, aby prawidłową wartość X dla tego przypadku pozyskiwać na podstawie innych wartości. Co w kodzie kończy się taką zmianą:

  public String getWartoscX() {
    if (wartoscX == null) {
      // tutaj poprawka
      return wartoscA + wartoscB;
    }
    return wartoscX;
  }

Zmiana jest wprowadzana; teraz wartości w każdym przypadku prezentują się poprawnie; zgłoszenie uznaję się za rozwiązany ;). Jednak wkradł się już błąd. Tutaj łatwiej go zauważyć: kryterium X dla modyfikowanych przypadków nie jest już poprawne. Jak już wspomniałem podmiana wartości X nie występuje zawsze, powiedzmy w 10% zwracanych rekordów, dlatego też błąd ten może istnieć długo zanim zostanie odkryty.

Dlaczego taka sytuacja powstała?
Oczywiście tutaj łatwiej widać absurdalność wykonania tej poprawki. W tym przypadku odpowiednim podejściem może być chociaż wprowadzenie widoku na bazie, co mogłoby wyglądać tak:

select NVL(wartoscX, wartoscA || wartoscB) as wartoscX, wartoscY, wartoscZ from tabela;

Jednak celem moim jest zastanowienie się dlaczego taka sytuacja mogła zajść (Skłoniło mnie do tego znalezienie w ostatnim czasie kliku przypadków takiego błędu):

  • pierwszą i główną przyczyną jest specyficzność przypadku, który dotyczy niewielkiego wycinka danych. Prowadzi to do tego, że osoba wykonująca poprawkę, może się błędnie sugerować prostotą rozwiązania jakie trzeba zastosować, i nie zastanawiać się bardziej nad jego prawidłowością.
  • drugą przyczyną jest złe skupienie uwagi co do miejsca w którym należy rozwiązać błąd. Nie na prezentacji wyników (tam umieszczać logikę rozwiązania), ale na zapytaniu i zwracanych wynikach. Powodem tego może być opis samego zgłoszenia, gdzie całość jest przedstawiona w łopatologicznej formie: „weź wartość A i wartość B; i dodaj do siebie”. Prawie jak poprawka literówki :).
  • kolejna sprawa to sugerowanie się domyślnym językiem używanym w pracy. Jak ktoś programuje w javie to łatwiej jest mu rozwiązać problem w tym języku, niż silić się na prace w sql.
  • jeszcze jeden powód to złożoność kodu, skutkujący tym że poprawiający ma trudność w uchwyceniu całego kontekstu. W przypadkach, które znalazłem, było to związane z dużym zamieszaniem w przekazaniu obiektów z danymi. Nie były to proste DTO, ale obiekty encji JPA przepakowane do pośrednich obiektów przy użyciu skomplikowanych reguł.

Ogólna rada na przyszłość to: jeśli poprawka dotyczy wycinka pewnego zbioru przyjrzeć się lepiej rozwiązaniu, traktować to bardziej podejrzliwie niż w przypadku gdy zmiany obejmują cały zbiór. Potrzeba wprowadzenia if_a powinna zapalać czerwone światełko :). Skłaniać do zastanowienia się czy w dobrym miejscu ten if jest umieszczany i czy znane są wszystkie miejsca w których powinien on być.
A co do wyszukiwarek dobrze jest rzeźbić je w sql, i nawet jeśli grozi to mnożeniem bytów, stosować szyte na miarę DTO (a nie kombinować z ponownym użyciem innych klas). Także nie umieszczać logiki biznesowej w prezentacji :).

Kategorie:Java Tagi: , , , ,

Opakowanie wynik zapytani JPA do DTO

25/01/2011 Dodaj komentarz

Ten wpis jest rodzajem koncepcji (życzenia) pewnego usprawnienia jakie by mi się przydało przy pracy z JPA. Może to być także usystematyzowaniem tego co chcę i odkryciem, że problem i ewentualne rozwiązanie leży gdzie indziej.

Pisząc ostatnio zapytania w JPA (stary sposób) typu:

List persons = entityManager.createQuery("SELECT p FROM Person p").getResultList();

Miałem potrzebę, głównie ze względów wydajnościowych, ograniczenia zwracanych szczegółów w obiektach. Można to zrobić poprzez użycie DTO i wydania odpowiedniego zapytania do napełnienia pożądanych wartości w nim. Ja ze względu na zastany kod (wprowadzenie nowej klasy propagowałoby zmian wyżej) i wygody chciałem użyć wcześniejszą klas encji w tej roli. Co wygląda jak poniżej:

List persons = entityManager.createQuery("SELECT new Person(p.id, p.name) FROM Person p").getResultList();

No i jest, i dział, ale w moim (rzeczywistym) przypadku wiązało się to z przekazaniem większej liczby wartości i także stworzeniem odpowiedniego konstruktora. Czyli dużo klepania kodu w zapytaniu oraz w kodzie konstruktora. Są dwa odległe miejsca zależne od siebie, przez to można łatwo wprowadzić błąd. W przypadku gdy konstruktor przyjmuje 10 stringów, pomyłka w przypisaniu tych wartości może być łatwa do wprowadzona (choćby przestawienie dwóch wartość o tym samym typie).

Problem
Tak więc cała koncepcja ulepszenia tego sprowadza się do odpowiedzenia na pytanie: Czy nie można tego jakoś wyrazić w jednolity sposób? Tak aby mieć pewność, że konstruktor dobrze przypisuję wartości, i zapytanie jest dobrze złożone.
Dotychczas trzymałem oba kawałki kodu obok siebie, tak aby łatwiej widzieć ewentualne błędy. Później wprowadziłem automat do budowania stringa konstruktora, który eliminował pewne pomyłki (można było na przykład wprowadzić sprawdzenie czy konstruktor wyrażony w stringu da się odnaleźć). Rozwiązania te nie są idealne i są kłopotliwe w implementacji.

Uaktualnienie: poniżej są dywagacje jak to chciałbym zrobić – znalazłem rozwiązanie tego problemu w trochę inny sposób. Materiały mówiące o tym tu lub tu. A to jak później zrobiłem na podstawie tego tutaj.
Czytaj dalej…

Kategorie:Java Tagi: ,

O testowaniu

28/11/2010 Dodaj komentarz

Ostatnio doświadczyłem jak pożytecznym narzędziem potrafią być testy. Chodzi o przypadek, w którym pomagają one skrócić czas, jaki jest pomiędzy zrodzenia się pomysłu, a wdrożeniem go i sprawdzenia czy jest dobry.
Czytaj dalej…

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: ,