author-avatar
Dominik Martyniak
JPQL 6 minut

Efektywne Tworzenie Zapytań SQL z Wykorzystaniem Querydsl

 

Często, pracując z JPA (Java Persistence API), pojawia się potrzeba wykonywania standardowych operacji CRUD (Create, Read, Update, Delete) na naszych encjach. Jednak problem pojawia się, gdy potrzebujemy dodać bardziej zaawansowane warunki do naszych zapytań. Rozważmy to na przykładzie.


Czemu warto rozważyć Querydsl.

 

Wyobraźmy sobie, że mamy encję reprezentującą stan transakcji na naszym koncie. W tej encji znajduje się wiele pól, a my chcielibyśmy, aby nasza kolekcja transakcji miała możliwość pobrania wszystkich transakcji z określonego zakresu dat XYZ, które jednocześnie dotyczą użytkownika Y i należą do kategorii ZZ.

Oczywiście, możemy próbować osiągnąć to za pomocą standardowych narzędzi dostępnych w JPA, takich jak adnotacje @Query, i tworzyć złożone warunki w zapytaniach. Jednak w tym momencie warto zastanowić się nad narzędziem, które może znacznie ułatwić to zadanie - Querydsl.

Dlaczego Querydsl jest tak cennym narzędziem? Otóż, Querydsl pozwala na tworzenie zapytań w sposób "programistyczny", wykorzystując język Java. Oznacza to, że możemy pisać zapytania, korzystając z gotowych klas i metod, zamiast tworzyć skomplikowane ciągi znaków SQL. Daje nam to dużą kontrolę nad naszymi zapytaniami i znacznie zwiększa czytelność kodu.

Teraz, zamiast pokazywać, jak trudno jest to zrobić w JPA, zachęcamy do rozważenia użycia Querydsl, które znacząco ułatwi i przyspieszy proces tworzenia zaawansowanych zapytań w naszym projekcie. Nie trzeba się obawiać, że ktoś będzie musiał walczyć z trudnościami implementacyjnymi - Querydsl jest tutaj, aby ułatwić nam życie i uczynić nasz kod bardziej efektywnym oraz czytelnym.

 

Przykład implemetacji Querydsl.

 

Aby nie być tylko teoretycznym, poniżej znajduje się praktyczny przykład:

 

public List<Transaction> find(TransactionSearchCriteria criteria) {
    var transaction = QSQLTransaction.sQLTransaction;
    var qTransactionCategory = QSQLTransactionCategory.sQLTransactionCategory;
    var query = queryFactory.query()
            .select(Projections.constructor(TransactionProjection.class,
                    transaction.date,
                    transaction.description,
                    qTransactionCategory.displayName,
                    transaction.value,
                    transaction.checkSum
            ))
            .from(transaction)
            .leftJoin(qTransactionCategory).on(transaction.categoryId.eq(qTransactionCategory.id));

    addWhereClauses(criteria, transaction, query);

    if (criteria.getPageable() != null) {
        query.limit(criteria.getPageable().getPageSize())
                .offset(criteria.getPageable().getOffset());
        query.orderBy(DEFAULT_SORT);
    }

    return query.fetch().stream().map(mapper::toDomain).toList();
}


Rozważmy teraz, co się dzieje: inicjalizujemy dwie zmienne, transaction oraz qTransactionCategory. Warto zaznaczyć, że QSQLTransaction oraz QSQLTransactionCategory to klasy wygenerowane automatycznie przez bibliotekę Querydsl, opierając się na strukturze naszych encji. Ich implementacja przedstawia się następująco:

/**
 * QSQLTransactionCategory is a Querydsl query type for SQLTransactionCategory
 */
@Generated("com.querydsl.codegen.DefaultEntitySerializer")
public class QSQLTransactionCategory extends EntityPathBase<SQLTransactionCategory> {

    private static final long serialVersionUID = -1905666693L;

    public static final QSQLTransactionCategory sQLTransactionCategory = new QSQLTransactionCategory("sQLTransactionCategory");

    public final StringPath displayName = createString("displayName");

    public final StringPath groupId = createString("groupId");

    public final StringPath icon = createString("icon");

    public final StringPath id = createString("id");

    public final StringPath name = createString("name");

    public final StringPath userId = createString("userId");

    public QSQLTransactionCategory(String variable) {
        super(SQLTransactionCategory.class, forVariable(variable));
    }

    public QSQLTransactionCategory(Path<? extends SQLTransactionCategory> path) {
        super(path.getType(), path.getMetadata());
    }

    public QSQLTransactionCategory(PathMetadata metadata) {
        super(SQLTransactionCategory.class, metadata);
    }

}

Można powiedzieć , że ta implementacja jest odwzorowaniem naszych encji. 

Następnie, korzystając z obiektu JPAQueryFactory queryFactory, tworzymy nasze zapytanie. Jest to zapytanie typu select, w którym mapujemy odpowiednią projekcję. W tym przypadku, korzystamy z projekcji TransactionProjection i wskazujemy, które pola z encji SQLTransaction nas interesują, odnosząć sie do pól obiektu QUERYDSQL.

Kolejnym krokiem jest wskazanie źródła danych w metodzie from. Dodajemy lewy join na tabeli category i łączymy je za pomocą pola category_id, które znajduje się w encji Transaction. Teraz mamy gotowy select. Jednak właśnie w tym momencie pojawia się największa zaleta użycia Querydsl.

Jeśli chcemy dynamicznie dodawać warunki WHERE do naszego zapytania SQL, to możemy to zrobić używając instrukcji warunkowej if w języku Java. Oto przykład, jak można to zrobić:

private void addWhereClauses(TransactionSearchCriteria criteria, QSQLTransaction transaction, JPAQuery<TransactionProjection> query) {
    if (criteria.getFromDate() != null && criteria.getToDate() != null) {
        query.where(transaction.date.between(criteria.getFromDate(), criteria.getToDate()));
    }
}

Na końcu dodajemy obsługę paginacji, jeśli jest to potrzebne, i mamy gotowy kod.

 

Podsumowanie.



Podsumowując, powyższy przykład ukazuje, jak łatwo i czytelnie możemy tworzyć bardziej zaawansowane zapytania, korzystając z biblioteki Querydsl. Dzięki temu narzędziu możemy dynamicznie budować zapytania SQL, dodając warunki WHERE w zależności od naszych potrzeb. Jest to niezwykle wygodny i elastyczny sposób tworzenia zapytań, który znacząco zwiększa czytelność naszego kodu.

2
[java, sql]

Więcej od Dominik Martyniak

Więcej artykułów