I'm trying to have a conditional query fragment, based upon a parameter which comes from the mybatis configuration, rather than a query parameter. Something like this:
<sql id="frag">
<if test="col_name != null">
SELECT * FROM TABLE WHERE ${col.name}=#{value}
</if>
<if test="col_name == null">
SELECT * FROM TABLE WHERE SAMPLECOL=#{value}
</if>
</sql>
where the value of col_name is a global parameter, specified inside the .properties file read by the mybatis configuration.
Apparently this does not work; looking at the source code, it seems that the OGNL expression evaluator is not aware of the configuration properties (which instead are working when I have the parameter substitution, through ${...} inside the SQL). Did anybody find a way to do this?
I found out this is not currently possible; the OGNL has effectively no access to configuration properties.
As a workaround, as suggested in this post on the mybatis mailing list, I wrote a simple interceptor which reads configuration parameters and adds them to the query parameter map. Not exactly clean, but it works.
Interceptor code:
#Intercepts({
#Signature(type = Executor.class,
method = "query",
args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})})
public class ConfigPropInterceptor implements Interceptor {
private final Map<String, Object> properties = new HashMap<String, Object>();
#Override
public Object intercept(Invocation invocation) throws Throwable {
Object param = invocation.getArgs()[1];
if (param instanceof Map) {
((Map<String, Object>)param).putAll(properties);
}
return invocation.proceed();
}
#Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
#Override
public void setProperties(Properties properties) {
for (String p : properties.stringPropertyNames()) {
this.properties.put(p, properties.getProperty(p));
}
}
}
Example usage in configuration .xml:
<plugins>
<plugin interceptor="...ConfigPropInterceptor">
<property name="_issuerLocation" value="${issuer.location}"/>
</plugin>
</plugins>
With this setting, I was able to test the _issuerLocation variable in OGNL expressions like everything else.
Related
i am using java, spring mvc and mybatis.
for http patch, it is used to update partial resource, while put updates the entirely resource.
my code looks like
#RestController
#RequestMapping("/test")
public class Test {
#PutMapping
public void update(MyBean myBean) {
//update MyBean
}
mybatis code is:
<update id="updateMyBean">
update My_Bean
<set>
<if test="filed1 != null>field1 = #{field1},</if>
<if test="filed2 != null>field1 = #{field2},</if>
<if test="filed3 != null>field1 = #{field3},</if>
</set>
where id = #{id}
</update>
then how to implement patch in the spring mvc? how to implement patch in mybatis?
is it add another update method like following?
#PutMapping
public void update(MyBean myBean) {
//update MyBean
}
#PatchMapping
public void updateBeanPartial(MyBean myBean) {
//update MyBean
}
//they look like the same just annotations and/or method name are different
or
#PatchMapping
public void updateBeanPartial(Map myBeanMap) {
//update MyBean
}
//use Map as parameters, but in this case, we cannot do bean validation easily and cannot show what fields need to be sent in swagger
//or use specified fields of MyBean as parameter, but it will introduce many controller methods because MyBean can have many fields
and they use same mybatis update statement?
Thus, how to implement put and patch in the code?
or their difference only in the semantic not in the code?
Lets clarify some things first:
Update: If Bean (MyBean) has multiple fields, then you create 1 SQL statement to update all fields once in your batis.
Patch: If Bean (MyBean) has multiple fields, then you create 1 SQL Statement to patch (updating only some fields).
In your Batis mapper XML, would you need to define 2 functions:
Update User ,
Patch User ,
What you define in your batis mapper is your DAO statements.
What you define your Spring MVC is your Controller/Services, and what you want to achieve.
If you want 2 endpoints (PUT, PATCH), and you want them to do different things, then you need 2 different statements in batis.
Take note of <mapper namespace="org.com.BatisUserService"> which is what you will use to later on reference the implementations from your other classes such as #Controller, or #Service in your Spring MVC app.
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="org.com.BatisUserService"> <---- Take note of this
<update id="updateUser"
parameterType="org.com.model.User">
UPDATE user SET
user_name=#{userName,jdbcType=VARCHAR},
user_password=#{userPassword,jdbcType=VARCHAR},
user_email=#{userEmail,jdbcType=VARCHAR}
WHERE
user_id=#{userId,jdbcType=INTEGER};
</update>
<update id="patchUser"
parameterType="org.com.model.User">
UPDATE user SET
user_name=#{userName,jdbcType=VARCHAR}
user_password=#{userPassword,jdbcType=VARCHAR},
WHERE
user_id=#{userId,jdbcType=INTEGER};
</update>
</mapper>
Then in your Controller, you would then call your Mapper org.com.BatisUserService which has the functions updateUser and patchUser defined:
#Controller
#RequestMapping(value = "/user")
public class UserController {
#Autowired
private BatisUserService batisUserService;
#RequestMapping(value = "/updateUser", method = RequestMethod.POST)
public User updateUser(User user) {
batisUserService.updateUser(user);
}
#RequestMapping(value = "/patchUser", method = RequestMethod.PATCH)
public User patchUser(User user) {
batisUserService.patchUser(user);
}
}
I am inserting an immutable object with Optional fields using a DAO and want to use the #BindBean annotation to bind the object to query parameters. This is not working because of the Optional fields.
My attempt to solve this problem was to make a custom Binder, but I feel there must be a more maintainable way.
This is how I'm creating my DAO statement:
#GetGeneratedKeys
#SqlUpdate("INSERT INTO TASKS (project_id, creator_id, stage, title, description, created_at, priority, est_work)" +
"VALUES (:projectId, :creatorId, :stage, :title, :description, :createdAt, :priority, :estimatedWork)")
int insertTask(#NewTaskBinder ImmutableNewTask newTask);
This is my current solution:
#BindingAnnotation(NewTaskBinder.NewTaskBinderFactory.class)
#Retention(RetentionPolicy.RUNTIME)
#Target(ElementType.PARAMETER)
public #interface NewTaskBinder {
class NewTaskBinderFactory implements BinderFactory {
#Override
public Binder build(Annotation t) {
return (Binder<NewTaskBinder, ImmutableNewTask>) (query, bind, newTask) -> {
query.bind("title", newTask.getTitle());
query.bind("stage", newTask.getStage());
query.bind("creatorId", newTask.getCreatorId());
query.bind("projectId", newTask.getProjectId());
query.bind("createdAt", new Date());
query.bind("dueDate", newTask.getDueDate().orElse(null));
query.bind("priority", newTask.getPriority().orElse(null));
query.bind("description", newTask.getDescription().orElse(null));
query.bind("estimatedWork", newTask.getEstimatedWork().orElse(null));
};
}
}
}
#BindBean works great without Optional fields present. It would be great if there's a way to adjust this to work with Optionals.
this is more than a simple question and my English is not as good as I want... I'll try my best.
I use java 8, with Mybatis 3.4.6 over Postgres 9.6 and I need to do a custom dynamic query.
In my mapper.java class I've created a method to use with myBatis SQL Builder class
#SelectProvider(type = PreIngestManager.class, method = "selectPreIngestsSQLBuilder")
#Results({ #Result(property = "id", column = "id"), #Result(property = "inputPath", column = "input_path"),
#Result(property = "idCategoriaDocumentale", column = "id_categoria_documentale"), #Result(property = "idCliente", column = "id_cliente"),
#Result(property = "outputSipPath", column = "output_sip_path"), #Result(property = "esito", column = "esito"),
#Result(property = "stato", column = "stato"), #Result(property = "pathRdp", column = "path_rdp"),
#Result(property = "dataInizio", column = "data_inizio"), #Result(property = "dataFine", column = "data_fine") })
List<PreIngest> selectPreIngestsByFilters(#Param("idCatDoc") Long idCatDoc, #Param("nomePacchetto") String nomePacchetto,
#Param("dataInizioInferiore") Date dataInizioInferiore, #Param("dataInizioSuperiore") Date dataInizioSuperiore,
#Param("statiPreIngest") String statiPreIngest);
I have specified the #SelectProvider annotation, class and method to point at, which, in the example is PreIngestManager.class and selectPreIngestsSQLBuilder method.
This is the method
public String selectPreIngestsSQLBuilder(Map<String, Object> params) {
return new SQL() {
{
SELECT("*");
FROM("pre_ingest");
WHERE("id_categoria_documentale = #{idCatDoc}");
if (params.get("nomePacchetto") != null)
WHERE("input_path like '%' || #{nomePacchetto}");
if (params.get("dataInizioInferiore") != null) {
if (params.get("dataInizioSuperiore") != null) {
WHERE("data_inizio between #{dataInizioInferiore} and #{dataInizioSuperiore}");
} else {
WHERE("data_inizio >= #{dataInizioInferiore}");
}
} else {
if (params.get("dataInizioSuperiore") != null) {
WHERE("data_inizio <= #{dataInizioSuperiore}");
}
}
if (params.get("statiPreIngest") != null)
WHERE("stato in (#{statiPreIngest})");
ORDER_BY("id ASC");
}
}.toString();
}
and these are my questions:
have I to specify #Results annotation and every #Result , or can I use a java model class ? I have tried with #ResultMap(value = { "mycompany.model.PreIngest" }) but it did not work.
Most of all, as stated on documentation, with SQL builder you can access method parameters having them as final objects
// With conditionals (note the final parameters, required for the anonymous inner class to access them)
public String selectPersonLike(final String id, final String firstName,
final String lastName) {
return new SQL() {{
SELECT("P.ID, P.USERNAME, P.PASSWORD, P.FIRST_NAME, P.LAST_NAME");
FROM("PERSON P");
if (id != null) {
WHERE("P.ID like #{id}");
}
if (firstName != null) {
WHERE("P.FIRST_NAME like #{firstName}");
}
if (lastName != null) {
WHERE("P.LAST_NAME like #{lastName}");
}
ORDER_BY("P.LAST_NAME");
}}.toString();
}
But if I put those final in my method I can't access them. Do I need to delete the #Param from the method declaration? Do SQLBuilder need to be called without #SelectProvider ? Am I mixing solutions ?
As far as I have researched, for now I see 3 methods to do a dynamic query, or a custom where condition.
To use MyBatisGenerator library and combine where condition as search criteria to use with SelectByExample method. (I use this when the query is simple)
To Write an SQL query directly, modifying XML mapper files using if, choose, statements and others as descripted here
To use SQL Builder class with #SelectProvider annotation.
Do you know when prefer the 2° method over the 3° one ? Why in the 3° method documentation I can't find how to use it ? There is written how to create custom queries but not how to launch them.
Thank a lot for your time and your suggestions.
I don't know whether you already found the answer, I just want to share my experience. Btw please forgive my english if it wasn't good.
Note: I use MyBatis 3.4.6 and Spring Framework.
have I to specify #Results annotation and every #Result , or can I use a java model class ?
Actually you can do either one.
if you want to use #Results and #ResultMap, you just need to specify #Results annotation just once in one mapper file. The trick is you need to specify id for the Results to be used in other functions.
Using truncated version of your classes, eg:
#Results(id="myResult", value= {
#Result(property = "id", column = "id"),
#Result(property = "inputPath", column = "input_path"),
#Result(property = "idCategoriaDocumentale", ... })
List<PreIngest> selectPreIngestsByFilters(#Param("idCatDoc") Long idCatDoc, #Param("nomePacchetto") String nomePacchetto, ...);
Then in another function you can use #ResultMap with the value refer to id from #Results mentioned before.
#ResultMap("myResult")
List<PreIngest> selectPreIngestsBySomethingElse(....);
..., or can I use a java model class ?
You can use java model class as result without using #Results and #ResultMap, but you have to make sure your java model class has the same properties/fields as the result of your query. Database tables usually have fields with snake_case. Since java is using camelCase, you have to add settings to your mybatis-config.xml file.
This is what I usually add to the mybatis-config.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<settings>
<!-- changes from the defaults -->
<setting name="lazyLoadingEnabled" value="false" />
<setting name="mapUnderscoreToCamelCase" value="true"/>
<setting name="jdbcTypeForNull" value="NULL"/>
</settings>
</configuration>
The important one is mapUnderscoreToCamelCase, set this to true than you can use your java model class without the hassle of #Results and #ResultMap. You can find all the explanation of the settings in MyBatis 3 Configuration.
This is the example using your classes,
The class:
public class PreIngest {
private Long idCategoriaDocumentale;
private Long idCliente;
........ other fields
........ setter, getter, etc
}
The mapper file:
List<PreIngest> selectPreIngestsByFilters(#Param("idCatDoc") Long idCatDoc, #Param("nomePacchetto") String nomePacchetto, ...);
Now onwards to SqlBuilder.
But if I put those final in my method I can't access them. Do I need to delete the #Param from the method declaration? Do SQLBuilder need to be called without #SelectProvider ?
I can't answer about those final in your method since I never made SqlBuilder class with final parameters.
For SqlBuilder you must use #SelectProvider, #InsertProvider, #UpdateProvider or #DeleteProvider and it depends on the query you use.
In my experience with SQLBuilder, #Param is necessary if you need more than one parameters and use Map params to access it from the SqlBuilder class. If you don't want to use #Param in the mapper file, then you need to make sure there is only one parameter in the said mapper function. You can use java model class as the parameter though if you just specify one parameter.
If using your class for example, you can have one class
public class PersonFilter {
private Long id;
private String firstName;
private String lastName;
...... setter, getter, etc
}
the mapper function
#SelectProvider(type=PersonSqlBuilder.class, method="selectPersonLike")
List<Person> selectPersonLike(PersonFilter filter);
the SqlBuilder class
public class PersonSqlBuilder {
public String selectPersonLike(PersonFilter filter) {
return new SQL() {{
SELECT("P.ID, P.USERNAME, P.PASSWORD, P.FIRST_NAME, P.LAST_NAME");
FROM("PERSON P");
if (filter.getId() != null) {
WHERE("P.ID like #{id}");
}
if (filter.getFirstName() != null) {
WHERE("P.FIRST_NAME like #{firstName}");
}
if (filter.getLastName() != null) {
WHERE("P.LAST_NAME like #{lastName}");
}
ORDER_BY("P.LAST_NAME");
}}.toString();
}
}
That's it. Hopefully my experience can help.
I don't know how to do this with the sql builder but I do have an idea how to do this with an xml mapper file:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="path.to.class.PreIngestMapper">
<resultMap id="preIngestManager" type="path.to.class.PreIngestManager">
<id property="id" column="id" />
<result property="id" column="id" />
<result property="inputPath" column="input_path" />
<result property="idCategoriaDocumentale" column="id_categoria_documentale" />
...
</resultMap>
<select id="selectPreIngests" parameterType="Map" resultMap="preIngestManager">
SELECT *
FROM pre_ingest
WHERE id_categoria_documentale = #{idCatDoc}
<if test = "nomePacchetto != null">
and input_path like '%' || #{nomePacchetto}
</if>
...
;
</select>
</mapper>
Overview
Given
Spring Data JPA, Spring Data Rest, QueryDsl
a Meetup entity
with a Map<String,String> properties field
persisted in a MEETUP_PROPERTY table as an #ElementCollection
a MeetupRepository
that extends QueryDslPredicateExecutor<Meetup>
I'd expect
A web query of
GET /api/meetup?properties[aKey]=aValue
to return only Meetups with a property entry that has the specified key and value: aKey=aValue.
However, that's not working for me.
What am I missing?
Tried
Simple Fields
Simple fields work, like name and description:
GET /api/meetup?name=whatever
Collection fields work, like participants:
GET /api/meetup?participants.name=whatever
But not this Map field.
Customize QueryDsl bindings
I've tried customizing the binding by having the repository
extend QuerydslBinderCustomizer<QMeetup>
and overriding the
customize(QuerydslBindings bindings, QMeetup meetup)
method, but while the customize() method is being hit, the binding code inside the lambda is not.
EDIT: Learned that's because QuerydslBindings means of evaluating the query parameter do not let it match up against the pathSpecs map it's internally holding - which has your custom bindings in it.
Some Specifics
Meetup.properties field
#ElementCollection(fetch = FetchType.EAGER)
#CollectionTable(name = "MEETUP_PROPERTY", joinColumns = #JoinColumn(name = "MEETUP_ID"))
#MapKeyColumn(name = "KEY")
#Column(name = "VALUE", length = 2048)
private Map<String, String> properties = new HashMap<>();
customized querydsl binding
EDIT: See above; turns out, this was doing nothing for my code.
public interface MeetupRepository extends PagingAndSortingRepository<Meetup, Long>,
QueryDslPredicateExecutor<Meetup>,
QuerydslBinderCustomizer<QMeetup> {
#Override
default void customize(QuerydslBindings bindings, QMeetup meetup) {
bindings.bind(meetup.properties).first((path, value) -> {
BooleanBuilder builder = new BooleanBuilder();
for (String key : value.keySet()) {
builder.and(path.containsKey(key).and(path.get(key).eq(value.get(key))));
}
return builder;
});
}
Additional Findings
QuerydslPredicateBuilder.getPredicate() asks QuerydslBindings.getPropertyPath() to try 2 ways to return a path from so it can make a predicate that QuerydslAwareRootResourceInformationHandlerMethodArgumentResolver.postProcess() can use.
1 is to look in the customized bindings. I don't see any way to express a map query there
2 is to default to Spring's bean paths. Same expression problem there. How do you express a map?
So it looks impossible to get QuerydslPredicateBuilder.getPredicate() to automatically create a predicate.
Fine - I can do it manually, if I can hook into QuerydslAwareRootResourceInformationHandlerMethodArgumentResolver.postProcess()
HOW can I override that class, or replace the bean? It's instantiated and returned as a bean in the RepositoryRestMvcConfiguration.repoRequestArgumentResolver() bean declaration.
I can override that bean by declaring my own repoRequestArgumentResolver bean, but it doesn't get used.
It gets overridden by RepositoryRestMvcConfigurations. I can't force it by setting it #Primary or #Ordered(HIGHEST_PRECEDENCE).
I can force it by explicitly component-scanning RepositoryRestMvcConfiguration.class, but that also messes up Spring Boot's autoconfiguration because it causes
RepositoryRestMvcConfiguration's bean declarations to be processed
before any auto-configuration runs. Among other things, that results in responses that are serialized by Jackson in unwanted ways.
The Question
Well - looks like the support I expected just isn't there.
So the question becomes:
HOW do I correctly override the repoRequestArgumentResolver bean?
BTW - QuerydslAwareRootResourceInformationHandlerMethodArgumentResolver is awkwardly non-public. :/
Replace the Bean
Implement ApplicationContextAware
This is how I replaced the bean in the application context.
It feels a little hacky. I'd love to hear a better way to do this.
#Configuration
public class CustomQuerydslHandlerMethodArgumentResolverConfig implements ApplicationContextAware {
/**
* This class is originally the class that instantiated QuerydslAwareRootResourceInformationHandlerMethodArgumentResolver and placed it into the Spring Application Context
* as a {#link RootResourceInformationHandlerMethodArgumentResolver} by the name of 'repoRequestArgumentResolver'.<br/>
* By injecting this bean, we can let {#link #meetupApiRepoRequestArgumentResolver} delegate as much as possible to the original code in that bean.
*/
private final RepositoryRestMvcConfiguration repositoryRestMvcConfiguration;
#Autowired
public CustomQuerydslHandlerMethodArgumentResolverConfig(RepositoryRestMvcConfiguration repositoryRestMvcConfiguration) {
this.repositoryRestMvcConfiguration = repositoryRestMvcConfiguration;
}
#Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
DefaultListableBeanFactory beanFactory = (DefaultListableBeanFactory) ((GenericApplicationContext) applicationContext).getBeanFactory();
beanFactory.destroySingleton(REPO_REQUEST_ARGUMENT_RESOLVER_BEAN_NAME);
beanFactory.registerSingleton(REPO_REQUEST_ARGUMENT_RESOLVER_BEAN_NAME,
meetupApiRepoRequestArgumentResolver(applicationContext, repositoryRestMvcConfiguration));
}
/**
* This code is mostly copied from {#link RepositoryRestMvcConfiguration#repoRequestArgumentResolver()}, except the if clause checking if the QueryDsl library is
* present has been removed, since we're counting on it anyway.<br/>
* That means that if that code changes in the future, we're going to need to alter this code... :/
*/
#Bean
public RootResourceInformationHandlerMethodArgumentResolver meetupApiRepoRequestArgumentResolver(ApplicationContext applicationContext,
RepositoryRestMvcConfiguration repositoryRestMvcConfiguration) {
QuerydslBindingsFactory factory = applicationContext.getBean(QuerydslBindingsFactory.class);
QuerydslPredicateBuilder predicateBuilder = new QuerydslPredicateBuilder(repositoryRestMvcConfiguration.defaultConversionService(),
factory.getEntityPathResolver());
return new CustomQuerydslHandlerMethodArgumentResolver(repositoryRestMvcConfiguration.repositories(),
repositoryRestMvcConfiguration.repositoryInvokerFactory(repositoryRestMvcConfiguration.defaultConversionService()),
repositoryRestMvcConfiguration.resourceMetadataHandlerMethodArgumentResolver(),
predicateBuilder, factory);
}
}
Create a Map-searching predicate from http params
Extend RootResourceInformationHandlerMethodArgumentResolver
And these are the snippets of code that create my own Map-searching predicate based on the http query parameters.
Again - would love to know a better way.
The postProcess method calls:
predicate = addCustomMapPredicates(parameterMap, predicate, domainType).getValue();
just before the predicate reference is passed into the QuerydslRepositoryInvokerAdapter constructor and returned.
Here is that addCustomMapPredicates method:
private BooleanBuilder addCustomMapPredicates(MultiValueMap<String, String> parameters, Predicate predicate, Class<?> domainType) {
BooleanBuilder booleanBuilder = new BooleanBuilder();
parameters.keySet()
.stream()
.filter(s -> s.contains("[") && matches(s) && s.endsWith("]"))
.collect(Collectors.toList())
.forEach(paramKey -> {
String property = paramKey.substring(0, paramKey.indexOf("["));
if (ReflectionUtils.findField(domainType, property) == null) {
LOGGER.warn("Skipping predicate matching on [%s]. It is not a known field on domainType %s", property, domainType.getName());
return;
}
String key = paramKey.substring(paramKey.indexOf("[") + 1, paramKey.indexOf("]"));
parameters.get(paramKey).forEach(value -> {
if (!StringUtils.hasLength(value)) {
booleanBuilder.or(matchesProperty(key, null));
} else {
booleanBuilder.or(matchesProperty(key, value));
}
});
});
return booleanBuilder.and(predicate);
}
static boolean matches(String key) {
return PATTERN.matcher(key).matches();
}
And the pattern:
/**
* disallow a . or ] from preceding a [
*/
private static final Pattern PATTERN = Pattern.compile(".*[^.]\\[.*[^\\[]");
I spent a few days looking into how to do this. In the end I just went with manually adding to the predicate. This solution feels simple and elegant.
So you access the map via
GET /api/meetup?properties.aKey=aValue
On the controller I injected the request parameters and the predicate.
public List<Meetup> getMeetupList(#QuerydslPredicate(root = Meetup.class) Predicate predicate,
#RequestParam Map<String, String> allRequestParams,
Pageable page) {
Predicate builder = createPredicateQuery(predicate, allRequestParams);
return meetupRepo.findAll(builder, page);
}
I then just simply parsed the query parameters and added contains
private static final String PREFIX = "properties.";
private BooleanBuilder createPredicateQuery(Predicate predicate, Map<String, String> allRequestParams) {
BooleanBuilder builder = new BooleanBuilder();
builder.and(predicate);
allRequestParams.entrySet().stream()
.filter(e -> e.getKey().startsWith(PREFIX))
.forEach(e -> {
var key = e.getKey().substring(PREFIX.length());
builder.and(QMeetup.meetup.properties.contains(key, e.getValue()));
});
return builder;
}
I'm using this awesome library, but I have a problem.
I'm implementing a DTO pattern, so I use another project to convert automaticaly an EJB to a DTO using naming conventions.
Then, I want to query the DTO and getting the real result (EJB query).
I implemented QueryDSL with JPAAnnotationProcessor on my ENTITIES, and the QuerydslAnnotationProcessor on my DTOs.
For example :
An entity User(Long Id, String username, Site site)
A DTO UserDto(Long id, String username, String siteName)
Converting objects is good, "siteName" automatically match "site.name".
And so, I put a QueryDSL Query like: userDto.id.gt(20).and(userDto.username.like("a%")).and(userDto.siteName.like("%b"));
I'm looking for a way to build the corresponding entity query
The only idea I got is to :
Clone the Query
Change the path "userDto" to "user"
Verify each predicate to know if the property exists and if the type is matching
Any way to do that or to reach my goal?
Thanks
Since this is still relevant and undocumented functionality, and since Timo's answer, while helpful, is very cryptic, here's how to do it:
First, extend ReplaceVisitor:
private class CustomReplaceVisior extends ReplaceVisitor<Void> {
#Override
public Expression<?> visit(Path<?> path, #Nullable Void context) {
// The map Timo mentioned to transform paths:
Map<Path<?>, Path<?>> map = Map.of(
QUser.user.id, QUserDto.userDto.id,
QUser.user.name, QUserDto.userDto.name
);
if (map.contains(path)) {
return map.get(path);
} else {
return super.visit(path, context);
}
}
}
Then use it like this:
CustomReplaceVisior replaceVisitor = new CustomReplaceVisior();
Predicate userPredicate = QUser.user.id.eq(2).and(QUser.user.name.eq("Somename"));
Predicate userDtoPredicate = (Predicate) userPredicate.accept(replaceVisitor, null);
You will need to convert expressions in general. With a custom ReplaceVisitor you can for example override visit(Path expr, #Nullable Void context)
A generic way to do the path replacements would be to use a Map map to define the replacements:
if (map.contains(path)) {
return map.get(path);
} else {
return super.visit(path, context);
}
You can use your visitor like this:
Expression transformedExpression = expr.accept(visitor, null);