I have an application running camel on spring-boot.
I want to pass a parameter retriesAllowed to a namedQuery from a camel route.
namedQuery:
#NamedQuery(name = "findFailedMessages", query = RawMessageTx.HQL_FIND_FAILED_MESSAGES)
public class RawMessageTx extends BaseEntityWithTransmitStatus implements Serializable {
public static final String HQL_FIND_FAILED_MESSAGES = "SELECT x from RawMessageTx x WHERE x.status = 'FAILED' and x.tryAgain = 1 and x.retriesAttempted <= :retriesAllowed ORDER BY x.created ";
Route:
from("seda:retry_poll_failed_messages").routeId("retry_poll_failed_messages")
.setHeader("retriesAllowed", constant(retriesAllowed))
.toF("jpa:%s?namedQuery=findFailedMessages&maximumResults=%d", RawMessageTx.class.getName(),
maxMessagesPerPollAttempt)
.split(body())
.to("direct:anotherEndpoint");
I tried different things and it does not work and I can't find a good example online on this.
This is explained in the doc of JPA component:
parameters: This key/value mapping is used for building the query
parameters. It is expected to be of the generic type java.util.Map
where the keys are the named parameters of a given JPA query
This means that you have to do something like:
to("jpa:...?parameters=#myMap")
Where "myMap" is the name of a bean of type 'java.util.Map' that is available in Camel registry.
One possible way (among others) to register such bean could be a Camel Processor (to invoke before the jpa endpoint of course)
public void process(Exchange exchange) throws Exception {
Map<String, Object> params = Map.of("status", "demo");
exchange.getContext().getRegistry().bind("myMap", params);
}
Related
I'm implementing Transactional Outbox pattern using Java. The Message Relay Service will poll the Outbox table for entries, and after an outbox message is found and processed it will update the Outbox entry.
Question is, how do I set the parameter from MessagingGateway to the Jpa Query?
#Bean
public JpaExecutor jpaUpdateStateExecutor() {
JpaExecutor jpaExecutor = new JpaExecutor(this.entityManagerFactory);
jpaExecutor.setNamedQuery("myQuery");
jpaExecutor.setUsePayloadAsParameterSource(true);
jpaExecutor.setExpectSingleResult(true);
return jpaExecutor;
}
#Bean
#ServiceActivator(inputChannel = "jpaChannel")
public MessageHandler jpaOutbound() {
JpaOutboundGateway gateway = new JpaOutboundGateway(jpaUpdateStateExecutor());
gateway.setGatewayType(OutboundGatewayType.UPDATING);
return gateway;
}
My Gateway:
#MessagingGateway
public interface MyGateway {
#Gateway(requestChannel = "jpaChannel")
#Transactional
void jpaActions(Long idOfEntity);
}
My domain object:
#Entity
#Getter
#Setter
#ToString
#NoArgsConstructor
#Table
#NamedQuery(name = "myQuery", query = "UPDATE MyTable SET state = 'PROCESSED' WHERE id = :id")
public class Outbox {
#Id
#GeneratedValue(strategy = GenerationType.SEQUENCE)
private Long id;
private String state;
}
It would be great to see your query and how you'd like to have that Long idOfEntity to be mapped to some query param.
The doc for that setUsePayloadAsParameterSource looks like this:
use-payload-as-parameter-source
If set to true, the payload of the Message is used as a source for parameters. If set to false, the entire Message is available as a source for parameters. If no JPA Parameters are passed in, this property defaults to true. This means that, if you use a default BeanPropertyParameterSourceFactory, the bean properties of the payload are used as a source for parameter values for the JPA query. However, if JPA Parameters are passed in, this property, by default, evaluates to false. The reason is that JPA Parameters let you provide SpEL Expressions. Therefore, it is highly beneficial to have access to the entire Message, including the headers. Optional.
https://docs.spring.io/spring-integration/docs/current/reference/html/jpa.html#jpa-outbound-gateway-common-parameters
Perhaps you'd prefer to have that option as false and then in your query you could use a param for that idOfEntity as just a :payload placeholder.
Consider the following method on a Spring Data JPA interface:
#Query("select distinct :columnName from Item i")
List<Item> findByName(#Param("columnName") String columnName);
I would like to use such a method for performing queries dynamically using different column names on the same entity. How can this be done?
You can't. You'll have to implement such a method by yourself. And you won't be able to use parameters: you'll have to use String concatenation or the criteria API. What you'll pass won't be a column name but a field/property name. And it won't return a List<Item>, since you only select one field.
You can use QueryDSL support built into Spring Data. See this tutorial to get started.
First of all you must implement custom Spring Data repository by adding interface:
public interface ItemCustomRepository {
List<Item> findBy(String columnName, String columnValue);
}
then you must extend your current Spring Data repository interface with newly created i.e.:
public interface ItemRepository extends JpaRepository<Item, Long>, ItemCustomRepository, QueryDslPredicateExecutor {
}
and then you must implement your interface using Query DSL dynamic expression feature (the name ItemRepositoryImpl is crucial - it will let you use original Spring Data repository implementation):
public class ItemRepositoryImpl implements ItemCustomRepository {
#Autowired
private ItemRepository itemRepository;
public List<Item> findBy(final String columnName, final String columnValue) {
Path<Item> item = Expressions.path(Item.class, "item");
Path<String> itemColumnName = Expressions.path(String.class, item, columnName);
Expression<String> itemValueExpression = Expressions.constant(columnValue);
BooleanExpression fieldEqualsExpression = Expressions.predicate(Ops.EQ, itemColumnName, itemValueExpression);
return itemRepository.findAll(fieldEqualsExpression);
}
}
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 Spring Boot 1.5.4, Spring Data REST, HATEOAS. I'm exposing REST endpoints to be consumed from a Angular client.
I'm using spring.data.rest.enable-enum-translation=true to convert enums. It works fine both in GET and POST requests exposed from Spring Data REST from repositories.
I added a custom method in a repository:
#Transactional(readOnly = true)
#PreAuthorize("isAuthenticated()")
public interface TransitCertificateRepository extends PagingAndSortingRepository<TransitCertificate, Long> {
#Query("SELECT t FROM TransitCertificate t WHERE :states IS NULL OR status IN (:states) ")
public Page<TransitCertificate> findAllByParameters(
#Param("states") #RequestParam(value = "states", required = false) List<TransitCertificateStatus> states, Pageable pageable);
This is the enum:
public enum TransitCertificateStatus {
PENDING, USED, CANCELED, ARCHIVED
}
This is the relevant part of the model:
#Entity
#EntityListeners(TransitCertificateListener.class)
public class TransitCertificate extends AbstractEntity {
private static final long serialVersionUID = 5978999252424024545L;
#NotNull(message = "The status cannot be empty")
#Column(nullable = false)
#Enumerated(EnumType.STRING)
private TransitCertificateStatus status = TransitCertificateStatus.PENDING;
In rest-messages.properties I've translation for the enum like:
server.model.enums.TransitCertificateStatus.PENDING = Pending
server.model.enums.TransitCertificateStatus.USED = Used
When the client try to call my method findAllByParameters and sends a array of String (translated how the server sent back), the conversion on the server fails.
I don't understand why the conversion works in save() method, for example, but not in my method.
Furthemore if the client sends me 2 states, Spring returns this error:
Parameter value element [USED] did not match expected type [server.model.enums.TransitCertificateStatus (n/a)]; nested exception is java.lang.IllegalArgumentException: Parameter value element [USED] did not match expected type [server.model.enums.TransitCertificateStatus (n/a)]
So I guess I've two problems:
For some reason Spring is not able to convert a String[] to a List<TransitCertificateStatus> even if the value is exactly the one defined in the TransitCertificateStatus (PENDING, USED, CANCELED, ARCHIVED)
Spring is not able to convert the String the client send, to the right enum when the value is one of that defined in rest-messages.properties (Pending, Used, etc).
Is there a way to solve the problem is a elegant way using internal facilities of Spring Data REST (I point out enum transation works in save() method) without reinventing the wheel?
I ended up to solve the problem in this way:
I created a custom #RepositoryRestController
I created my method
I get the enum from the client like a String and then I convert it. In this way the client can send also the translated string for the enum
This a piece of code:
#PostMapping(path = "/licensePlates/searches")
public ResponseEntity<?> search(#RequestBody(required = true) List<Filter> filters, Pageable pageable, Locale locale,
PersistentEntityResourceAssembler resourceAssembler) {
EngineType engineType = enumTranslator.fromText(EngineType.class, filterMap.get("engineType"));
You have to inject enumTranslation in this way:
#Autowired
private EnumTranslator enumTranslator;
Not sure is the best way but that solved my problem with little code.
Normally I use annotiations:#Query("SELECT c FROM Country c") with JpaRepositoryor predefined methods like findAll
but in my case I want to generate dynamic query.
String baseQuery =SELECT c FROM Country c`
if(age!=null)
baseQuery+="WHERE c.age=20"
I need to perform same query from code level like this:
Query q1 = em.createQuery("SELECT c FROM Country c");
but I dont use EntityManager in spring boot
How can I generate query from code level?
If you would like to create dynamic queries from code you can take advantage of Spring's JdbcTemplate. Using spring boot it is as simple as injecting JdbcOperations bean to your repository class (assuming you have provided spring-boot-starter-jdbc module to your project).
But remember! This solution uses SQL, not JPQL. That's why you have to use proper tables and columns names in queries and properly map result to objects (i.e. using RowMapper)
This simple example worked fine for me (with different entity, but in same manner - I've adapted it to your example):
#Repository
public class CountryRepository {
#Autowired
private JdbcOperations jdbcOperations;
private static String BASIC_QUERY = "SELECT * FROM COUNTRY";
public List<Country> selectCoutry(Long age){
String query = BASIC_QUERY;
if (age != null){
query += " WHERE AGE = ";
query += age.toString();
}
//let's pretend that Country has constructor Conutry(String name, int age)
return jdbcOperations.query(query, (rs, rowNum) ->
{ return new Country(rs.getString("NAME"), rs.getInt("AGE");}
);
};
}
Then in service or whatever you inject CountryRepository and call method.
Since you're using Spring Boot, you can use Spring Data to create queries in your repository:
#Repository
public interface CountryRepository extends JpaRepository<Country, Long> {
}
Not a 100% on syntax, but should be something similar.
Now you can autowire this class:
#Autowired
public CountryRepository countryRepo;
And all basic methods are already available to you like:
countryRepo.findOne(id);
countryRepo.find();
If you want to make more advanced queries, you can use Spring Data e.g.:
#Repository
public interface CountryRepository extends JpaRepository<Country, Long> {
public Country findByNameAndContinent(String name, String continent);
}
This is just an example (a stupid one) of course and assumes your Country class has the field names 'name' and 'continent' and both are strings. More is available here:
http://docs.spring.io/spring-data/jpa/docs/current/reference/html/
Section 5.3 more specifically.
PS: Make sure your Country class has the #Entity annotation