21

I want to externalize some large queries in my application to properties\sql\xml files. However I was wondering if anyone has some recommendations as to how achieve this in a clean fashion. Most results recommend using an ORM framework but this isn't applicable due to some data constraints.

I took a look at: Java - Storing SQL statements in an external file but doing this propertyname .1, .2 etc for a few queries that are each longer that 20 lines does not seem that clean.

Community
  • 1
  • 1
Jack Dans
  • 241
  • 1
  • 2
  • 5
  • 1
    What will you gain by externalizing the query? Now instead of seeing which query you're executing while reading or debugging the code, you'll have to find the file where the query is defined. – JB Nizet Mar 15 '13 at 12:55
  • 1
    Plus externalized queries are quite difficult to use when the queries are built dynamically. For instance, if you build the where clause dynamically based on certain conditions, or if you select the order of the elements based on how the user wants them sorted, or you restrict the number of columns based on different configurations or needs to display data. So, externalizing queries has never been a good idea in my experience. – Edwin Dalorzo Mar 15 '13 at 12:58
  • JB Nizet, I will gain nothing from it. But those are the constraints I have to work within. – Jack Dans Mar 15 '13 at 13:00
  • How about building a wrapper over the Spring JDBC template and as you execute the queries they can be saved in a file. Eventually the file will contain all queries and you can use the file contents to externalize them. – Edwin Dalorzo Mar 15 '13 at 13:07
  • And why don't you discuss the situation with the guy imposing such a constraint, if you don't gain anything from it? If the guy imposes this, he should have a good reason to do so, and should explain how he wants the SQL query to be externalized. – JB Nizet Mar 15 '13 at 13:23
  • 3
    I was looking for the same solution, but I can state a gain. It is a pain to write and test a query in an SQL editor and then take it to Java and have to wrap it as a String, just to have to unwrap and reindent it for debugging later. I understand that as the code matures, or if it is a dynamically built query the externalization is less useful, but I do think that having the query outside of the Java code makes it cleaner. – lpacheco Mar 06 '15 at 17:21

5 Answers5

35

You can put your queries in a xml file

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">

<properties>
<entry key="getPersonById">
    <![CDATA[
        Select Name From Person 
        Where Id =?     
    ]]>

</entry>    
<entry key="getPersonBySSN">
    <![CDATA[

    ]]>
</entry>

</properties>

In Spring application Context, load this xml file

<bean id="queryProps" class="org.springframework.beans.factory.config.PropertiesFactoryBean">
    <property name="locations" value="classpath:/queries.xml" />
</bean>

Inject this bean in your DAO class

<bean id="myDAO" class="com.xyz.dao.MyDAOImpl">
  <property name="queryProps" ref="queryProps" />
</bean>

Define queryProps in your DAO class and don't forget to have setter method for this

 private Properties queryProps;

Now you can access the query in your DAO like this -

 String query = queryProps.getProperty("getPersonById");

Hope this helps.

Pankaj
  • 3,512
  • 16
  • 49
  • 83
  • This solution is very helpful in cases where DBA applying his/her updates then save it back to the file. Of course, you can employ some sort of change listener for new updates and have the XML property files deployed outside of `WEB-INF` and another property file visible to your application that reads a location of where the XML property file is located. – Malvon Mar 30 '14 at 21:01
  • 1
    It worked without CDATA for me. – Arun Chandrasekaran Jun 10 '14 at 12:33
  • That's cool and solves the problem, but I would rather have a .sql file per query and having them stored together with the Java code that will run them. And I think it would be nicer to have the query file loaded from the Java code, avoiding the Spring configuration in XML files. Is that possible? – lpacheco Mar 06 '15 at 17:24
  • Hello, Do you know how to do exactly the same with java Config (with springboot annotations) ? I have tried but I don't know much about Spring.. I have directly started my learning by springboot.. – John Sep 07 '17 at 14:59
  • This example is perfect. I just one thing. How can you resolve the table name using a table name resolver (Custom java Util class that returns the full table name with schema when shortname of table is passed as input.)? – RV. Jan 07 '19 at 23:35
  • YAML is a cleaner solution IMO. See @siphiuel's answer. XML is really has always been a terrible, ugly thing and it should be deprecated. – splashout Apr 23 '20 at 20:00
14

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.

siphiuel
  • 3,480
  • 4
  • 31
  • 34
  • A clean solution. You can use this with a Spring NamedParameterJdbcTemplate, to make code more readable. It allows you do use named parameters like Hibernate (e.g., where dt >= :startDt) – splashout Apr 23 '20 at 20:05
  • what would be the proper string for "dir_with_yml_files" if my yml files are located in "/resources/queries"? – Anthony Pham Jun 06 '21 at 14:00
6

This is in addition to what Pankaj has answered. This one doesn't have CDATA in the Properties XML and uses autowiring. I had to add this as answer, as I cannot format code if I had to do this in comment section.

Make sure you have following namespace in spring application context xml file.

xmlns:util="http://www.springframework.org/schema/util

Add the following bean to spring application context xml

<util:properties id="sqls" location="classpath:oracle/sqls.xml" />

Contents of the file sqls.xml is

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">
<properties>
    <comment>Employee Queries</comment>
    <entry key="employee.insert">
        INSERT
        INTO EMPLOYEE
          (
            ID,
            NAME,
            AGE,
            DEPARTMENT
          )
        VALUES
          (
            EMPLOYEE_SEQ.NEXTVAL,
            ?,
            ?,
            ?
          )
    </entry>
</properties>

Autowired the properties as below

@Autowired
@Qualifier("sqls")
private Properties sqls;

Code to get the sql query from Properties

String sql = sqls.getProperty("employee.insert");
Community
  • 1
  • 1
Arun Chandrasekaran
  • 2,370
  • 2
  • 23
  • 33
  • I think you will need the `<![CDATA[` tag **IF** your SQL contains something comparison keyword like `<`, `>` which are reserved chars in xml. Without enclosed inside `CDATA`, Spring would throw exception when load the application context. – LeOn - Han Li Apr 06 '16 at 19:27
  • You can use > for > and < for <. – Arun Chandrasekaran Apr 07 '16 at 14:02
  • 4
    Yes, that kind of defeat the purpose of this IMHO. One benefit of externalize this way is we can clear see the query and also we can copy the sql directly and paste into a sql tool and run. MY 2 CENTS. – LeOn - Han Li Apr 07 '16 at 14:30
  • 1
    just to add one more point, it is also necessary to mention the same in schemaLocation (@ArunBC maybe you could edit your answer) `xmlns:util="http://www.springframework.org/schema/util" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-3.1.xsd">` – Dhruvil Thaker Jun 01 '17 at 09:37
1

You can do multiline queries in a properties file by putting a \ at the end of the line. For example

queries.myquery = select \
foo, bar \
from mytable \
where baz > 10
Adam
  • 490
  • 2
  • 8
1

Take a loot at JdbcTestUtils and the methods "executeSqlScript" and "readScript".

LoganMzz
  • 1,597
  • 3
  • 18
  • 31
  • 1
    FAI (for anyone's information); as of Spring 4.0.3 all these methods (`executeSqlScript`, `readScript`, `containsSqlScriptDelimiters`, `splitSqlScript`) were marked as deprecated; as of Spring 5.0 they all have been either moved to [`ScriptUtils`](https://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/jdbc/datasource/init/ScriptUtils.html) or replaced by features provided by [`ResourceDatabasePopulator`](https://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/jdbc/datasource/init/ResourceDatabasePopulator.html). – maxxyme Feb 12 '20 at 10:03