public Collection<Student> findByNameAgeGender(String name, int age, Gender gender) {
return em.createNamedQuery("Student.findByNameAgeGender")
.setParameter("name", name)
.setParameter("age", age)
.setParameter("gender", gender)
.setFirstResult(1)
.setMaxResults(30)
.setHint("hintName", "hintValue")
.getResultList();
}
Looking at this code we can now create a naive Query implementation:
public interface Query {
Query setParameter (String name, Object value);
Collection getResultList() throws SqlException;
}
public class PreparedSqlQuery implements Query {
protected Map params = new HashMap();
public Query setParameter (String name, Object value) {
params.add(name, value);
return this;
}
public Collection getResultList() throws SqlException {
// Execute query and return results
}
}
public class SimpleQueryUser {
public static void main(String[] args) throws Exception {
new PreparedSqlQuery().setParameter("name", "value").getResultList();
}
}
No problems at all, our Fluent Interface is clean and elegant. But what happens when you create a subclass for executing prepared statements? You don't want to implement all that multiple setXxx methods once again, and instead decide to inherit from PreparedSqlQuery:
public class StoredProcedureCall extends PreparedSqlQuery {
protected String outputParameter = null;
// Stored procedures may have both input and output parameters
public StoredProcedureCall setOutputParameter(String param) {
outputParameter = param;
}
public Collection getResultList() throws SqlException {
// Execute stored procedure and return results
}
}
public class SimpleQueryUser {
public static void main(String[] args) throws Exception {
// This works fine
new StoredProcedureCall().setOutputParameter("OUT").setParameter("name", "value").getResultList();
// This produces compilation errors
new StoredProcedureCall().setParameter("name", "value").setOutputParameter("OUT").getResultList();
}
}
The second oneliner produces a compilation error because Query object returned by setParameter() does not actually have setOutputParameter() method. To address this problem we can use return type covariance, which is a language feature added to Java 5. Here goes a fixed version of our StoredProcedureCall:
public class StoredProcedureCall extends PreparedSqlQuery {
protected String outputParameter = null;
public StoredProcedureCall setOutputParameter(String param) {
outputParameter = param;
}
// Compiler allows us to use StoredProcedureCall instead of Query as the return type here because StoredProcedureCall implements Query
public StoredProcedureCall setParameter (String name, Object value) {
super.setParameter(name, value);
return this;
}
public Collection getResultList() throws SqlException {
// Execute stored procedure and return results
}
}
Actually you can see exactly the same approach used in the real-world Query implementations, like OpenJPAQuery. Now what if you don't want to override all that numerous setXxx methods in all subclasses just to change their return types? Here is my solution utilizing another Java 5 language feature - Generics:
public interface Query <T extends Query >{
T setParameter (String name, Object value);
Collection getResultList() throws SqlException;
}
public class PreparedSqlQuery<S extends PreparedSqlQuery> implements Query<S> {
protected Map params = new HashMap();
public S setParameter (String name, Object value) {
params.add(name, value);
return (S) this;
}
public Collection getResultList() throws SqlException {
// Execute query and return results
}
// Hide constructor, use factory to ensure users won't try to do something like: new PreparedSqlQuery<StoredProcedureCall>().setOutputParameter
}
// We make it generic too to enable further extension
public class StoredProcedureCall<R extends StoredProcedureCall> extends PreparedSqlQuery<StoredProcedureCall> {
protected String outputParameter = null;
public R setOutputParameter(String param) {
outputParameter = param;
return (R) this;
}
// Note we don't have to override setParameter here
public Collection getResultList() throws SqlException {
// Execute stored procedure and return results
}
}
public class EntityManager {
public static StoredProcedureCall<StoredProcedureCall> createStoredProcCall() {
return new StoredProcedureCall<StoredProcedureCall>();
}
public static PreparedSqlQuery<PreparedSqlQuery> createNamedQuery() {
return new PreparedSqlQuery<PreparedSqlQuery>();
}
}
public class SimpleQueryUser {
public static void main(String[] args) throws Exception {
EntityManager.createStoredProcCall().setParameter("name", "value").setOutputParameter("OUT").getResultList();
}
}
All that trickery with generics is done to fool the compiler. Good thing about it is that no additional methods are created, that means there are no super calls, which can potentially improve runtime performance. Bad things are obvious too:
- It is not safe unless you are using factories to instantiate that builders;
- It is definitely not obvious and looks ugly;
Disclaimer: this is a hack, never use it in your code :)
This has been a major help to me in developing a nice object oriented structure for my project. This is good stuff and I am distressed to see how many Android programmers are abusing Java's advanced features and using hacks as their meat and potatoes. They don't like to use UML, so I don't know how to show them a class hierarchy. I want to see elegant solutions that read like sentences on the client side and efficient, concise and clean as possible on the back side. I don't have any bad habits yet, as I am new to Java but not programming. I have read most of Thinking in Java and Programming Android (O'Reilley) and I am working on Effective Java. I must say first that it is alot like C#.
ReplyDeleteThanks for clearing up some things for me. Could you recommend a good place (not SO) to talk Java? I need to have human conversations with other people who know about conversations with machines, but the flames, jeez, hackers are egomaniacal (as a group, present company excluded, wink, wink, nudge, nudge, say no more).