views:

1576

answers:

9

Java - Storing SQL statements in an external file

I am looking for a Java library/framework/technique of storing SQL statements in an external file. The support team (including DBAs) should be able to alter (slightly) the statement to keep them in sync in case database schema changes or for tuning purposes.

Here are the requirements:

  • The file must be readable from a Java application but also must be editable by the support team without the need of fancy editors
  • Ideally, the file should be in plain text format but XML is OK too
  • Allow DML as well as DDL statements to be stored / retrieved
  • New statements can be added at a later stage (the application is flexible enough to pick them up and execute them)
  • Statements can be grouped (and executed as a group by the application)
  • Statements should allow parameters

Notes:

  • Once retrieved, the statements will executed using Spring’s JDBCTemplate
  • Hibernate or Spring’s IOC container will not be used

So far, I managed to find the following Java libraries, which use external files for storing SQL statements. However, I am mainly interested in the storage rather than a library that hides all JDBC “complexities”.

Sample file content:

<s:query name="get_emp">
  <s:param name="name" type="string"/>
  <s:sql databases="oracle">
    select    *
    from      scott.emp
              join scott.dept on (emp.deptno = dept.deptno)
    where     emp.ename = <s:bind param="name"/>
  </s:sql>
</s:query>

Sample file content:

<sqlMap namespace="Contact"">
    <typeAlias alias="contact"
        type="com.sample.contact.Contact"/">
    <select id="getContact"
        parameterClass="int" resultClass="contact"">
            select CONTACTID as contactId,
                   FIRSTNAME as firstName,
                   LASTNAME as lastName from
                   ADMINISTRATOR.CONTACT where CONTACTID = #id#
    </select>
</sqlMap>
<insert id="insertContact" parameterClass="contact">
INSERT INTO ADMINISTRATOR.CONTACT( CONTACTID,FIRSTNAME,LASTNAME)
        VALUES(#contactId#,#firstName#,#lastName#);
 </insert>
<update id="updateContact" parameterClass="contact">
update ADMINISTRATOR.CONTACT SET
FIRSTNAME=#firstName# ,
LASTNAME=#lastName#
where contactid=#contactId#
</update>
<delete id="deleteContact" parameterClass="int">
DELETE FROM ADMINISTRATOR.CONTACT WHERE CONTACTID=#contactId#
</delete>
  • WEB4J

    -- This is a comment ADD_MESSAGE { INSERT INTO MyMessage -- another comment (LoginName, Body, CreationDate) -- another comment VALUES (?,?,?) }

    -- Example of referring to a constant defined above. FETCH_RECENT_MESSAGES { SELECT LoginName, Body, CreationDate FROM MyMessage ORDER BY Id DESC LIMIT ${num_messages_to_view} }

Can anyone recommend a solution that is tried and tested?

+3  A: 

I would strongly encourage you to use Stored Procedures. This kind of thing is exactly what they're for.

Dave
Yes, I am aware of the Stored Procedure option. Normally, it would be a path I would consider but for this project is not possible
Adrian
I cannot disagree more. Stored procedures make your business logic alienated from Java code. Stored procedures are written in plsql code closely coupled to the DB vendor without proper tools for refactoring, more often no OOP support etc. Use Stored procedures only for performance optimizations.
Boris Pavlović
Useful stored procedures are only written in plsql if you're using Oracle. Generally useful stored procedures are written in SQL and can be easily port to another platform.
Dave
+1 - Putting all our SQL into (Oracle) packages is great. It keeps all the queries in one nice place and allows the DB team play about without worrying about recompiling the application.
mlk
This project will run agains an Ingres database; Ingres supports stored procedures so this is not a problem. But one of the requirements is not to use stored procedures.
Adrian
That's too bad. It's an odd restriction.
Dave
That's absolutely not an odd restriction. With stored procedures, all the business logic is running on the database machine (that won't scale easily because it's a single big machine). With SQL queries, the business logic is coded in Java (and you can benefit from all the language features) and runs on the application server which scale better and for less money than a DB server. To me, not using stored procedures is actually a good restriction.
Pascal Thivent
@Boris Pavlović - I cannot disagree more. :) Most enterprise applications never need to change the DB anyway, especially because this would incur a heavy licence fee penalty for doubtful gains. And if you narrow yourself to a common subset of SQL that works on all RDBMS (or at least the few you are targeting), you will almost always have performance penalties, sometimes severe.
Vilx-
If you do wish to make your application multi-RDBMS compatible you should take the ORM way and build your application from the ground up keeping its strengths and limitations in mind.
Vilx-
+9  A: 

Just create a simple Java Properties file with key-value pairs like this one:

users.select.all = select * from user

Declare a private field of type Properties in your DAO class and inject it using Spring configuration which will read the values from the file.

UPDATE: if you want to support SQL statements in multiple lines use this notation:

users.select.all.0 = select *
users.select.all.1 = from   user
Boris Pavlović
+1 for simple but effective, and will work well with prepared statements (http://java.sun.com/docs/books/tutorial/jdbc/basics/prepared.html)
A properties file is a good option, I agree. It is easy to implement and easy to edit by the support team. However, there are a few points I am not entirely happy the fact that "One statement per line" might mean long statements will make the file difficult to view/edit
Adrian
You should be able to use \ at the end of the line to allow the content to go over multiply lines.
mlk
If your java is sufficiently up-to-date you could use an XML properties file instead of nvp.
martin clayton
The project will use JDK6 and XML is an accepted format
Adrian
Be careful with properties files - the DBA's would likely rebel against having to append a '\' on every line, in order to continue a statement.
John O
I have used a properties file before and it was very simple, the \ to continue a newline is a little annoying, but it's such a simple solution that we lived with it.
delux247
I have used properties stored in XML file for exactly this purpose. It has simple syntax (`properties` element as root, `entry` elements with `key` attribute as children), can be loaded as simply as normal Properties (by using loadFromXML method instead of load method), and it works perfectly. Only one small issue was encoding of <, > characters, which must be written as entities in XML (<, >)
Peter Štibraný
+3  A: 

If you must do this, you should look at the iBatis project, from APache . I haven't used it, but have heard it recommended a number of times.

Separating SQL and Java isn't my favorite approach, since SQL is actually code, and is tightly coupled to the Java code that calls it. Maintaining and debugging the separated code can be challenging.

Absolutely don't used stored procs for this. They should only be used to improve performance by reducing traffic between the DB and the application.

John Stauffer
I'm puzzled by your statement that the Java and SQL are tightly coupled. The only coupling should be the names of the columns in the query.
Dave
iBATIS is one option I am considering; it is a bit complex but it ticks most of the boxes
Adrian
"Separating SQL and Java isn't my favorite approach..."I assume you do not agree with the statements in this article"Keep SQL out of code" http://www.javapractices.com/topic/TopicAction.do?Id=105
Adrian
I would recommend iBatis as well; it usually takes new developer minutes to start using it. (Of course, assuming you already are using it; introducing it to a new project usually takes a bit more work depending on project itself.)
Domchi
Adrian - The article you link to equates SQL code with metadata, which is, I believe, a false characterization.My preferred approach is to isolate all SQL into a set of Java Data Access Object (DAO) classes, which are responsible for translating between the database and the Java object model, and are not allowed to contain any additional logic. Now you *still* have all of the SQL code in a well-defined location (a set of java files), with the added benefit of having all of the appropriate context of where the query is used.I could go on, but I'm out of chars...
John Stauffer
I'm with John - separating queries this way makes it hard to figure out what fields you're setting/reading. You start with PreparedStatement p = new PreparedStatement(readSqlQuery("getContact")). Then what? What arguments should you pass it? What result columns are there? You have to go hunt down the right entry in the sql file. If it's in the code, it is right there for you to look at.
Keith Randall
+1  A: 

A simple solution we have implemented when faced with this was to externalize the SQL/DML into a file (mySql.properties), then use MessageFormat.format(String[] args) to inject dynamic properties into the SQL.

For example: mySql.properties:

select    *
    from      scott.emp
              join scott.dept on (emp.deptno = dept.deptno)
    where     emp.ename = {0}

Utility methods:

public static String format(String template, Object[] args) {
    String cleanedTemplate = replaceSingleQuotes(template);
    MessageFormat mf = new MessageFormat(cleanedTemplate);
    String output = mf.format(args);
    return output;
}
private static String replaceSingleQuotes(String template) {
    String cleaned = template.replace("'", "''");
    return cleaned;
}

Then use it like so:

String sqlString = youStringReaderImpl("/path/to/file");
String parsedSql = format(sqlString, new String[] {"bob"});
Rich Kroll
I presume you want to store more than one statement in the "mySql.properties" file. How do you indentify it?Also, if you want to group several statements, what method would you suggest?
Adrian
We actually store a single properties file per sql/dml statement and use spring to inject a map of the files into the component responsible for loading the files.
Rich Kroll
I considered using MessageFormat when implementing web4j's data layer, but I rejected it. The main reason was the arbitrary maximum of 10 parameters allowed.
John O
A: 

You can use the localization facilities to do this. You then use the name of the database as the locale to get the "oraclish" version of "insert-foo-in-bar" instead of the English or French version.

The translations are usually stored in property files, and there are good tools for localizing applications by allowing editing these property files.

Thorbjørn Ravn Andersen
Can you point me to an example of such file?
Adrian
Have a look at the Java Tutorial on this subject: http://java.sun.com/docs/books/tutorial/i18n/resbundle/propfile.html
Thorbjørn Ravn Andersen
+1  A: 

You can also use the QueryLoader class in Apache Commons DbUtils, which will read the sql from a properties file. However, you will have to use DbUtils which sort of serves the same purpose as the JDBCTemplate.

Ken Liu
A: 

You can use velocity to have "scriptable" sql templates that you can use to work with the files in a flexible way. You have primitive statements like conditionals and loops to build your sql commands.

But I strongly suggest to use prepared statements and/or stored procedures. Building your SQL the way you're planning will make you vulnerable to SQL injection, the DB server will not be able to cache the sql queries (which will lead to bad performance).

BTW: You can store the definition of the prepared statements in files too. This is not the best solution but pretty close to it and you get the benefit of SQL-injection protection and performance.

When your SQL schema is not build to work with prepared statements or stored procedures you might want to rethink your schema. Maybe it needs to be refactored.

Patrick Cornelissen
The application will be built on a legacy database so schema refactoring is out of the question. Also, using stored procedure is not an accepted solution (arguably, they can be a good idea).Caching (fortunately) is not a major issue, every statement is executed once a day (it is a batch application)
Adrian
OK, then I'd choose Velocity. IMHO it's flexible enough to "script" the scripts a little bit and not that flexible that it's a programming language on its own.
Patrick Cornelissen
A: 

You can use Spring and have your sql statements stored in your beans file that are injected when you get the class from your bean factory. That class can also to use an instance of SimpleJDBCTemplate that can be configured via the bean file to help simplify your code.

shane
A: 

It's simple and reliable to do using classes from Spring. Take your SQL files and save them in some location on your classpath. This can be in a JAR file that only contains SQL if you want. Then use Spring's ClassPathResource to load the file into a stream and use Apache IOUtils to convert it into a String. You can then execute the SQL using SimpleJdbcTemplate, or DB code of your choice.

I suggest you create a utility class that takes a simple Java class with public String fields that correspond to the SQL file names following a convention of your choosing. Then use reflection in conjunction with the ClassPathResource class to go find the SQL files conforming to your naming convention and assign them to the String fields. After that just refer to the class fields when you need the SQL. It's simple, works great, and achieves the goal you want. It also uses well worn classes and techniques. Nothing fancy. I did it couple years back. Works great. Too lazy to go get the code. You'll have no time figuring it out yourself.

marcus none