Brak wyników z zapytania JPA. Potrzebny code review

0

Witajcie,
Napisałem takie zapytanie do bazy MySQL

SELECT users_sessions.session_id, users_sessions.series 
FROM users_sessions 
WHERE users_sessions.user_id = 8 
AND users_sessions.session_id IN (
	SELECT session_id 
	FROM sessions_history 
	GROUP BY sessions_history.session_id 
	HAVING COUNT(CASE WHEN sessions_history.action = 2 THEN 1 END) = 0
);

Staram się je napisać w JPA. Kod wygląda tak:

        CriteriaBuilder criteriaBuilder = this.em.getCriteriaBuilder();
        CriteriaQuery<Session> criteriaQuery = criteriaBuilder.createQuery(Session.class);
        Root<Session> from = criteriaQuery.from(Session.class);

        CriteriaQuery<Session> select = criteriaQuery.select(from);

        Subquery<SessionHistory> subquery = criteriaQuery.subquery(SessionHistory.class);
        Root<SessionHistory> fromSub = subquery.from(SessionHistory.class);
        subquery.select(fromSub.get("parent"));
        subquery.groupBy(fromSub.get("parent"));

        Expression<Integer> exp = criteriaBuilder.<Integer>selectCase()
                .when(criteriaBuilder.equal(fromSub.get("action"), action), 1)
                .otherwise(0);

        subquery.having(criteriaBuilder.lessThanOrEqualTo(criteriaBuilder.count(exp), 0l));

        select.where(criteriaBuilder.equal(from.get("owner"), user));
        select.where(criteriaBuilder.in(from.get("id")).value(subquery));

        TypedQuery<Session> typedQuery = this.em.createQuery(select);
        List<Session> result = typedQuery.getResultList();

Niestety w wyniku wykonania tego kodu otrzymuję pustą listę wyników. Dodam że po wykonaniu zapytania SQL na tej samej bazie otrzymuję poprawny wynik.

Encje:
Session:

@Entity
@Table(name = "users_sessions")
public class Session {

    @Id
    @GeneratedValue
    @Column(name = "session_id")
    private Integer id;

    @ManyToOne(fetch = FetchType.EAGER)
    @JoinColumn(name = "user_id")
    private User owner;

    @Column(length = 64, nullable = false)
    private String series;

    @OneToMany(fetch = FetchType.EAGER)
    @JoinColumn(name = "session_id")
    private List<SessionHistory> history;

    /* Gettery i settery */

}

SessionHistory:

@Entity
@Table(name = "sessions_history")
public class SessionHistory {

    @Id
    @GeneratedValue
    @Column(name = "history_id")
    private Integer id;

    @ManyToOne(fetch = FetchType.EAGER)
    @JoinColumn(name = "session_id")
    private Session parent;

    @Column(name = "action")
    private Integer action;

    @Column(length = 64, nullable = false)
    private String token;

    private Date date;

    /* Gettery i settery */

}
0

Wlacz logowanie sql przez JPA i wtedy zobaczysz czy wygenerowane zapytanie SQL jest identyczne

0

Sprawdź co zawiera zmienna 'subquery'.

0

Polecam debugger

0

Zrobiłem jak powiedział szczery. Podglądnąłem wygenerowane zapytanie. Wygląda ono tak:

select session0_.session_id as session_1_8_, session0_.user_id as user_id3_8_, session0_.series as series2_8_ 
from users_sessions session0_ 
where session0_.session_id in (
	select sessionhis1_.session_id 
	from sessions_history sessionhis1_, users_sessions session2_ 
	where sessionhis1_.session_id=session2_.session_id 
	group by sessionhis1_.session_id 
	having count(case when sessionhis1_.action=? then 1 else 0 end)<=0
)

Problem polega w tym, że w wygenerowanym zapytaniu do warunku w HAVING dodane jest ELSE, dlatego COUNT i tak liczy wtedy to zwrócone 0. Aby temu zapobiec, zmieniłem COUNT na SUM. Zmodyfikowana linijka wygląda tak:

subquery.having(criteriaBuilder.lessThanOrEqualTo(criteriaBuilder.sum(exp), 0));

Tutaj cały kod po modyfikacji: http://4programmers.net/Pastebin/3972

Lecz w tym miejscu pojawił się kolejny problem - NullPointerException. Wyjątek rzucany jest w zmodyfikowanej linijce.
Stacktrace:

[2015-04-04T23:05:28.940+0200] [glassfish 4.1] [WARNING] [] [javax.enterprise.web] [tid: _ThreadID=33 _ThreadName=http-listener-1(4)] [timeMillis: 1428181528940] [levelValue: 900] [[
  StandardWrapperValve[mvc-dispatcher]: Servlet.service() for servlet mvc-dispatcher threw exception
java.lang.NullPointerException
	at java.lang.Class.isAssignableFrom(Native Method)
	at org.hibernate.jpa.criteria.ValueHandlerFactory.isNumeric(ValueHandlerFactory.java:69)
	at org.hibernate.jpa.criteria.predicate.ComparisonPredicate.<init>(ComparisonPredicate.java:69)
	at org.hibernate.jpa.criteria.CriteriaBuilderImpl.lessThanOrEqualTo(CriteriaBuilderImpl.java:452)
	at pl.zaprogramowany.cms.service.SessionService.test(SessionService.java:102) // Zmodyfikowana linijka z SUM
	at pl.zaprogramowany.cms.service.SessionService$$FastClassBySpringCGLIB$$79431486.invoke(<generated>)
	at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:204)
	at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:711)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:157)
	at org.springframework.transaction.interceptor.TransactionInterceptor$1.proceedWithInvocation(TransactionInterceptor.java:98)
	at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:262)
	at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:95)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179)
	at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:644)
	at pl.zaprogramowany.cms.service.SessionService$$EnhancerBySpringCGLIB$$a1b07cb6.test(<generated>)
	at pl.zaprogramowany.cms.controller.MainController.index(MainController.java:44)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:483)
1

Może najlepiej skonstruować inaczej zapytanie. Napisz co chcesz uzyskać ?

SELECT t.*, 0 AS s
FROM  (SELECT 8 AS user_id, 2 AS action) t
LEFT   JOIN users_sessions  us USING (user_id)
LEFT   JOIN session_history sh USING (action, session_id)
WHERE  sh.session_id IS NULL;
 SELECT session_id, series 
FROM users_sessions as us
WHERE users_sessions.user_id = 8 
AND NOT EXISTS
 (  SELECT * 
    FROM sessions_history as sh
    WHERE action = 2
    AND us.session_id = sh.session_id 
 ) 
0

Mam 2 tabele. W pierwszej trzymam sesje użytkowników w formie: ID_SESJI, ID_USERA, SERIES (klucz z security). W drugiej natomiast trzymam historię tych sesji tzn. kiedy sesja została stworzona, użyta, usunięta w formie: ID_REKORDU_HISTORII, ID_SESJI, DATA, AKCJA, TOKEN (aktualnie ustawiony token w ciasteczku usera. Null jeżeli akcja to 2 - remove).

Potrzebuję w metodzie removeUserTokens z interfejsu PersistentTokenRepository zamknąć wszystkie otwarte sesje użytkownika. Z tego też powodu chcę napisać metodę, która zwróci mi wszystkie otwarte sesje, lub żeby było bardziej elastycznie wszystkie sesje, które nie zawierają akcji o konkretnym id dla danego usera, tak żebym mógł je potem zamknąć.

Wydaje mi się że drugi zaproponowany przez kadoela sposób rozwiązania tego problemu jest dobry.

Dam wam znać potem czy się udało :)

0

Dokładnie tak jak myślałem. Sposób rozwiązania problemu zaproponowany przez @kadoel jest poprawny. Napisałem taki kod:

        CriteriaBuilder cb = this.em.getCriteriaBuilder();

        CriteriaQuery<Session> query = cb.createQuery(Session.class);
        Root<Session> root = query.from(Session.class);
        query.select(root);

        Subquery<SessionHistory> subquery = query.subquery(SessionHistory.class);
        Root<SessionHistory> subRoot = subquery.from(SessionHistory.class);
        subquery.select(subRoot);
        subquery.where(cb.and(cb.equal(root, subRoot.get("parent")), cb.equal(subRoot.get("action"), action)));

        query.where(cb.equal(root.get("owner"), user));
        query.where(cb.not(cb.exists(subquery)));


        TypedQuery<Session> typedQuery = em.createQuery(query);
        List<Session> result = typedQuery.getResultList();

Pisząc kod, według zapytania @kadoel zobaczyłem też jeden błąd w starym. Jest tam dwa razy wywoływana metoda where. Podejrzewam że to przez to miałem problemy z NullPointerException. W tym kodzie użyłem metody "and" z obiektu "CriteriaBuilder" by połączyć dwa warunki.

Dzięki za pomoc :)

0

Jak na to patrze to CriteriaAPI w JPA 2 to jakiś shit jeśli chodzi o czytelność, Criteria hibernate o wiele lepsze, a najlepiej to JPQL/HQL

0

Z tego co wyczytałem w JPQL i HQL nie ma możliwości zrobienia subquery. Sam się zgodzę z tym że wygląda to fatalnie

1 użytkowników online, w tym zalogowanych: 0, gości: 1