在開發JavaWeb應用程序的過程中可能經常需要使用到SQL語句來訪問數據庫。為了屏蔽SQL注入帶來的危險,在Java中通常使用PreparedStatement,使用預編譯的SQL語句。預編譯的SQL語句是那些包含?的語句,使用PreparedStatement可以讓數據庫預先編譯這些SQL模板,只有調用的時候套用必須的參數即可。

 

SQL文件的存放位置

那么在JavaWeb項目中預編譯的SQL語句到底放在那里呢?

 

放在Java代碼里肯定是不好的,為什么,有兩點,第一,SQL語句放在Java代碼里

太難看可,有不好的味道(參看Refactor),第二,每次SQL語句變更(可能經常發生)

都需要編譯這些Java代碼,比較煩。

 

那么SQL語句到底放在那里呢?根據這么多年的開發經驗SQL文通常可以放在classes目錄

下的文件中,存放SQL語句的文件有三種類型:properties文件,xml文件和txt文件。

 

在詳細討論文件格式之前,我們先討論以下如何在Java類中得到這些文件的引用,以便從中得到需要的SQL語句。

 

使用ClassgetResourceAsStream方法可以獲得對文件引用的InputStream。例如:文件目錄結構:

src

    com

       jpleasure

          dao

                SomeDao.java

                SomeDao.properties

                             SomeDao.xml

                                      SomeDao.sql

// properties

public class SomeDao{

         public InputStream getInputStream() {                                                                                          

this.getClass().getClassLoader().

getResourceAsStream("com/jpleasure/dao/SomeDao.properties");

         }

}

 

// xml

public class SomeDao{

         public InputStream getInputStream() {

                  this.getClass().getClassLoader().

getResourceAsStream("com/jpleasure/dao/SomeDao.properties");

         }

}

 

// txt(.sql)

public class SomeDao{

         public InputStream getInputStream() {

                   this.getClass().getClassLoader().

getResourceAsStream("com/jpleasure/dao/SomeDao.properties");

         }

}

 

Propertis文件

Properties 文件是Java支持的標準的屬性文件(相當于Windowsini文件的支持)。

 

Properties 文件的格式為:

 

# 注釋

# 定義key,并且與key對應的值為value

key = value

 

可以使用java.util.Properties類來包裝properties文件,使用如下

Properties props = new Properties();

try {

         props.load(this.getInputStream());

} catch(IOException ioex) {

         // 文件不存在,或者格式問題等

}

String value = props.getString("key")

 

注意上述格式中value不能換行,要想換行必須使用轉義字符“\”。

 

在存儲SQL的時候我們使用如下的格式,例如:

 

# 某業務,某操作SQL

xxx_0001 = select * from dual

 

# 某業務,其他操作SQL

xxx_0002 =  \

select      \

         *    \

from       \

         dual

 

使用Properties文件的優點是:Java內置支持布需要手寫文件解析代碼,另外使用也非常簡單。

缺點是:SQL語句不能直接編寫,需要追加轉義字符"\",無法將這樣的SQL語句直接拷貝到數據庫客戶端中運行。

 

XML文件

使用XML文件保存SQL比較常用的格式為:

<sqls>

         <sql id=”xxx_0001”>

                   Select

*

from   

dual   <!-- 文件注釋  -->

</sql>

         <sql id=”xxx_0001”>

                  

</sql>

        

</sqls>

 

使用XML格式的文件保存SQL需要自己寫文件解析代碼,由于JavaXML提供了內置的支持,并且第三方的開源庫也很多,并且非常容易使用所以從xml文件中解析SQL語句也沒有什么困難。以下以jdom為例講解如何解析上述的XML格式的SQL文件。

 

protected Map analysis() {

 

                   Map map = new HashMap();

 

                   DOMBuilder builder = new DOMBuilder();

                   Document doc = null;

                   try {

                            // 解析XML文件

                            doc = builder.build(this.getInputStream());

 

                            // 獲得根節點: <sqls>

                            Element element = doc.getRootElement();

 

                            // 獲得所有根節點的子節點: <sql>節點列表

                            List sqlNodeList = element.getChildren("sql");

                            for (int i = 0; i < sqlNodeList.size(); i++) {

                                     Element sqlNode = (Element) sqlNodeList.get(i);

                                     // 獲得SQL語句ID

                                     String id = sqlNode.getAttribute("id").getValue();

                                     // 獲得SQL語句內容

                                     String sql = sqlNode.getTextTrim();

                                     map.put(id, sql);

                            }

                   } catch (JDOMException e) {

                            e.printStackTrace();

                   }

                   return map;

         }

 

使用XML文件格式保存SQL語句據的時候需要注意,SQL語句中的大于(>)小于(<)XML文件的格式沖突,有兩種解決方法,

第一:使用全角的大于()小于()號。

第二:使用<![CDATA[    ]]>來包圍所有的內容。

 

另外,通常情況下(不使用<![CDATA[    ]]>的時候)SQL的注釋只能使用XML的注釋格式(<!-- -->)。

 

使用XML格式的文件的有點:SQL語句可以正常書寫,文件解析相對簡單。

缺點:大于號,小于號的沖突;無法添加SQL注釋(通常只能使用XML格式的注釋)

 

TXT文件(以.SQL為后綴)

我們先說一下TXT文件(.sql文件)的格式

 

------------------------------------

--@ SQL-1

------------------------------------

SELECT

    SYSDATE

FROM

    DUAL(SQL)

   

------------------------------------

--@ SQL-2

-- 某某用途的SQL

------------------------------------

SELECT

    SYSDATE, ROWID   -- 某某字段

FROM

    DUAL(SQL)         -- 某某表

 

使用txt格式的文件,非常的簡單,和一般的寫SQL語句一樣,可以使用任何的SQL標準語法。只是有一個地方需要注意,就是每個SQL語句的頭注釋的地方加上一行特殊的內容用來標記SQL語句的ID

--@ SQL-1

 

這樣的文件解析比較困難,但是也不是不能做,解析代碼如下:

         protected Map analysis() {

 

                   Map sqlMap = new HashMap();

 

                   String id = null;

                   StringBuffer sql = new StringBuffer();

 

                   if (this.getInputStream() != null) {

                            BufferedReader sqlFileReader = null;

                            try {

                                     sqlFileReader = new BufferedReader(

                                                        new InputStreamReader(this.getInputStream()));

                                     String currentLine = null;

 

                                     // 逐行讀取文件內容

                                     while ((currentLine = sqlFileReader.readLine()) != null) {

 

                                               // 發現新的SQL語句,將已經發現的SQL語句放在SQL容器中

                                               if (currentLine.startsWith("--@")) {

                                                        if (id != null) {

                                                                 sqlMap.put(id, sql.toString());

                                                                 id = null;

                                                                 sql = new StringBuffer();

                                                        }

                                                        id = currentLine.substring("--@".length()).trim();

                                               } else if (currentLine.startsWith("--")) {

                                                        // 不讀取注視行

                                                        continue;

                                               } else {

                                                        // 非注釋,ID

 

                                                        // 去掉SQL語句行的末尾注視

                                                        if (currentLine.length() > 0) {

                                                                 int commentsIndex = currentLine.indexOf("--");

                                                                 if (commentsIndex > 0) {

                                                                           currentLine = currentLine.substring(0,

                                                                                             commentsIndex);

                                                                 }

                                                                 // 將換行符替換為空格

                                                                 sql.append(currentLine + " ");

                                                        } else {

                                                                 // 將換行符替換為空格

                                                                 sql.append(" ");

                                                        }

 

                                               }

                                               currentLine = null;

                                     }

                                     if (id != null) {

                                               sqlMap.put(id, sql.toString());

                                     }

 

                            } catch (IOException ioex) {

                                     ioex.printStackTrace();

                            } finally {

                                     if(sqlFileReader != null) {

                                               try {

                                                        sqlFileReader.close();

                                               } catch (IOException e) {

                                                       

                                               }

                                     }

                            }

                   }

                   return sqlMap;

         }

 

使用TXT格式的文件的有點:SQL語句書寫非常方便,可以拷貝出來直接運行,可以使用標準的SQL注釋格式。

缺點:每個SQL語句必須添加特殊的ID標志(相對與Propertiesxml來說可能也不算是缺點),解析困難,需要較強的文本接卸的能力。

 

通用SQL文件讀取庫

如何設計一個同時支持三種文件格式的SQL文件讀取庫?

 

 

首先我們抽象出一個SqlManager的接口,它提供通過Key獲得對應SQL語句的操作。代碼為:

public interface SqlManager {

         /**

          * 獲取指定KeySQL語句

          *

          * @param key

          *            SQL語句的Key

          * @return SQL語句內容

          * @throws SqlMgntException

          *             無法取得SQL語句時拋出此異常。

          */

         public String getSql(String key) throws SqlMgntException;

}

 

然后使用一個抽象基類,來定義通用的屬性和操作,代碼為:

public abstract class AbstractSqlManager implements SqlManager {

 

         /* SQL文件輸入流 */

         private final InputStream is;

 

         /**

          * SQL文件輸入流獲取方法。

          *

          * @return SQL文件輸入流

          */

         public InputStream getInputStream() {

                   return is;

         }

 

         /**

          * 默認構造方法

          *

          * @param is

          *            SQL文件輸入流

          */

         public AbstractSqlManager(InputStream is) {

                   this.is = is;

         }

 

         /* SQL語句容器 */

         private Map sqlContainer = null;

 

         /* 同步KEY */

         private final Object syncKey = new Object();

 

         /**

          * 通過SQL語句ID獲取SQL語句內容。

          *

          * @param key

          *            SQL語句ID

          * @return SQL語句

          */

         public String getSql(final String key) throws SqlMgntException {

                   synchronized (syncKey) {

                            if (sqlContainer == null) {

                                     this.sqlContainer = initContainer();

                            }

                   }

                   return (String) sqlContainer.get(key);

         }

        

         /**

          * 抽象的模板方法,用來解析不同類型的SQL文件。

          * @return 配對的SQL內容

          */

         protected abstract Map initContainer();

 

}

 

注意其中的抽象方法:

protected abstract Map initContainer();

通過這個抽象方法把SQL文件的解析交給了具體的實例(模式參看:模板方法)

 

三個具體的SqlManager類我們不必具體說了,都是對上述抽線的解析方法的實現。

 

工廠類可以根據對應文件的后綴明創建對應的SqlManager。代碼為:

public class SqlManagerFactory {

 

         /* XMl文件類型 */

         public static final String TYPE_XML = ".xml";

 

         /* SQL文件類型 */

         public static final String TYPE_SQL = ".sql";

 

         /* 屬性文件類型 */

         public static final String TYPE_PROP = ".properties";

 

         // begin ma.zhao@dl.cn 2006/03/09

         // add singleton map for SqlManager

 

         private static Map sqlManagerContainer = new HashMap();

 

         // end ma.zhao@dl.cn 2006/03/09

 

         /**

          * 創建對應的SqlManager實現類

          *

          * @param filePath

          *            SQL文件相對路徑和文件名

          * @return

          */

         public static SqlManager createSqlManager(String filePath) throws SqlMgntException {

                   SqlManager manager = null;

                   synchronized (sqlManagerContainer) {

 

                            manager = (SqlManager) sqlManagerContainer.get(filePath);

                           

                            // 如果SqlManager不存在,則根據文件初始化SqlManager,

                            // 并且將它放在SqlManager容器中

                            if (manager == null) {

                                    

                                     InputStream is = SqlManagerFactory.class.getClassLoader()

                                                        .getResourceAsStream(filePath);

                                    

                                     if(is == null) {

                                               throw new SqlMgntException(

"Sql File Not Fount! Input file name path is:" + filePath);

                                     }

                                    

                                     if (filePath.endsWith(TYPE_XML)

                                                        || filePath.endsWith(TYPE_XML.toUpperCase())) {

                                               manager = new SqlManagerXmlImpl(is);

                                     } else if (filePath.endsWith(TYPE_SQL)

                                                        || filePath.endsWith(TYPE_SQL.toUpperCase())) {

                                               manager = new SqlManagerSqlImpl(is);

                                     } else if (filePath.endsWith(TYPE_PROP)

                                                        || filePath.endsWith(TYPE_PROP.toUpperCase())) {

                                               manager = new SqlManagerPropImpl(is);

                                     } else {

                                               throw new SqlMgntException(

"Sql File Type Not Support, Input file path is:" + filePath);

                                     }

                                    

                                     //

                                     if (manager != null) {

                                               sqlManagerContainer.put(filePath, manager);

                                     }

                            }

                   }

                   return manager;

         }

}

注意上述代碼可以控制每個對應的SQL文件始終只有一個對應的SqlManager,不會對同一個文件創建多個SqlManager類。

 

所有上述過程中的異常都以SqlMgntException的方式向上拋出,代碼為:

public class SqlMgntException extends Exception {

 

         /**

          *

          */

         private static final long serialVersionUID = 1L;

 

         public SqlMgntException() {

                   super();

                   // TODO Auto-generated constructor stub

         }

 

         public SqlMgntException(String message, Throwable cause) {

                   super(message, cause);

                   // TODO Auto-generated constructor stub

         }

 

         public SqlMgntException(String message) {

                   super(message);

                   // TODO Auto-generated constructor stub

         }

 

         public SqlMgntException(Throwable cause) {

                   super(cause);

                   // TODO Auto-generated constructor stub

         }

}

 

優化的方向,

1 初始話加載所有的SQL語句

         程序運行之初,將所有的SQL語句裝載在內存中。



ExtJS教程- Hibernate教程-Struts2 教程-Lucene教程