2

Lets say that i have an abstract class Product and i have two more entities that extend it ProductA and ProductB with different fields. I am using Product repository to search for all of them using Specification and SpecificationBuilder using some common fields. What i want to accomplish is to detect what type of product it is (A or B) and based on that to use different search query.

For example ProductA has stock and ProductB has featured so naturally if i insert search criteria quantity and try to do a specification on Product repo, i would like the api to search for ProductA. Is this form of search possible in any way? This is my current code:

public interface ProductRepo extends JpaRepository<Product, Integer>,JpaSpecificationExecutor<Product>{

}

public class ProductSpecificationBuilder {
 public final ProductSpecificationBuilder with(final String orPredicate, final String key
                , final String operation, Object value, final String prefix, final String suffix) {
            System.out.println("called "+value);
            //Fixing boolean value

            if (!key.equals("visible") || key.equals("modified")) {
                System.out.println("not equal visible");
            SearchOperation op = SearchOperation.getSimpleOperation(operation.charAt(0));
            if (op != null) {
                if (op == SearchOperation.EQUALITY) { // the operation may be complex operation
                    final boolean startWithAsterisk = prefix != null && prefix.contains(SearchOperation.ZERO_OR_MORE_REGEX);
                    final boolean endWithAsterisk = suffix != null && suffix.contains(SearchOperation.ZERO_OR_MORE_REGEX);
                    System.out.println("prefix "+startWithAsterisk+" "+endWithAsterisk);
                    if (startWithAsterisk && endWithAsterisk) {
                        op = SearchOperation.CONTAINS;
                    } else if (startWithAsterisk) {
                        op = SearchOperation.ENDS_WITH;
                    } else if (endWithAsterisk) {
                        op = SearchOperation.STARTS_WITH;
                    }
                }else if (op == SearchOperation.LIKE) {
                    System.out.println("we got like in builder");
                }
                params.add(new SearchCriteria(orPredicate, key, op, value));
            }
            }
            return this;
        }
}
public class ProductSpecification implements Specification<Product>{
@Override
    public Predicate toPredicate(final Root<Product> root, final CriteriaQuery<?> query, final CriteriaBuilder builder) {
        //TODO JOIN class for nested search for promotion
        switch (criteria.getOperation()) {
        case EQUALITY:
            return builder.equal(root.get(criteria.getKey()), criteria.getValue());
        case NEGATION:
            return builder.notEqual(root.get(criteria.getKey()), criteria.getValue());
        case GREATER_THAN:
            return builder.greaterThan(root.get(criteria.getKey()), criteria.getValue().toString());
        case LESS_THAN:

            return builder.lessThan(root.get(criteria.getKey()), criteria.getValue().toString());
        case LIKE:
            return builder.like(root.get(criteria.getKey()), criteria.getValue().toString());
        case STARTS_WITH:
            return builder.like(root.get(criteria.getKey()), criteria.getValue() + "%");
        case ENDS_WITH:
            return builder.like(root.get(criteria.getKey()), "%" + criteria.getValue());
        case CONTAINS:
            return builder.like(root.get(criteria.getKey()), "%" + criteria.getValue() + "%");
        default:
            return null;
        }
    }
}
public enum SearchOperation {
    EQUALITY, NEGATION, GREATER_THAN, LESS_THAN, LIKE, STARTS_WITH, ENDS_WITH, CONTAINS;

    public static final String[] SIMPLE_OPERATION_SET = { ":", "!", ">", "<", "~","@"};

    public static final String OR_PREDICATE_FLAG = "'";

    public static final String ZERO_OR_MORE_REGEX = "*";

    public static final String OR_OPERATOR = "OR";

    public static final String AND_OPERATOR = "AND";

    public static final String LEFT_PARANTHESIS = "(";

    public static final String RIGHT_PARANTHESIS = ")";

    public static SearchOperation getSimpleOperation(final char input) {
        switch (input) {
        case ':':
            return EQUALITY;
        case '!':
            return NEGATION;
        case '>':
            return GREATER_THAN;
        case '<':
            return LESS_THAN;
        case '~':
            return LIKE;
        case '@':{

            return CONTAINS;
        }
        default:
            return null;
        }
    }
}

And my Product classes

@Entity
@Table(name = "products")
@Inheritance(strategy = InheritanceType.JOINED)
public abstract class Product {


    @Id
    @GeneratedValue(strategy = GenerationType.AUTO, generator = "native")
    @GenericGenerator(name = "native", strategy = "native")
    @Column(name = "id")
    private int id;
    -----Common fields----
}
public class ProductA extends Product{ int stock; }
public class ProductB extends Product{ int featured; }

If you have any questions feel free to ask. Any form of help would be much appreciated! Thank you in advance.

vibetribe93
  • 257
  • 8
  • 23
  • Hi, it looks like this could help you: https://stackoverflow.com/a/48576814/7804652 if this does not help, please also check out query by example: https://github.com/spring-projects/spring-data-examples/tree/master/jpa/query-by-example – dinosaur Dec 19 '18 at 22:56
  • Thanks for the reply, checked it. It does not fit my cause simply because i need to search by fields from class that is inheriting the abstract class and those fields could have multiple options etc. query by example as far as i understand does not give that option... Or am i wrong? – vibetribe93 Dec 24 '18 at 04:00

1 Answers1

1

As far as I know there is no "nice way" of doing this (if you want to preserve the type), but you could employ one of the following solutions:

0. Switch case generally I am really against switch and case in Java but it has its own place... The easiest solution is to build your query in separate cases as well... they are separate cases (pun not intended).

From your question I assume that you have a way more complicated structure than the presented one, so next I have two possible solutions for you.

1. Programmatic solution You could create a solution based on reflection. You can have a full solution implementing some interesting logic. Mainly you have to have a list from the objects that are "searchable" in this feature. In a loop you can use reflection to determine if the field is "acceptable" in the entity. (For example ˙stock˙ is only acceptable in PRODUCT_A so the creation of the criteria will be based on that Entity. I definitely do not recommend this approach as this could introduce a lot of problems. (For example how would you handle if stock and featured is set simultaneously in your DTO?

2. create a view to construct your search basically you create a database view and an entity in Spring having all the possible parameters.

Original tables:

 |TABLE_A   |     |TABLE_B   |
 |ID|FIELD_A|     |ID|FIELD_B|
 |__________|     |__________|
 | 1|EXAMPLE|     | 1|  STUFF|

View:

 |COMBINED_VIEW                |
 |ID|FIELD_A|FIELD_B|ORIG_TABLE|
 |_____________________________|
 | 1|EXAMPLE|   null|   TABLE_A|
 | 1|   null|  STUFF|   TABLE_B|

Creation:

SELECT 
 ID as id, FIELD_A as field_a, null as field_b, 'TABLE_A' as ORIG_TABLE
 FROM TABLE_A 
UNION ALL
SELECT 
 ID as id, null as field_a, FIELD_B as field_b, 'TABLE_B' as ORIG_TABLE
 FROM TABLE_B 

Be careful though, the ID field could mess you up if you use caching, add a generated id or include the ORIG_TABLE in to your @Id mapping.

TL.DR.: The second way you could map all the stuff you need and (in my opinioin) it is an acceptable way to get the results. It is also a good point that you do not have take into consideration if more question parameters are set than possible for a single entity. (For example: FIELD_A='EXAMPLE' AND FIELD_B='STUFF' simply there will be no result.) While there are other ways to achieve this like using JPQL and left join and constructor mapping I think the second option is the clearest and most maintainable one.

Hash
  • 4,647
  • 5
  • 21
  • 39
  • Hello, thanks on the reply! I will experiment a bit with the info you provided and give a feedback here when i get some results – vibetribe93 Dec 26 '18 at 20:38
  • Have you had the time to check the solutions? :) – Hash Dec 29 '18 at 05:12
  • Hey, yes. By reflection it seems like a mess, the structure of the database is too complicated for it in my opinion, to many different nested entities from different extended classes...Thought about the second option and it could be done, but over there i have overlapping ids because Table A and Field from Table b represent almost the same thing and if i put them in the same table, the code logic doesn't seem clean. I am going with switch case for now and will do the second option when i clean up the logic. Will post what i've done here. – vibetribe93 Jan 03 '19 at 21:22
  • Could you accept the answer? The bounty has expired so the points of it got lost sadly :( – Hash Jan 06 '19 at 07:53