9 czerwca 2007

Zbędne złączenia w zapytaniach SQL generowanych przez TopLink JPA

To, co dzisiaj opiszę to efekt moich doświadczeń z pracy z TopLink Essentials 2.0-b47-beta3 jako dostawcą Java Persistence API 1.0. Nie weryfikowałem, czy inne implementacje zachowują się analogicznie ani czy na ten temat mówi cokolwiek specyfikacja, ale zważywszy, że jest to szczegół implementacyjny wydaje mi się to wysoce nieprawdopodobne. No więc do rzeczy. Zerknijmy na poniższe klasy, encje JPA:

@Entity
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long productId;

private String productDescr;

private ProductCategory productCategory;
}

@Entity
public class ProductCategory {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long categoryId;

private String categoryName;

@OneToMany(mappedBy = "productCategory", fetch = FetchType.LAZY)
private List products;
}

Nie istotne dla przykładu właściwości i metody pominąłem dla przejrzystości. Dla encji tych TopLink wygenerował następujące tabele i więzy w bazie danych:

CREATE TABLE PRODUCT (
PRODUCTID BIGINT NOT NULL,
PRODUCTDESCR VARCHAR(255),
PRODUCTCATEGORY_CATEGORYID BIGINT,
PRIMARY KEY (PRODUCTID)
)

CREATE TABLE PRODUCTCATEGORY (
CATEGORYID BIGINT NOT NULL,
CATEGORYNAME VARCHAR(255),
PRIMARY KEY (CATEGORYID)
)

ALTER TABLE PRODUCT
ADD CONSTRAINT FK_PRODUCT_PRODUCTCATEGORY_CATEGORYID
FOREIGN KEY (PRODUCTCATEGORY_CATEGORYID)
REFERENCES PRODUCTCATEGORY (CATEGORYID)

Napiszmy teraz zapytanie JPQL pobierające wszystkie produkty należące do danej kategorii. Oto pierwsza próba:

SELECT p FROM Product p WHERE p.productCategory.categoryId = :categoryId

No i w tym miejscu nabrałem wątpliwości. Czy aby na pewno JPA zadba o optymalizację? Czy zauważy, że złączenie z tabelą PRODUCTCATEGORY nie jest potrzebne do wyliczenia wyniku tego zapytania? TopLink we wspomnianej wersji nie zauważy i wygeneruje następujące zapytanie SQL:

SELECT t0.PRODUCTID, t0.PRODUCTDESCR, t0.PRODUCTCATEGORY_CATEGORYID
FROM PRODUCT t0, PRODUCTCATEGORY t1
WHERE (t1.CATEGORYID = ?) AND (t1.CATEGORYID = t0.PRODUCTCATEGORY_CATEGORYID)

Hmmm… czyżby implementacja takiej z pozoru prostej optymalizacji była trudna? Korci mnie żeby zajrzeć jak to implementują inni, ale póki co wygrywa brak czasu, tym bardziej że wystarczy zmienić nieco nasze zapytanie i będzie jak trzeba. Poprawiona, równoznaczna semantycznie wersja zapytania JPQL poniżej:

SELECT p FROM Product p WHERE p.productCategory = :prodCat

Zmiana wydaje się oczywista, aczkolwiek do uruchomienia takiego zapytania nie wystarczy już identyfikator kategorii, będziemy potrzebowali obiektu kategorii. Jest to trochę irytujące zwłaszcza, że obiekt ten posłuży li tylko jako opakowanie do identyfikatora, ale z drugiej strony ratuje sytuację, bowiem jako parametr zapytania wystarczy podać obiekt new ProductCategory(categoryId). Upewnijmy się jeszcze, że teraz jest dobrze. Rzeczywiście, wygenerowane zapytanie SQL to:

SELECT PRODUCTID, PRODUCTDESCR, PRODUCTCATEGORY_CATEGORYID
FROM PRODUCT
WHERE PRODUCTCATEGORY_CATEGORYID = ?

Brak komentarzy: