Archiwum

Archive for Wrzesień 2011

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