48

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”.

  • Axamol SQL Library

    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>
    
  • iBATIS

    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?

Tapas Bose
  • 28,796
  • 74
  • 215
  • 331
Adrian
  • 6,013
  • 10
  • 47
  • 68
  • 4
    Are you sure you want to centralise all your SQL into a file that the support team can edit? How badly could they break things? – martin clayton Oct 09 '09 at 15:17
  • 1
    Once deployed, the application is unlikely to change unless major bugs are found. The support team will not have the expertise to tinker with a complex Java applications by they know how to tinker with SQL statements. Usually they will need to do DBA type changes rather than actual application changes – Adrian Oct 09 '09 at 15:25
  • WEB4J example is not properly formatted, could somebody with edit privileges fix that? – Juha Syrjälä Oct 24 '09 at 10:19
  • 4
    Thinking that the Java code won't change is an illusion. If you change your queries, you'll likely have to change the code that is using the results. – Pascal Thivent Oct 29 '09 at 22:39
  • [SQLind](http://www.sqlind.net) provides a xml templated way to externalize SQL from java code. –  Oct 18 '11 at 08:47
  • an alternative solution using xml: http://stackoverflow.com/q/25556825/217324 – Nathan Hughes Feb 24 '16 at 14:32
  • I hate these types of close reasons. I prefer to store in `.sql` resources in the same package then I have a helper to remove comments from the file and set a string. `INSERT_WIDGET = getSql(WidgetService.class.getResourceAsStream("widget_insert.sql"));`. The benefit of this is that because the SQL is in an SQL file you get syntax highlighting and ability to simply execute the script in your SQL editor. I do also use another bean called a `QueryCache` with a method `String get(Class, String)` that will allow me to share queries across classes without duplicating the string. – Brett Ryan May 26 '16 at 06:15
  • This is sometimes discussed as support for putting most of your SQL in stored procedures that the application executes. DBAs can still maintain the stored proc without having to update the application. I'd like to know how you protect your queries to prevent injection attacks since you have to interpret the SQL after inputting from a file. Are you doing anything to validate the text? Also, my concern would be that this approach can make the app more fragile because all parameters of a prepared statement need mapped (by type and position) somehow by whatever code executes the statement. – Kelly S. French Apr 26 '17 at 14:34

12 Answers12

61

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ć
  • 63,078
  • 28
  • 122
  • 148
  • 2
    +1 for simple but effective, and will work well with prepared statements (http://java.sun.com/docs/books/tutorial/jdbc/basics/prepared.html) – user86614 Oct 09 '09 at 15:14
  • 3
    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 Oct 09 '09 at 15:16
  • 31
    You should be able to use \ at the end of the line to allow the content to go over multiply lines. – Michael Lloyd Lee mlk Oct 09 '09 at 15:27
  • If your java is sufficiently up-to-date you could use an XML properties file instead of nvp. – martin clayton Oct 09 '09 at 15:32
  • The project will use JDK6 and XML is an accepted format – Adrian Oct 09 '09 at 16:05
  • 1
    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 Oct 26 '09 at 23:01
  • 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 Oct 27 '09 at 04:46
  • 7
    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ý Nov 02 '09 at 14:11
  • 6
    @PeterŠtibraný You can put the entire entry in CDATA elements, so you don't have to bother with XML entities – Dag Oct 17 '11 at 09:11
  • @PeterŠtibraný Could you show an example of your xml file. Please – John Alexander Betts Sep 17 '13 at 13:40
  • @JohnB: I have since switched to a different file format, but my XML properties file looked like this: https://gist.github.com/pstibrany/c01fa0643121f3ffa26b – Peter Štibraný Sep 17 '13 at 14:02
  • @PeterŠtibraný If the different file format is better could you tell me about that? – John Alexander Betts Sep 17 '13 at 14:03
  • @JohnB: I am simply using text file like this: https://gist.github.com/pstibrany/38aadfec22dfd72d52f2. Nothing interesting here, `/*$* */` marks the beginning of the query, `/*$ */` marks the end, and everything between is a query itself. It's just simpler than XML. One of the reasons is also that once you parse Properties (e.g. from XML), you can't get them in the same order as they were in the file, which is what we require to execute Database Upgrade queries in the right order. – Peter Štibraný Sep 18 '13 at 07:57
  • only for simple queries, when you have few sql queries, if you have complex queries that are constantly changing, with all the problems of readability, merging into other branches and code highlighting things, you really get frusterated by this solution. – Shilan Sep 09 '16 at 11:41
  • 1
    @Shilan, why? SQL editing usually separated from code, I mean you test queries improve etc and then add to code, add to file push to git. Looks like good solution. Anyway, code looks clearer without huge queries :) – Sonique Jun 23 '17 at 04:44
  • 1
    @siphiuel's answer is way more elegant. Instead of properties files uses yaml files. Supports preparedStatements and multiple queries per file but being yaml allows for copy-paste of SQL code without having to manage each line as a single property. https://stackoverflow.com/a/30733697/67945 – Frankie Jun 11 '20 at 17:41
  • 1
    I'm sorry but lmao... that update makes me cringe if ever I see a project like that. – html_programmer Aug 27 '21 at 12:10
  • 1
    @html_programmer there are tons of obsolete projects that could benefit from improvements like this one – Boris Pavlović Aug 27 '21 at 12:12
10

Pasting here my answer to Clean way to externalize long (+20 lines sql) when using spring jdbc?:

I faced the same issue some time ago, and came up with YAML. It supports multi-line string property values, so you can write something like this in your query files:

selectSomething: >
  SELECT column1, column2 FROM SOMETHING

insertSomething: >
  INSERT INTO SOMETHING(column1, column2)
  VALUES(1, '1')

Here, selectSomething and insertSomething are query names. So it's really convenient and contains very few special characters. Queries are separated by blank lines, and each query text must be indented. Note that queries can absolutely contain the indentation of their own, so that the following is perfectly valid:

anotherSelect: <
  SELECT column1 FROM SOMETHING
  WHERE column2 IN (
    SELECT * FROM SOMETHING_ELSE
  )

You can then read the contents of the file into a hash-map with the help of SnakeYAML library, using the code below:

import org.apache.commons.io.FilenameUtils;
import org.apache.commons.io.FileUtils;
import java.io.FileReader;

import org.yaml.snakeyaml.Yaml;
import java.io.File;
import java.io.FileNotFoundException;

public class SQLReader {
  private Map<String, Map> sqlQueries = new HashMap<String, Map>();

  private SQLReader() {
    try {
      final File sqlYmlDir = new File("dir_with_yml_files");
      Collection<File> ymlFiles = FileUtils.listFiles(sqlYmlDir, new String[]{"yml"}, false);
      for (File f : ymlFiles) {
        final String fileName = FilenameUtils.getBaseName(f.getName());
        Map ymlQueries = (Map)new Yaml().load(new FileReader(f));
        sqlQueries.put(fileName, ymlQueries);
      }
    }
    catch (FileNotFoundException ex) {
      System.out.println("File not found!!!");
    }
  }
}

In the example above a map of maps is created, mapping each YAML file to a map containing query names/strings.

Community
  • 1
  • 1
siphiuel
  • 3,480
  • 4
  • 31
  • 34
  • If you're not going to use a full fledged library this answer is spot-on. Very clean and elegant. – Frankie Jun 11 '20 at 17:42
8

If you must do this, you should look at the MyBatis project. 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.

artbristol
  • 32,010
  • 5
  • 70
  • 103
John Stauffer
  • 16,150
  • 10
  • 40
  • 35
  • 5
    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 Oct 09 '09 at 15:19
  • iBATIS is one option I am considering; it is a bit complex but it ticks most of the boxes – Adrian Oct 09 '09 at 15:22
  • 1
    "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 Oct 09 '09 at 15:52
  • 1
    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 Oct 09 '09 at 15:54
  • 2
    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 Oct 09 '09 at 19:24
  • 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 Oct 27 '09 at 23:59
7

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
  • 3,995
  • 3
  • 23
  • 28
  • 2
    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 Oct 09 '09 at 15:46
  • 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 Oct 10 '09 at 02:28
5

The ElSql library provides this functionality.

ElSql consists of a small jar file (six public classes) that allows an external SQL file (elsql) to be loaded. The file uses a simple format to optionally provide slightly more behaviour than simply loading a file:

-- an example comment
@NAME(SelectBlogs)
  @PAGING(:paging_offset,:paging_fetch)
    SELECT @INCLUDE(CommonFields)
    FROM blogs
    WHERE id = :id
      @AND(:date)
        date > :date
      @AND(:active)
        active = :active
    ORDER BY title, author
@NAME(CommonFields)
  title, author, content

// Java code:
bundle.getSql("SelectBlogs", searchArgs);

The file is broken up into @NAME blocks which can be referred to from code. Each block is defined by significant whitespace indentation. @PAGING will insert the necessary code for paging such as FETCH/OFFSET. @AND will only be output if the specified variable exists (helping build dynamic searches). The DSL also handles LIKE vs = for wildcards in searches. The goal of the optional DSL tags is to provide the common basics that often hit when trying to build dynamic SQL in a database-neutral way.

More info on the blog or user guide.

JodaStephen
  • 60,927
  • 15
  • 95
  • 117
5

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
  • 22,503
  • 19
  • 75
  • 98
3

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.

3

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.

1

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
  • 73,784
  • 33
  • 194
  • 347
1

dynamic-query is a good open source framework for those want something between JDBC and ORM.

1 plain SQL. - It saves plain sql to external files. no redundant tags, supports comments.

/* It also supports comment.
This code is in an external file 'sample.sql', Not inisde java code.*/
listUsers : select * from user_table
where user_id= $$;  /* $$ will automatically catch a parameter userId */


2 expandable SQL. -It supports parameters, including other files and sub query.

listUsers:
select
    id, amount, created
    @checkEmail{ ,email } 
from user_table
where amount > $amt and balance < $amt
    @checkDate { and created = $$ }
    @checkEmail{ and email in (
        select email from vip_list ) } ;        
/* Above query can be four queries like below.
1. listUsers
2. listUsers.checkDate 
3. listUsers.checkEmail
4. listUsers.checkDate.checkEmail 
*/



-- It can include other files like below
& ../hr/additional hr.sql ; 
& ../fi/additional fi.sql ;


Java example code using above. setting values to db.

QueryUtil qu = qm.createQueryUtil("selectAll");
try {
    qu.setConnection(conn);

    // with native jdbc
    qu.setString("alpha");
    qu.setDouble(10.1);
    qu.executeQuery();

    // or with bean
    qu.executeQuery(new User("alpha", 10.1));

    // or with map
    Map<String, Object> map=new HashMap<String, Object>();
    map.put("userName", "alpha");
    map.put("amt", 10.1);
    qu.executeQuery(map);

    // or with array
    qu.executeQueryParameters("alpha", 10.1);

Java example code using above. getting values from db.

    while (qu.next()) // == qu.rs.next()
    {
        // native jdbc
        String usreName = qu.getString("user_name"); 
        double amt = qu.getDouble("amt");

        // or bean
        User user = new User();
        qu.updateBean(user);

        // or array
        Object[] values = qu.populateArray();
    }
} catch (Exception e) {
    e.printStackTrace();
} finally {
    qu.closeJust();
}
0

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

Dave
  • 4,546
  • 2
  • 38
  • 59
  • 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 Oct 09 '09 at 15:03
  • 34
    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ć Oct 09 '09 at 15:05
  • 2
    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 Oct 09 '09 at 15:09
  • 3
    +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. – Michael Lloyd Lee mlk Oct 09 '09 at 15:29
  • 1
    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 Oct 09 '09 at 15:37
  • That's too bad. It's an odd restriction. – Dave Oct 09 '09 at 15:41
  • 3
    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 Oct 30 '09 at 00:10
  • 1
    @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- Aug 09 '10 at 19:03
  • 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- Aug 09 '10 at 19:04
0

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
  • 7,968
  • 6
  • 48
  • 70
  • 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 Oct 24 '09 at 10:21
  • 1
    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 Oct 26 '09 at 07:49