Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[BUG] Collection .contains() not working anymore #3618

Open
H-Lo opened this issue Nov 4, 2023 · 30 comments
Open

[BUG] Collection .contains() not working anymore #3618

H-Lo opened this issue Nov 4, 2023 · 30 comments
Labels

Comments

@H-Lo
Copy link

H-Lo commented Nov 4, 2023

Observed vs. expected behavior

The query that I wrote in the past previously was working with older Hibernate, now is not working.

Steps to reproduce

In the repository classes there are 3 methods and none of those is working:

[UserRepositoryImpl] findByUserRole_1()

org.hibernate.query.SemanticException: Operand of 'member of' operator must be a plural path

[UserRepositoryImpl] findByUserRole_2() Alternate where clause also throws exception:

org.hibernate.query.QueryArgumentException:
Argument [ROLE_ADMIN] of type [com.thevegcat.app.config.UserRole]
did not match parameter type [java.util.Set (n/a)]

[UserRepository] findByUserRole_3() This one also used to work and now throws exception:

java.lang.ClassCastException:
class org.hibernate.metamodel.mapping.internal.BasicAttributeMapping cannot be cast to class
org.hibernate.sql.ast.tree.from.TableGroupJoinProducer
(org.hibernate.metamodel.mapping.internal.BasicAttributeMapping and
org.hibernate.sql.ast.tree.from.TableGroupJoinProducer are in unnamed module of loader 'app')

UserRole.java

public enum UserRole {
    ROLE_ADMIN,
    ROLE_ADVANCED,
    ROLE_EDITOR,
    ROLE_USER,
    ROLE_ANONYMOUS
}

User.java

@Entity 
public class User {
    // ...

	@Convert( converter = UserRolesConverter.class )
	@Column( columnDefinition = "TEXT" )
	private Set<UserRole> roles;

    // ...
}


UserRepositoryImpl.java

public class UserRepositoryImpl 
    // ...

    // case #1 - using user.roles.contains( userRole )
    public List<User> findByUserRole_1( final UserRole userRole ) {
        final QUser user = QUser.user;
        return 
            new JPAQueryFactory( this.entityManager )
                .selectFrom( user )
                .where(
                    new BooleanBuilder().and( user.roles.contains( userRole ) ).getValue()
                )
                .fetch();
    }

    // case #2 - using user.roles.any().eq( userRole )
    public List<User> findByUserRole_2( final UserRole userRole ) {
        final QUser user = QUser.user;
        return 
            new JPAQueryFactory( this.entityManager )
                .selectFrom( user )
                .where(
                    new BooleanBuilder().and( user.roles.any().eq( userRole ) ).getValue()
                )
                .fetch();
    }

    // ...
}

UserRepository.java

public interface UserRepository extends
    PagingAndSortingRepository<User, Integer>,
    CrudRepository<User, Integer>
{
    // ...

    @Query( "SELECT u FROM User u LEFT JOIN FETCH u.roles r WHERE :role IN ( r )" )
    List<User> findByUserRole_3( UserRole role );

    // ...
}

UserRolesConverter.java

public class UserRolesConverter implements AttributeConverter<Set<UserRole>, String>, Serializable {

	@Serial private static final long serialVersionUID = -6105356555706277280L;

	public static final TypeReference<Set<UserRole>> TYPE_REFERENCE = new TypeReference<>() { /**/ };

	@Override
	public String convertToDatabaseColumn( final Set<UserRole> attribute ) {
		if( attribute == null || attribute.isEmpty() ) {
			return null;
		}
		try {
			return new ObjectMapper().writeValueAsString( attribute );
		}
		catch( final JsonProcessingException e ) {
			return null;
		}
	}

	@Override
	public Set<UserRole> convertToEntityAttribute( final String dbData ) {
		if( dbData == null ) {
			return Collections.emptySet();
		}
		try {
			final Set<UserRole> result = new ObjectMapper().readValue( dbData, TYPE_REFERENCE );
			if( result == null ) {
				throw new IllegalStateException( "This should never happen" );
			}
			return result;
		}
		catch( final JsonProcessingException e ) {
			throw new IllegalStateException( "JsonProcessingException: " + e.getMessage() );
		}
	}

}

Environment

OS: Windows 11 Pro 23H2 22631.2506 Windows Feature Experience Pack 1000.22677.1000.0
IDE: Eclipse STS Version: 4.20.1.RELEASE Build Id: 202310260003 Revision: b4f357cb0399519f520df4fc968315d5735367da
Spring: Spring Boot 3.1.5
Hibernate: Hibernate ORM 6.3.1.Final
Querydsl version: 5.0.0
Querydsl module: querydsl-apt, querydsl-jpa, com.querydsl.apt.jpa.JPAAnnotationProcessor
Database: mysql Ver 8.0.16 for Win64 on x86_64 (MySQL Community Server - GPL)
JDK: openjdk version "17.0.7" 2023-04-18 OpenJDK Runtime Environment Temurin-17.0.7+7 (build 17.0.7+7) OpenJDK 64-Bit Server VM Temurin-17.0.7+7 (build 17.0.7+7, mixed mode, sharing)

See also

https://stackoverflow.com/questions/77409442/querydsl-collection-contains

@H-Lo H-Lo added the bug label Nov 4, 2023
@H-Lo H-Lo changed the title Collections .contains() not working anymore Collection .contains() not working anymore Nov 4, 2023
@Koocka44
Copy link

migrate to jpa specifications. This project is abandonware.

@velo
Copy link
Contributor

velo commented Dec 3, 2023

migrate to jpa specifications. This project is abandonware.

@H-Lo doubt you will get any help here.

I tried to volunteer... but what the present team is looking for is NOT someone to keep the project active
https://querydsl.slack.com/archives/C06KLG1BP/p1698220668765159

Since there is a conscientious choice to get this project on a death spiral, I forked it and start my own release cycles

https://github.com/OpenFeign/querydsl

There are 3 releases out already, latest one with first contributions from opensource.

5.1.1 at the very least contain fair less extremely old dependencies and was tested with java 21.

@danielvion and myself are preparing for a major push towards querydsl 6.0 with hibernate 6 support.

At this stage, we are looking for volunteer to try it out.

@velo
Copy link
Contributor

velo commented Dec 3, 2023

migrate to jpa specifications. This project is abandonware.

agreed, that is why I forked it.

@danielvion
Copy link

There are some major incompatibilities when it comes to the JPA module and hibernate 6. I am currently working on migrating to hibernate 6 in the fork mentioned above.

https://github.com/OpenFeign/querydsl

At the moment everything compiles and the tests are running. I am testing the changes in my project (big monolith with 400k lines of code) and I have found some further compatibility problems. I am working of fixing that, but afterwards some more testing will be appreciated, especially in different contexts.

Hibernate changed the way their type mappings work and also a lot of other implementation details have changed when it comes to ResultItterators for example.

Maybe you can clone the fork and test with the updated version?

@H-Lo
Copy link
Author

H-Lo commented Dec 4, 2023

Oh my.... This is pretty unclear situation for me as a consumer of dependecies.
This is so sad that such a useful component gets abandoned.

@danielvion @velo I will give it a try and hope it will be appropriate to continue with. I don't like queries in String because there is more chance to have an error not detected at design time.

@H-Lo
Copy link
Author

H-Lo commented Dec 4, 2023

@H-Lo
Copy link
Author

H-Lo commented Dec 4, 2023

@danielvion @velo Ok, I managed to replace original QueryDSL dependency with 5.1.1 but I get the same error as I did before (which I know is expected because you're not ready for Hibernate 6).
So you can now assign this bug report to your project :)
In the meantime I'll switch to stored procedure and if one day 6.0.0 comes out, I'll be the first in line for it.
Thanks!

BR,
Hrvoje

@velo
Copy link
Contributor

velo commented Dec 4, 2023

I'm gonna try to get some sort of 6.0 release candidate out today. Let's see how that goes

@velo
Copy link
Contributor

velo commented Dec 4, 2023

I released a milestone to maven central
https://repo1.maven.org/maven2/io/github/openfeign/querydsl/querydsl-core/6.0.0.M1/

Give 6.0.0.M1 a shot, please let me know how it goes.

@H-Lo
Copy link
Author

H-Lo commented Dec 5, 2023

Tried version 6.0.0.M1 but I'm getting the same errors as with abandoned 5.0.0 or OpenFeign 5.1.1 for both contains() and any().
Yesterday I created MySQL stored procedure for this purpose and now I can go on with my project.
It's not the way I wanted to go, but still it's better to have it working than to stop everything and wait.

@H-Lo
Copy link
Author

H-Lo commented Dec 6, 2023

For those who came here and want to get rid of abandoned QueryDSL, here is a simple example how to switch to Criteria API. Criteria API is extremely ugly by syntax but it still works comparing to Query DSL.

Query DSL

import com.querydsl.jpa.impl.JPAQueryFactory;

public Stream<CustomCurrency> findAllAsStream() {
	final QCustomCurrency customCurrency = QCustomCurrency.customCurrency;
	return
		new JPAQueryFactory( this.entityManager )
			.select( customCurrency )
			.from( customCurrency )
			.orderBy( customCurrency.id.asc() )
			.stream();
}

Criteria API

import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import jakarta.persistence.TypedQuery;
import jakarta.persistence.criteria.CriteriaBuilder;
import jakarta.persistence.criteria.CriteriaQuery;
import jakarta.persistence.criteria.Path;
import jakarta.persistence.criteria.Predicate;
import jakarta.persistence.criteria.Root;

public Stream<CustomCurrency> findAllAsStream() {
	final CriteriaBuilder               criteriaBuilder = this.entityManager.getCriteriaBuilder();
	final CriteriaQuery<CustomCurrency> criteriaQuery   = criteriaBuilder.createQuery( CustomCurrency.class );
	final Root<CustomCurrency>          root            = criteriaQuery.from( CustomCurrency.class );
	criteriaQuery.orderBy( criteriaBuilder.asc( root.get( "id" ) ) );
	final TypedQuery<CustomCurrency>    typedQuery      = this.entityManager.createQuery( criteriaQuery );
	return typedQuery.getResultStream();
}

@H-Lo
Copy link
Author

H-Lo commented Dec 6, 2023

Another example

Query DSL

import com.querydsl.jpa.impl.JPAQueryFactory;

public CustomCurrency findByIso4217( final String iso4217code ) {
	final java.util.Currency javaCurrency = java.util.Currency.getInstance( iso4217code );
	final QCustomCurrency customCurrency = QCustomCurrency.customCurrency;
	return
		new JPAQueryFactory(this.entityManager)
			.select( customCurrency )
			.from( customCurrency )
			.where( customCurrency.javaCurrency.eq( javaCurrency ) )
			.fetchFirst();
}

Criteria API

import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import jakarta.persistence.TypedQuery;
import jakarta.persistence.criteria.CriteriaBuilder;
import jakarta.persistence.criteria.CriteriaQuery;
import jakarta.persistence.criteria.Path;
import jakarta.persistence.criteria.Predicate;
import jakarta.persistence.criteria.Root;

public CustomCurrency findByIso4217( final String iso4217code ) {
	final CriteriaBuilder               criteriaBuilder = this.entityManager.getCriteriaBuilder();
	final CriteriaQuery<CustomCurrency> criteriaQuery   = criteriaBuilder.createQuery( CustomCurrency.class );
	final Root<CustomCurrency>          root            = criteriaQuery.from( CustomCurrency.class );
	final Path<CustomCurrency>          path            = root.get( "javaCurrency" );
	final java.util.Currency            javaCurrency    = java.util.Currency.getInstance( iso4217code );
	final Predicate                     predicate       = criteriaBuilder.equal( path, javaCurrency );
	criteriaQuery.where(predicate);
	final TypedQuery<CustomCurrency>    typedQuery      = this.entityManager.createQuery( criteriaQuery );
	final List<CustomCurrency>          results         = typedQuery.getResultList();
	if( results.isEmpty() ) {
		return null;
	}
	if( results.size() > 1 ) {
		Log.error( "Too many results ({})", Integer.valueOf( results.size() ) );
		return null;
	}
	return results.get( 0 );
}

@velo
Copy link
Contributor

velo commented Dec 13, 2023

@H-Lo
New release out with plenty of fixes on java.time territory, give it a shot

https://github.com/OpenFeign/querydsl/releases/tag/6.0.0.M2

@H-Lo
Copy link
Author

H-Lo commented Dec 19, 2023

@velo Thanks! I'll give it a shot for sure because Criteria API is created for masochists 😢

@H-Lo
Copy link
Author

H-Lo commented Dec 19, 2023

@velo Bad news for me. Upgraded to 6.0.0.M2 but exceptions from my original question are still thrown - for both cases: findByUserRole_1() and findByUserRole_2().

Is there a way to write those queries in different way to comply with syntax?

@H-Lo
Copy link
Author

H-Lo commented Dec 19, 2023

@velo I am aware it's Hibernate thing because Hibernate obviously did something to break QueryDSL, but I guess I cannot ask Hibernate team to make modifications to comply with old abandoned project.

@danielvion
Copy link

@H-Lo I will try to do some more debugging into the issue. I will try to reproduce it in the querydsl tests. At the moment all the tests are running using hibernate 6, but I am not sure if there are any for the contains() or any() on the collections.

@H-Lo
Copy link
Author

H-Lo commented Dec 19, 2023

@danielvion Thanks a million! I can help if you want. I can try to put some breakpoints, inspect and send you results or line numbers where this issue happens or even try to create runnable minimalistic test case if nothing else helps.

@danielvion
Copy link

I will be working on the fork https://github.com/OpenFeign/querydsl to reproduce the issue. A minimalistic failing test case would be great for me to use as a starting point. I am not sure if we should leave out spring since I believe this is more of a hibernate related issue.

@H-Lo
Copy link
Author

H-Lo commented Dec 19, 2023

@danielvion Hi! Please try with this one that I created just now: https://github.com/H-Lo/QueryDSLContainsBug
And yes, Spring not needed, the same exceptions are thrown without Spring.

@danielvion
Copy link

I cloned the repository and had a look at the errors. It doesn't seem straightforward to fix. I will try to reproduce the errors in the querydsl repository as some unit tests and fix it there. Hibernate 6 changed the way the types of the query parameters get validated, I believe this is what is causing the issues now.

@H-Lo
Copy link
Author

H-Lo commented Dec 20, 2023

Today I did an experiment - wrote the same query in 3 ways: by using QueryDSL, HQL and stored procedure on MySQL database.
To be honest, I gave up on Criteria API because it's extremely ugly and hard to use.
QueryDSL is the absolute winner because you have code, not string or outsiide the project. And refactoring is easy and you cannot have invalid query due to usage of classes instead strings.
That's the reason I would be really happy if you fix this. And I can bet I'm not the only one.

public List<Category> findActiveSubcategoriesWithArticlesForParent( final Integer parentId ) {
    final QCategory  category = QCategory.category;
    final QPriceInfo pi       = QPriceInfo.priceInfo;
    return 
        new JPAQueryFactory( this.entityManager )
            .select( category )
            .distinct()
            .from( pi )
            .leftJoin( pi.article.categories, category )
            .where(
                category.objectStatus.eq( ObjectStatus.ENABLED )
                .and( pi.article.objectStatus.eq( ObjectStatus.ENABLED ) )
                .and( category.parent.id.eq( parentId ) )
            )
            .fetch();
}

@velo
Copy link
Contributor

velo commented Dec 20, 2023

I cloned the repository and had a look at the errors. It doesn't seem straightforward to fix. I will try to reproduce the errors in the querydsl repository as some unit tests and fix it there. Hibernate 6 changed the way the types of the query parameters get validated, I believe this is what is causing the issues now.

I managed to reproduce it here...

So, seems querydsl translates in into JPA member of.... member of is the correct approach for collections annotated with @ElementCollection but, that is not the case here.

So, there will be need to change the apt tool and the jpql query serializer.

@velo
Copy link
Contributor

velo commented Dec 21, 2023

@H-Lo can you confirm if OpenFeign/querydsl#173 adequately reproduces the issue you are communicating?

@H-Lo
Copy link
Author

H-Lo commented Dec 21, 2023

@velo Hi! Please accept my apologies because I'm not sure what should I confirm. Just to clarify - should I clone the whole QueryDSL on my local computer and try it instead the one from Maven repository? No problem to me, but please give me a bit more info on what to do. Thanks and sorry for possible misunderstanding.

@velo
Copy link
Contributor

velo commented Dec 21, 2023

@H-Lo
Copy link
Author

H-Lo commented Dec 22, 2023

@velo Hi! Thank you for explanation!

case #1 - The query generated by using user.roles.contains( userRole ):

select user
from User user
where ?1 member of user.roles

case #2 - The query generated by using user.roles.any().eq( userRole ):

select user
from User user
where exists (
    select 1
    from User user_594692666
    inner join user_594692666.roles as user_roles_0
    where user_594692666 = user and user_roles_0 = ?1
)

@H-Lo
Copy link
Author

H-Lo commented Mar 12, 2024

Just to mention that I tried with com.querydsl 5.1.0 and openfeign.querydsl 6.1 and the bug is still there. Switched to my own stored procedure to do the job properly but still hoping this will be fixed one day in the future.

@H-Lo
Copy link
Author

H-Lo commented Apr 10, 2024

Just to mention that I tried with com.querydsl 5.1.0 and openfeign.querydsl 6.2 and the bug is still there. Switched to my own stored procedure to do the job properly but still hoping this will be fixed one day in the future.

@H-Lo
Copy link
Author

H-Lo commented Apr 30, 2024

Just to mention that I tried with com.querydsl 5.1.0 and openfeign.querydsl 6.2.1 published on Apr 09, 2024 and the bug is still there.
Switched to my own stored procedure to do the job properly but still hoping this will be fixed one day in the future.

@H-Lo H-Lo changed the title Collection .contains() not working anymore [BUG] Collection .contains() not working anymore Apr 30, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

4 participants