views:

893

answers:

6

This is more of a design than implementation question and it's going to be long so bear with me. It's best explained with an example:

Let's say I have a business entity called Product with a bunch of properties (name, price, vendor, etc...).

It's represented by an interface (Product) and implementation (ProductImpl, mapped in Hibernate) as well as basic CRUD service interface (ProductService) and implementation (ProductServiceImpl).
Product and ProductService are exposed as API, their implementations are not.

I want to add a List findProducts(QueryCriteria criteria) method to ProductService that would return a list of products satisfying given criteria. The requirements are:

  1. Query by direct Product properties (e.g. product.price gt 50.0)
  2. Query by association (e.g. product.vendor.name = "Oracle")
  3. Sort results (e.g. order by product.vendor.name desc, product.price asc")
  4. Apply additional filters. Unlike the above 3 items which are all specified by API client, additional filters may be applied by the service based on client's identity (e.g. client invoking this method may be limited to only seeing products manufactured by given vendor). Such filters take precedence over any criteria specified by the client (e.g. if the filter is set to product.vendor.name = "Microsoft", query in (2) above should produce empty result set.

The question, therefore, is what should QueryCriteria interface used by such a method look like? I can think of 3 solutions and I don't like either one of them:

  • Allow clients to specify HQL (starting with "where" clause) directly. This is the most straightforward solution, but also the most problematic security-wise. Even assuming that filters (#4 above) are simple enough to be implemented via Hibernate's session filters, HQL still needs to be parsed to - at the very least - ensure that query parameters are specified as parameters and not inlined.
  • Use thinly wrapped Hibernate's DetachedCriteria in place of QueryCriteria. "Thinly wrapped" because client can not be allowed to create DetachedCriteria directly for there would be no way to control what mapped entity it was created for. Also, this would not as flexible as HQL for some queries are not easily (or at all) expressible via Criteria API. As with HQL approach, filters (#4 above) will be limited to Hibernate session filters.
  • Write my own QueryCriteria interface / implementation which will form either DetachedCriteria or HQL behind the scenes. While probably the most flexible solution, this will have to duplicate a lot of code from Criteria API which seems less than ideal.

Any comments on the validity of the above approaches or - fingers crossed - simple elegant solutions that didn't occur to me would be highly appreciated.

P.S. In my specific case, all API clients are internal and "semi-trusted" - that is I'm not as much concerned with someone trying to deliberately break something as with poor programming resulting in Cartesian product of 5 tables :-) However, it'd be nice to come up with a solution that would withstand API exposure to public.

A: 

It is never a good idea to expose such implementation details. You are bounded to that library from there on. Worse, any api change of the library will couse an api i change of your service. Any security considerations left behind...

What about bean property names used in critera (a triple of property name, enum with less, equal and more and value). With a bean wrapper on your model, you may transform this to a hibernate criteria.

It will also be possible to transform this property names to a new version after a model change.

Arne Burmeister
I realize that you can abstract everything but there's a line somewhere - I'm pretty sure I'm not going to be switching ORM layers and I doubt Hibernate API will change in backwards-incompatible way in the foreseeable future. That said, I did not suggest exposing straight Hibernate API in any of the 3 proposed solutions above. Not sure what you meant by "model bean wrapper" but property / comparison / value is just a simple case. Once you add ANDs / ORs, property-to-property comparisons, collections, etc... you'll end up rewriting the entire Criteria API.
ChssPly76
+1  A: 

Hmm - interesting question.

Having thought it over, writing you own criteria interface is probably the way to go. It won't tie you to an implementation and will lower the security concerns.

Also depending on how many objects are involved have considered returning the whole set of products (with necessary filters applied) then having the end user apply filters with lambdaj or similar. See:

http://code.google.com/p/lambdaj/

Pablojim
Thank you. LambdaJ is an interesting approach for applying post-criteria filters (#4 above) that can't be expressed via Hibernate filters. But as you said, that's not going to help with find() method due to data volume.
ChssPly76
A: 

Hibernate is a low-level infrastructure framework, and as such should remain hidden behind the scenes. If next month your application needs to switch to another ORM framework for some reason, your API will be useless. Encapsulation even between layers within the same application, is vitally important.

Having said all this, I think your method should receive an abstraction of the information you need to perform the search. I advise you to create an enum of Product's fields, and implement one or two simple versions of Restriction.

The parameters to the method can be a list of equality restrictions, another list of relative restrictions and of course an order indicator (one of the enum values plus a flag for asc/desc).

This is just a general direction, I hope I made my point clear =8-)

Yuval
Layer separation is not the same as dependency (or absence thereof) on third-party library. If my application were to switch ORM layers, this API would be the last of my problems (far lower on the list than hundreds of Hibernate-annotated entities and thousands of HQL queries that will all have to be redone). That point aside, what you're suggesting is my own (more restricted) DetachedCriteria re-implementation with property names further wrapped in enum in case my product interface needs to change next month? :-) Or did I misunderstand?
ChssPly76
+1  A: 

Option One: If it's possible to expand your API, I suggest making your API "richer" -- adding more methods such as a few below to make your service sound more natural. It can be tricky to make your API larger without it seeming bloated, but if you follow a similar naming scheme it will seem natural to use.

productService.findProductsByName("Widget")
productService.findProductsByName(STARTS_WITH,"Widg")
productService.findProductsByVendorName("Oracle")
productService.findProductsByPrice(OVER,50)

Combining the results (applying multiple restrictions) could be left as something for the clients to do after they received the result set by using CollectionUtils and Predicates. You could even build a few common Predicates for the consumers of your API just to be nice. CollectionUtils.select() is fun.

Option Two: If it is not possible to expand the API, your third bullet is the one I would go with.

  • Write my own QueryCriteria interface / implementation which will form either DetachedCriteria or HQL behind the scenes...

You could try to apply a DSL style approach to the naming using something akin to the Builder pattern to make things more readable and natural sounding. This gets a little clumsy in Java with all the dots and parens, but maybe something like:

Product.Restriction restriction = new Product.Restriction().name("Widget").vendor("Oracle").price(OVER,50) );
productService.findProducts(restriction);

Option Three: Combine the two approaches, providing a restriction-style criteria along with a richer API. These solutions would be clean in that they hides the Hibernate implementation details from the consumer of your API. (Not that anyone would ever think of switching away from Hibernate.)

dustmachine
Thank you for the answer. While rich API works great for "known" clients (e.g. other services with predictable / known queries), it's not going to work for UI where criteria / sort order are arbitrarily specified by users. QBE-style queries quickly approach full DetachedCriteria API in complexity once associations / collections are involved so that's also not the answer. So consensus does seem to be on reinventing the wheel here, huh? I really don't like that :-(
ChssPly76
Perhaps evaluate whether clients need absolutely everything. In similar situations, for example, I provide the results to the consuming applications in a default sort order, and if they want the results sorted differently then I leave it to them to perform the alternate sort.
dustmachine
A: 

I think query by example would work really good here.

01
I disagree. Hibernate QBEs are _extremely_ limited - "equals" / "like" operations only, no condition grouping, etc... And doing your own implementation is effectively the same as writing your own version of DetachedCriteria
ChssPly76
+2  A: 

The actual solution I've implemented uses a hybrid approach.

Methods that use well-defined queries (e.g. methods that are used internally by other services, predefined reports, etc.) have signature similar to HibernateTemplate's findBy methods:

public List<Entity> findEntities(String queryName, QueryParameters parameters);

where QueryParameters is a convenience class for specifying named parameters explicitly or taking them from a bean. Sample usage is:

List<Product> products = findProducts("latestUpdates",
 new QueryParameters()
  .add("vendor", "Oracle")
  .add("price", "50.0")
);

or

List<Product> products = findProducts("latestUpdates",
 new QueryParameters(product, "vendor", "price"));

Access to such methods is limited to "trusted" code; queries used obviously must obviously be defined in Hibernate mappings. Filters are built into query or defined as session filters. The benefits are cleaner code (no Criteria-like stuff spread across half a page) and clearly defined HQL (easier to optimize and deal with cache if necessary).


Methods that are exposed to UI or otherwise need to be more dynamic use Search interface from Hibernate-Generic-DAO project. It's somewhat similar to Hibernate's DetachedCriteria but has several advantages:

  1. It can be created without being tied to particular entity. It's a big deal for me because entity interface (part of API visible to users) and implementation (POJO mapped in Hibernate) are two different classes and implementation is not available to user at compile time.

  2. It's a well thought out open interface; quite unlike DetachedCriteria from which it's nearly impossible to extract anything (yes, I know DC wasn't designed for that; but still)

  3. Built-in pagination / results with total count / bunch of other little niceties.

  4. No explicit ties to Hibernate (though I personally don't really care about this; I'm not going to suddenly drop Hibernate and go with EclipseLink tomorrow); there are both Hibernate and generic JPA implementations available.

Filters can be added to Search on service side; that's when entity class is specified as well. The only thing's missing is quick-fail on client side if invalid property name is specified and that can be resolved by writing my own ISearch / IMutableSearch implementation but I didn't get to that yet.

ChssPly76