2008-03-11

A NamedQuery a day keeps the RuntimeExceptions away

Getting your queries right takes some work; and if you get them wrong, you'll get exceptions at runtime, right?

Wrong. Using named queries means that (at least with sensible persistence engines) that your queries are parsed at startup time (technically when the persistence engine is instantiated). Your application fails early, and that is a good thing.

There is a further idea lurking below, so read on!

Consider the following code (Spring-/Hibernate-y, but the principle is general):

public User findUserByName(String username) {
return (User)getHibernateTemplate().find("select from User u where u.username = '" + username + "'");
}

Leaving aside the obvious stupidity of using string concatenation to construct queries (can you say "SQL injection"?), there is a further problem here.

Look at the User class:

@Entity
public class User {
@Id @GeneratedValue public Long id;
public String userName;
}

Spot the problem.

The above query will fail at runtime, with an error message along the lines of "failed to find property username of User"; the capitalization is wrong.

Of course, you will catch this during testing. You will, right? Right?

But why not let the engine catch it for you?

@Entity
@NamedQuery(name="User.byName", query="select from User u where u.userName = :username")
public class User {
@Id @GeneratedValue public Long id;
public String userName;
}

Any errors in the query will be reported at startup time. However, we introduce another failure point: What if you mistype the query name when using it? Or, for that matter, when defining it - I've done both?

public User findUserByName(String username) {
return (User)getHibernateTemplate().findByNamedQueryAndNamedParam("User.byname", "username", username);
}

But we have a compiler; introduce a constant and let it do the work for us!

@Entity
@NamedQuery(name=User.BY_NAME, query="select from User u where u.userName = :username")
public class User {
public static final String BY_NAME = "User.byName";
@Id @GeneratedValue public Long id;
public String userName;
}

public User findUserByName(String username) {
return (User)getHibernateTemplate().findByNamedQueryAndNamedParam(User.BY_NAME, "username", username);
}

End of problem.

Of course, you still have to remember how many parameters a query takes and what their names are. If I come up with a solution, you'll be the first to know.

2008-03-10

Tomcat and Log4jConfigListener don't mix

At least not if you want to load your log4j.properties with classpath:log4j.properties. What happens is that Tomcat internally uses commons-logging, which finds log4j on your classpath and thinks "Hey, I'll use log4j". Log4j then finds your log4j.properties and reads it before Log4jConfigListener ever gets instantiated.

The result?
log4j:ERROR setFile(null,true) call failed.
java.io.FileNotFoundException: /logs/spring.log (No such file or directory)
at java.io.FileOutputStream.openAppend(Native Method)
at java.io.FileOutputStream.(FileOutputStream.java:177)
at java.io.FileOutputStream.(FileOutputStream.java:102)
at org.apache.log4j.FileAppender.setFile(FileAppender.java:289)
at org.apache.log4j.FileAppender.activateOptions(FileAppender.java:163)
at org.apache.log4j.config.PropertySetter.activate(PropertySetter.java:256)
at org.apache.log4j.config.PropertySetter.setProperties(PropertySetter.java:132)
[...]
No worries, though; the application comes up just fine, and logs where you expect... If you're lucky and don't have a security manager.

With a security manager, you get a nice, fatal AccessControlException.

Bah.

The fix is simple: move log4j.properties away from the classpath root, e.g. into /WEB-INF.

2008-03-06

Marking target as derived

From this post on the maven-users mailing list comes the following script, lightly edited to work with the current version of Monkey:

--- Came wiffling through the eclipsey wood ---
/*
* Menu: Maven > Make Maven Targets Derived
* Kudos: Donnchadh Ó Donnabháin
* License: EPL 1.0
* DOM: http://download.eclipse.org/technology/dash/update/org.eclipse.eclipsemonkey.lang.javascript
*/

function main() {
var files = resources.filesMatching(".*/pom\\.xml");
var targetFolder;

for each( file in files ) {
if (targetFolder = file.eclipseObject.parent.findMember("target")) {
targetFolder.setDerived(true);
}
}
}
--- And burbled as it ran! ---
You will of course need Monkey installed, point Eclipse at http://download.eclipse.org/technology/dash/update/. Then copy the above script (including the funky separator lines) and select the Scripts->Paste New Script menu item.

Why would you want to do this? Well, if you (like me) are tired of Eclipse suggesting stuff in the target directory when you do "Open Resource", this is for you.