spring-data-commons icon indicating copy to clipboard operation
spring-data-commons copied to clipboard

Querydsl web binding support can not bind ListPath query parameters [DATACMNS-1546]

Open spring-projects-issues opened this issue 6 years ago • 5 comments

XhstormR opened DATACMNS-1546 and commented

I have a Repository class like this:

@Repository
interface ContractRepository :
        BaseRepository<Contract, QContract, Long> {

    @JvmDefault
    override fun customize(bindings: QuerydslBindings, root: QContract) {
    }
}

BaseRepository inherits QuerydslPredicateExecutor and QuerydslBinderCustomizer, by the way this is kotlin code.

And i have a Project entity that contains a one-to-many relationship like this:

@Entity
@DynamicInsert
@DynamicUpdate
class Project(
        var startTime: LocalDateTime? = null,
        var deadline: LocalDateTime? = null,
        var finishTime: LocalDateTime? = null,
        var finish: Boolean = false
) : BaseSequenceEntity() {

    @OneToMany(mappedBy = "project", cascade = [CascadeType.PERSIST])
    var subsystems = mutableListOf<Subsystem>()
}

Now I have a binding like this, but I found it doesn't work, use only the most basic equals operator:

@Repository
interface ContractRepository :
        BaseRepository<Contract, QContract, Long> {

    @JvmDefault
    override fun customize(bindings: QuerydslBindings, root: QContract) {
        bindings.bind(root.project.subsystems.any().name).first { path, value ->
            println(111)
            path.contains(value)
        }
    }
}

After I send a request to the server, request like this:

http://127.0.0.1:8080/api/search/contract?page=0&size=10&sort=createdAt,desc&project.subsystems.name=ffff

Server output is:

2019-06-19 08:39:23.142  INFO 415552 --- [nio-8080-exec-4] c.x.e.a.c.handler.RequestInterceptor     : GET [uri=/api/search/contract;client=127.0.0.1;session=039EB2C355C8CE222CC9FEC789F672DB;[email protected]]
2019-06-19 08:39:23.145 DEBUG 415552 --- [nio-8080-exec-4] org.hibernate.SQL                        : /* select contract
from Contract contract
where exists (select 1
from contract.project.subsystems as contract_project_subsystems_0
where contract_project_subsystems_0.name = ?1)
order by contract.createdAt desc */ select contract0_.id as id1_5_, contract0_.created_at as created_2_5_, contract0_.created_by_id as created_9_5_, contract0_.updated_at as updated_3_5_, contract0_.updated_by_id as updated10_5_, contract0_.optlock as optlock4_5_, contract0_.company_id as company11_5_, contract0_.file_id as file_id12_5_, contract0_.finish_date as finish_d5_5_, contract0_.name as name6_5_, contract0_.start_date as start_da7_5_, contract0_.urgency as urgency8_5_ from erp_dev.contract contract0_ where exists (select 1 from erp_dev.project project1_ cross join erp_dev.subsystem subsystems2_ where contract0_.id=project1_.contract_id and project1_.id=subsystems2_.project_id and subsystems2_.name=?) order by contract0_.created_at desc limit ?

The main SQL is:

select contract
from Contract contract
where exists (select 1
from contract.project.subsystems as contract_project_subsystems_0
where contract_project_subsystems_0.name = ?1)
order by contract.createdAt desc

But I found that if my binding is like this, the binding will take effect:

@Repository
interface ContractRepository :
        BaseRepository<Contract, QContract, Long> {

    @JvmDefault
    override fun customize(bindings: QuerydslBindings, root: QContract) {
        bindings.bind(String::class.java).all(MultiValueBinding<StringPath, String> { path, values ->
            println(222)
            if (root.project.subsystems.any().name == path) Optional.of(path.contains(values.single()))
            else Optional.empty()
        })
    }
}

Server output is:

2019-06-19 08:42:19.862  INFO 416072 --- [nio-8080-exec-1] c.x.e.a.c.handler.RequestInterceptor     : GET [uri=/api/search/contract;client=127.0.0.1;session=820AB7D7035D193D33711DEEAF9486BD;[email protected]]
222
2019-06-19 08:42:19.865 DEBUG 416072 --- [nio-8080-exec-1] org.hibernate.SQL                        : /* select contract
from Contract contract
where exists (select 1
from contract.project.subsystems as contract_project_subsystems_0
where contract_project_subsystems_0.name like ?1 escape '!')
order by contract.createdAt desc */ select contract0_.id as id1_5_, contract0_.created_at as created_2_5_, contract0_.created_by_id as created_9_5_, contract0_.updated_at as updated_3_5_, contract0_.updated_by_id as updated10_5_, contract0_.optlock as optlock4_5_, contract0_.company_id as company11_5_, contract0_.file_id as file_id12_5_, contract0_.finish_date as finish_d5_5_, contract0_.name as name6_5_, contract0_.start_date as start_da7_5_, contract0_.urgency as urgency8_5_ from erp_dev.contract contract0_ where exists (select 1 from erp_dev.project project1_ cross join erp_dev.subsystem subsystems2_ where contract0_.id=project1_.contract_id and project1_.id=subsystems2_.project_id and (subsystems2_.name like ? escape '!')) order by contract0_.created_at desc limit ?

The main SQL is:

select contract
from Contract contract
where exists (select 1
from contract.project.subsystems as contract_project_subsystems_0
where contract_project_subsystems_0.name like ?1 escape '!')
order by contract.createdAt desc

1 votes, 4 watchers

spring-projects-issues avatar Jun 19 '19 01:06 spring-projects-issues

stevenwanuk commented

experienced same issue after upgraded from spring boot 1.x (1.13.8.release of spring-data-common) to spring-boot 2 (2.1.9.release spring-data-common)

 

the following codes register a binding with path tity.childrenEntities)

 

bindings.bind(parentEntity.childrenEntities.any.childProperty).first(....)

 

 

becuase of this method from 2.1.9 release here

 

return path.toString().substring(rootPath.getMetadata().getName().length() + 1);

with following runtime values: 

path: "any(parentEntity.childrenEntities"

rootPath.getMetadata().getName(): "parentEntity"

 

here is the link of how it's handled at 1.13.8.release.

https://github.com/spring-projects/spring-data-commons/blob/1.13.8.RELEASE/src/main/java/org/springframework/data/querydsl/QueryDslUtils.java#L43

 

 

I think, at least, it should have an additional check before performs a substring. 

 

 

spring-projects-issues avatar Sep 09 '19 15:09 spring-projects-issues

stevenwanuk commented

It looks like a small fix, right?  Oliver Drotbohm

spring-projects-issues avatar Sep 09 '19 15:09 spring-projects-issues

stevenwanuk commented

this bug / issue breaks our exising bindings.  couldn't find a proper fix for it without using relfection.

however, a hacky fix is:

 

@SuppressWarnings("unchecked")
@Override
public void customize(QuerydslBindings bindings, QPrivacyNotice privacyNotice) {
 
    bindings.bind(parent.children.any().name).as("children.name").first((path, value) ->
            path.like("%" + value + "%"));

//adding correct path to pathSpecs by using reflection.
    Field field = ReflectionUtils.findField(QuerydslBindings.class, "pathSpecs");
    field.setAccessible(true);
    Map<String, Object> map = (Map<String, Object>)ReflectionUtils.getField(ReflectionUtils.findField(QuerydslBindings.class, "pathSpecs"), bindings);

    Path<?> toBeFixedPath = parent.children.any().name;
    map.put(QuerydslUtils.toDotPath(toBeFixedPath), map.get(fromRootPath(toBeFixedPath)));

}

// this is current path (which is wrong)
private static String fromRootPath(Path<?> path) {

    Path<?> rootPath = path.getMetadata().getRootPath();

    if (rootPath == null) {
        throw new IllegalStateException(String.format("Couldn't find root path on path %s!", path));
    }

    return path.toString().substring(rootPath.getMetadata().getName().length() + 1);
}

spring-projects-issues avatar Sep 19 '19 13:09 spring-projects-issues

slangeberg commented

Seems that adding alias to match the web request params: 

bindings.bind(parent.children.any().name).as("children.name")

gets the bindings picked up again in our project

spring-projects-issues avatar Dec 01 '20 20:12 spring-projects-issues

This bug affects me as well, maybe to fix we can remove the "any()" from the string, and apply the current substring removing the "root.", to match the String that is searched in getBindingForPath; Basic Example:

return path.toString()
    .replace("any(", "").replace(")", "")
    .substring(rootPath.getMetadata().getName().length() + 1);

rafaelndev avatar May 04 '21 15:05 rafaelndev