Working with Spring data jpa and specifications, I have a requirement to implement a filter/search feature in spring mvc. The backend receives an object (ReportTemplateBean) which is basically a bean with some fields that represents the filters in the front end.
public class ReportTemplateBean implements Serializable {
private static final long serialVersionUID = -3915391620260021813L;
private Long id;
private String property;
private String city;
private String state;
private String zipCode;
private String propertyStatus;
private String realEstateRep;
//more code
We have the Controller
#RequestMapping(value = "/search", method = RequestMethod.GET)
#ResponseBody
public ReportBean search(#AuthenticationPrincipal ActiveUser activeUser,
#ModelAttribute("templateForm") ReportTemplateBean template,
Pageable pageable) throws GenericException {
LOGGER.info("Pulling report requested");
ReportBean report = reportService.searchProperties(template,
pageable.getPageNumber(), pageable.getPageSize());
return report;
}
The service
#Override
#Transactional(readOnly = true, timeout = 20)
public ReportBean searchProperties(ReportTemplateBean template,
Integer pageNumber, Integer pageSize) throws GenericException,
TransactionTimedOutException {
LOGGER.info("searchProperties({})", template);
try {
// pageNumber = (pageNumber == null ? 0 : pageNumber);
// pageSize = (pageSize == null ? 10 : pageSize);
ReportTemplate t = reportTemplateMapper.beanToEntity(template);
List<PropertyBean> beans = new ArrayList<PropertyBean>();
PropertySpecification spec = new PropertySpecification(t);
Page<Property> properties = propertyRepository.findAll(spec,
new PageRequest(pageNumber, pageSize, Sort.Direction.ASC,
"name"));
And then it builds the query dynamically, but using a long IF chain that I don't like it. This is the Specification.
#SuppressWarnings("unchecked")
#Override
public Predicate toPredicate(Root<Property> root, CriteriaQuery<?> query,
CriteriaBuilder cb) {
Path<String> propertyName = root.get(Property_.name);
Path<String> city = root.get(Property_.city);
Path<String> state = root.get(Property_.state);
Path<String> zipCode = root.get(Property_.zipCode);
final List<Predicate> orPredicates = new ArrayList<Predicate>();
final List<Predicate> andPredicates = new ArrayList<Predicate>();
if (template.getProperty() != null
&& template.getProperty().length() > 0) {
andPredicates.add(cb.equal(propertyName, template.getProperty()));
}
if (template.getCity() != null && template.getCity().length() > 0) {
andPredicates.add(cb.equal(city, template.getCity()));
}
if (template.getState() != null && template.getState().length() > 0) {
andPredicates.add(cb.equal(state, template.getState()));
}
if (template.getZipCode() != null && template.getZipCode().length() > 0) {
andPredicates.add(cb.equal(zipCode, template.getZipCode()));
}
if (template.getRealEstateRep() != null) {
Join<Property, User> pu = null;
if (query.getResultType().getName().equals("java.lang.Long")) {
pu = (Join<Property, User>) root.fetch(Property_.createdBy);
} else {
pu = root.join(Property_.createdBy);
}
Path<Long> userId = pu.get(User_.id);
andPredicates.add(cb.equal(userId, template.getRealEstateRep()));
}
if (template.getProjectType() != null
&& template.getProjectType().length() > 0) {
Join<Property, Project> pp = null;
if (query.getResultType().getName().equals("java.lang.Long")) {
pp = root.join(Property_.projects);
} else {
pp = (Join<Property, Project>) root.fetch(Property_.projects);
}
Path<String> projectType = pp.get(Project_.projectName);
andPredicates.add(cb.equal(projectType, template.getProjectType()));
}
//more IF's
return query.getRestriction();
}
As you can notice the Specification seems ugly and besides that SONAR complains about The Cyclomatic Complexity of this method (which is good).
So question is, How can I refactor the Specification (IF's) to be more OO code?.
Thanks in advance.
UPDATE- I would like to use/implement something like the new feature in Spring Data JPA (Query by Example) It seems that if you pass a bean the ExampleMatcher class will ignore the null value in the bean fields which is almost what I am looking for. Ignore null and empty values.
I write my solution to give you another option, but as i say in the comment i do not used Specification, and i am curious to see if anyone knows another way to do dynamic queries in spring jpa.
You could write your own query with the #Query annotation inside a #Repository interface.
In your case (assuming ReportTemplateBean is your Entity and its primary key is of Long type) it would be like:
#Repository
public interface ReportTemplateRepo extends JpaRepository<ReportTemplateBean, Long>{
#Query("SELECT rb FROM ReportBeanTemplate rb JOIN ExampleTable et WHERE et.idTemplate = rb.id AND (:id is null OR :id = rb.id) AND (:city is null OR :city = rb.city) AND (:state is null OR :state = rb.state)")
public List<ReportTemplateBean> findTemplates(#Param("id") Long id, #Param("city") String city, #Param("state") String state);
}
You can add all the parameters you want, pass it as null when you call the method.
Example of method invocation(in you service class):
#Autowire
ReportTemplateRepo templateRepo;
public void invocation(ReportTemplateBean template){
List<ReportTemplateBean> templateRepo.findTemplates(
template.getId(), template.getCity(), template.getState());
}
This is the only way i found to do this kind of query.
Related
Graphql Query
type Query {
filteredPersons(filter: Person) : PersonApi
}
PersonApi
type PersonApi{
items: [Person]
total_count: Int
}
Person
type Person{
id: ID!
firstName : String
lastName : String
}
Resolver
public PersonApi filteredPersons(Person filter) {
ExampleMatcher matcher = ExampleMatcher.matchingAny()
.withMatcher("firstName", ExampleMatcher.GenericPropertyMatchers.contains().ignoreCase())
.withMatcher("lastName", ExampleMatcher.GenericPropertyMatchers.contains().ignoreCase()));
Example<Person> filteredPerson = Example.of(filter, matcher);
Page<Person> personPage = repository.findAll(example);
if (personPage != null) {
return new PersonApi (
new ArrayList<>(personPage.getContent()),
NumberUtils.toInt(String.valueOf(personPage.getTotalElements())));
}
return new PersonApi(new ArrayList<>(), NumberUtils.INTEGER_ZERO);
}
}
Repository
#Repository
public interface PersonRepository extends JpaRepository<Person, Integer> {}
As show the code above, I'm trying to make a query with a filter parameter that is a type. Obviously, graphql expect this parameter to be an input.
This mean I have to use a dto (PersonFilter) and translate it into an entity in order to do the query.
private CsvRawEntity getPersonFrom(PersonFilter filter) {
Person entity = new Person();
entity.setFirstName(filter.getFirstName());
entity.setLastName(filter.getLastName());
return entity;
}
public PersonApi filteredPersons(Person filter) {
...
Person entity = getEntityFrom(filter);
Example<Person> example = Example.of(entity, customExampleMatcher);
personPage = repository.findAll(example, pageable);
PersonApi response = new PersonApi(
new ArrayList<>(personPage.getContent()),
NumberUtils.toInt(String.valueOf(personPage.getTotalElements())));
return response;
}
(which then work like a charm).
Same issue with outputif my type contains another type.
Question
Is there is a way or trick to make GraphQL to accept a type when it expect an input or an output ?
I didn't find anywhere whether it's possible to create pageable manually from string, let assume that we have the following service method
public <T> List<T> findAnything(final int page, final int size, final String sort) { // e.g. id,desc&username,asc
final Pageable pageable = new PageRequest(page, size, null);
return null;
}
my question is how can i instantiate an object of
org.springframework.data.domain.Sort
from a given string of format, important note that these parameters are chagned dynamically, so more likely i need a path to the spring parser, in my example im passing null instead the object
id,desc&username,asc
EDIT
A little bit more details I'm looking for a mechanism of how spring converts the 'sort' string(with the rest of default parameters) that's coming to the rest endpoint as a query param to Pageable object
You can do :
private Sort orderBy() {
return new Sort(Sort.Direction.DESC, "ID")
.and(new Sort(Sort.Direction.ASC, "username"));
}
I think this is helpful
Sort class has static nested class Order :
public static class Order{
private final Direction direction;
private final String property;
private final boolean ignoreCase;
private final NullHandling nullHandling;
}
and then you can use :
public static Sort by(List<Order> orders)
where you create your Order from String like simply splitting.
For that purpose I've written something similar to what spring has, i'd be happy if spring exposes SortHandlerMethodArgumentResolver.parseParameterIntoSort for usage outside the package but so far it's not
private Sort parseMultipleSortQueries(final String query) {
final String[] queries = query.split("&");
return parseSortQuery(queries, ",");
}
private Sort parseSortQuery(final String[] query, String delimiter) {
final List<Sort.Order> orders = new ArrayList<>();
for (String q : query) {
if (q == null) {
continue;
}
final String[] parts = q.split(delimiter);
final Sort.Direction direction = parts.length == 0 ? null : Sort.Direction.fromStringOrNull(parts[parts.length - 1]);
for (int i = 0; i < parts.length; i++) {
if (i == parts.length - 1 && direction != null) {
continue;
}
final String property = parts[i];
if (!StringUtils.hasText(property)) {
continue;
}
orders.add(new Sort.Order(direction, property));
}
}
return orders.isEmpty() ? null : new Sort(orders);
}
and here is the test
#Test
public void testParseQuery() {
System.out.println(parseMultipleSortQueries("firstName,asc&lastName,desc")); //firstName: ASC,lastName: DESC
}
I need to execute a procedure on my sql server database that will return me some fields and I wish to transform this fields directly in a List of my DTO Object that will be returned, but i'm new on spring boot and can't get it to work. I tried to do a Converter class but didnt understand much of how it works e probally did it wrong, here is my code on a way i wish it work:
public interface IMyDtoRepository extends JpaRepository<SomeEntity, Long> {
#Query(value = "EXECUTE MyProcedure :param1, :param2, :param3, :param4, :param5)")
public List<MyDtoObject> execMyProcedure(#Param(value = "param1") Integer param1,
#Param(value = "param2") String param2,
#Param(value = "param3") String param3,
#Param(value = "param4") String param4,
#Param(value = "param5") Integer param5);
}
The DtoObject
public class MyDtoObject{
// My Declared Fields...
public MyDtoObject() {
}
public MyDtoObject(/* My Fields */) {
// Setting fields
}
public MyDtoObject(Object[] objects) {
// Setting fields
}
// Getters n Setters...
I omitted the information that i didn't think it was necessary but i can give more explanation if need it
to map the result on your DtoObject with spring-data-jpa your can use : #SqlResultSetMapping
javadoc here
I have a similar method that I use in my DAL. It uses reflection and generics to convert a datatable to whatever type you pass in. Just pass in the datatable you get as a result of your procedure and you're good to go.
public List<T> ConvertDataToTypeList<T>(System.Data.DataTable DataTable) where T : class, new()
{
try
{
System.Type t_Object_Type = typeof(T);
ICollection<PropertyInfo> p_Properties;
lock (Properties_Dictionary)
{
if (!Properties_Dictionary.TryGetValue(t_Object_Type, out p_Properties))
{
p_Properties = t_Object_Type.GetProperties().Where(property => property.CanWrite).ToList();
Properties_Dictionary.Add(t_Object_Type, p_Properties);
}
}
System.Collections.Generic.List<T> l_List = new List<T>(DataTable.Rows.Count);
foreach (var v_Row in DataTable.AsEnumerable())
{
T o_Object = new T();
foreach (var prop in p_Properties)
{
var propType = Nullable.GetUnderlyingType(prop.PropertyType) ?? prop.PropertyType;
var safeValue = v_Row[prop.Name] == null ? null : Convert.ChangeType(v_Row[prop.Name], propType);
prop.SetValue(o_Object, safeValue, null);
}
l_List.Add(o_Object);
}
return l_List;
}
catch
{
return new List<T>();
}
}
I have a few repositories that extend org.springframework.data.mongodb.repository.MongoRepository. I have added some methods for searching entities by different parameters, however, in any search, I only want to search for entities that have the active field set to true (have opted for marking as active=false in place of deleting). For example, two sample repositories would look like this:
interface XxRepository extends MongoRepository<Xx, String> {
Optional<Xx> findOneByNameIgnoreCaseAndActiveTrue(String name)
Page<Xx> findByActiveTrue(Pageable pageable)
Xx findOneByIdAndActiveTrue(String id)
}
interface YyRepository extends MongoRepository<Yy, String> {
Optional<Yy> findOneByEmailAndActiveTrue(String email)
}
Is there any way that would allow me not to add byActiveTrue\ andActiveTrue to each and every method and set it up somewhere in one place for all the queries?
Please try this. No need to provide implementation. Change 'active' and 'email' to your db column name.
interface YyRepository extends MongoRepository<Yy, String> {
#Query(value = "{ 'active' : { '$eq': true }, 'email' : ?0 }")
Optional<Yy> findOneByEmailAndActiveTrue(#Param("email") String email)
}
You can write an template query into the MongoRepository using Criteria.
Example
class abstract MongoRepository<W, X> {
protected Class<W> typeOfEntity;
private Class<X> typeOfId;
public MongoRepository() {
typeOfEntity = (Class<W>) ((ParameterizedType) getClass().getGenericSuperclass()).getActualTypeArguments()[0];
typeOfId = (Class<X>) ((ParameterizedType) getClass().getGenericSuperclass()).getActualTypeArguments()[0];
}
public W get(X id) throws Exception {
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<W> q = cb.createQuery(typeOfEntity);
Root<W> r = q.from(typeOfEntity);
String idName = CommonEntity.findIdFieldName(typeOfEntity);
Predicate andClose = cb.and(cb.equal(r.get("active"), true));
q.where(cb.equal(r.get(idName), id), andClose);
return em.createQuery(q).getSingleResult();
}
}
After that, you stay confident into the object running way ans stereotyping to run the good type of request.
The findIdFieldNameis an method using the #Id to get the id field name.
public abstract class CommonEntity implements Serializable {
public static String findIdFieldName(Class cls) {
for (Field field : cls.getDeclaredFields()) {
String name = field.getName();
Annotation[] annotations = field.getDeclaredAnnotations();
for (int i = 0; i < annotations.length; i++) {
if (annotations[i].annotationType().equals(Id.class)) {
return name;
}
}
}
return null;
}
}
What I am trying to achieve is to set a result transformer on a query defined in the following way:
String hqlQueryString = "select o.id as id, o.name as objectName from MyObject";
Class resultClass = MyObject.class;
Query query = session.createQuery(hqlQueryString).setResultTransformer(
new new AliasToBeanResultTransformer(resultClass));
List result = query.list();
MyObject looks like this:
public class MyObject {
private int id;
private String objectName;
public int getId() {
return id;
}
public void setId(int value) {
this.id = value;
}
public String getObjectName() {
return objectName;
}
public void setobjectName(String value) {
this.objectName = value;
}
}
The problem is, that although I have specified id and objectName to be my aliases, the actual query being executed uses different aliases. This causes my AliasToBeanResultTransformer to fail to construct MyObject because the aliases do not match property names.
Is it possible to obtain the aliases of the query generated by hibernate programmatically (I can set them to the alias to bean result tranformer)? I tried using query.getReturnAliases() but it returns the aliases that I have defined in my HQL, not the ones that Hibernate actually uses.
Can I explicitly specify the aliases in a createQuery statement? Currently I am trying not to use criterion for this to work, so I'd appreciate an approach that uses query objects, if such exists.
Update
Although the issue described above is invalid for standard HQL queries (see comments), it is valid when executing a native query. To be specific - native queries seemed to treat all aliases as lowecase strings (despite specific capitalization that might have been introduced in the query). This causes the AliasToBeanResultTransformer to fail when setting the properties, in cases where capitalization matters.
Actually don't need to implement another AliasToBeanResultTransformer , you can use addScalar(String columnAlias, Type type) to explicitly alias the columns of the native SQL:
String nativeSQL = "select o.id as id, o.name as objectName from MyObject";
List<MyObject> resultList = session.createSQLQuery(nativeSQL)
.addScalar("id" ,StandardBasicTypes.INTEGER)
.addScalar("objectName",StandardBasicTypes.STRING)
.setResultTransformer(new AliasToBeanResultTransformer(MyObject.class))
.list();
The transformer will then look for a MyObject class and expect it having the setters setId() and setObjectName() in order to populate the returned values to the MyObject instance
As for native queries, there was no simple solution involved. I had to look into the implementation of the AliasToBeanResultTransformer class and put a fix in there. I resolved the problem by creating a copy of the AliasToBeanResultTransformer class and modified the private initialize method of that class in the following way:
public class CaseInsensitiveAliasToBeanResultTransformer {
private void initialize(String[] aliases) {
this.aliases = new String[ aliases.length ];
setters = new Setter[aliases.length];
for ( int i = 0; i < aliases.length; i++ ) {
String alias = aliases[i];
if (alias != null) {
this.aliases[i] = alias;
setters[i] = CaseInsensitiveSetter.getSetter(resultClass, alias);
}
}
isInitialized = true;
}
}
This code differs mainly in the line CaseInsensitiveSetter.getSetter(resultClass, alias), where I have introduced a CaseInsensitiveSetter class I will describe below. This class implements the Setter interface and allows retrieving the setter method of a class using case-insensitive matching - so this will allow me to bind the lower-cased query aliases to the proper members of my result class. Here is the code of the custom setter (only the important lines are shown for brevity):
public class CaseInsensitiveSetter {
public static Setter getSetter(Class<?> theClass, String propertyName) {
Setter setter;
if (theClass == Object.class || theClass == null) {
setter = null;
} else {
setter = doGetSetter(theClass, propertyName);
if (setter != null) {
if (!ReflectHelper.isPublic(theClass, setter.getMethod())) {
setter.getMethod().setAccessible(true);
}
} else {
setter = doGetSetter(theClass.getSuperclass(), propertyName);
if (setter == null) {
Class<?>[] interfaces = theClass.getInterfaces();
for (int i = 0; setter == null && i < interfaces.length; i++) {
setter = doGetSetter( interfaces[i], propertyName);
}
}
}
if (setter == null) {
throw new PropertyNotFoundException(
"Could not find a setter for property " +
propertyName + " in class " + theClass.getName());
}
}
return setter;
}
// The actual work is done here
private static Setter doGetSetter(Class<?> resultClass, String propertyName) {
Method[] methods = resultClass.getDeclaredMethods();
for (int i = 0; i < methods.length; i++) {
// only carry on if the method has 1 parameter
if ( methods[i].getParameterTypes().length == 1 ) {
String methodName = methods[i].getName();
if (methodName.startsWith("set")) {
String testStdMethod = methodName.substring(3);
if (testStdMethod.equalsIgnoreCase(propertyName)) {
Setter result = new CustomSetter(
resultClass, methods[i], propertyName);
return result;
}
}
}
}
return null;
}
}
The source of this is based on the BaseSetter class that comes with Hibernate, but is changed to support case-insensitive matching. Still, this one, and the original class that Hibernate uses, lacks performance because of the heavy usage of reflection.
Also, keep in mind that if the result class contains different properties with names that would be equal in case-insensitive comparison, then only one of them will be picked by the current code and it might not work as expected.