1.導讀
- 什么是DSL?領域特定語言(Domain Specific language)通常被定義為一種特別針對某類特殊問題的計算機語言,它不打算解決其領域外的問題。了解更多
2.你使用JDBC來
存取
數據時,怎么處理你的SQL
2.1 對于一個固定條件的查詢,我們會使用PreparedStatement來實現。就像下面這個例子,只需要DateOfBirth一個固定條件來查詢。
PreparedStatement statement = null;
try {
Connection connection = getConnection();
statement = connection.prepareStatement(
"SELECT Name" +
" FROM Students" +
" WHERE DateOfBirth < ?");
statement.setDate(1, new java.sql.Date(new java.util.Date().getTime()));
ResultSet rs = statement.executeQuery();
while (rs.next()) {
System.out.print(rs.getString(1));
}
} catch (SQLException e) {
e.printStackTrace();
}
2.2 你遇到過這樣的問題么?
- 你使用JDBC來實現數據存取,如果你要實現一個復雜條件的查詢,而且條件數目還不一定,這時候就很難使用PreparedStatement來
解決了,因為你的SQL模板不是固定的。就像上面的這個例子,如果用戶可能要使用DateOfBirth或者Name作為條件查詢,或者還有更多的條件。
2.3 這個問題可以怎么解決呢?
你當然可以使用簡單的字符串拼接,根據不同的條件拼接成不同的SQL。就像以下代碼
int id = 0;
String name = "Heis";
String gender = "male";
String sql = "select Name from Students where id=" + id;
if (name != null) {
sql += " and name='" + name + "' ";
}
if (gender != null) {
sql += " and gender='" + gender + "' ";
}
System.out.println(sql);
輸出:
select Name from Students where id=0 and name='Heis' and gender='male'
這樣處理的缺點是很明顯的。首先,敏感字符沒有過濾,容易被
注入攻擊
;其次,代碼不容易讀;第三,出于debug的需要,我希望可以保留SQL模板作日志記錄,而不是完整的SQL,就是希望用問號?代替真實的數據。
3. 我的解決方案
我同樣在項目中遇到這樣的問題,所以借助DSL的思想對SQL做了一些封裝。把SQL實現為java版的DSL,這樣不但不會失去SQL的簡單易懂的特性,而且本來SQL就是一門DSL,實現起來不會太困難。
我實現的QuerySQL:
int id = 0;
String name = "Heis";
String gender = "male";
QuerySQL sql = new QuerySQL();
sql.select("name")
.from("Students")
.where("id=?", new Integer(id));
if (name != null) {
sql.and("name='?'",name);
}
if (gender != null) {
sql.and("gender='?'",gender);
}
System.out.println(sql.toPreparedString());
System.out.println(sql.toString());
輸出:
select name from Students where id=? and name='?' and gender='?'
select name from Students where id=0 and name='Heis' and gender='male'
4. QuerySQL是怎么實現的
其實實現的原理也很簡單,就是在QuerySQL的內部準備兩個StringBuffer,一個用來拼接SQL模板,另一個是拼接SQL;而對于API的設計,只要在完成拼接后,返回實例本身即可。
QuerySQL實現的片段:
public class QuerySQL extends SQL {
public QuerySQL() {
buffer = new StringBuffer(100);
preBuffer = new StringBuffer(90);
}
public QuerySQL select(String value) {
buffer.append(SELECT);
preBuffer.append(SELECT);
append(value);
return this;
}
public QuerySQL and(String pattern, Object value) {
String str = format(pattern, value);
buffer.append(WS).append(AND).append(WS).append(str);
preBuffer.append(WS).append(AND).append(WS).append(pattern);
return this;
}
//format 會過濾掉value的敏感字符
protected String format(String pattern, Object value) {
if (value instanceof String) {
String val = (String) value;
val = SymbolUtils.filterSensitiveSQLSymbol(val);
return StringUtils.replaceFirst(pattern, CHAR_FOR_REPLACE, val);
} else if (value instanceof java.sql.Date) {
Date date = DateUtils.convertToDate((java.sql.Date) value);
return StringUtils.replaceFirst(pattern, CHAR_FOR_REPLACE,
DateUtils.formatDate(date));
} else if(value instanceof Date){
return StringUtils.replaceFirst(pattern, CHAR_FOR_REPLACE,
DateUtils.formatDate(value));
}else {
return StringUtils.replaceFirst(pattern, CHAR_FOR_REPLACE, value
.toString());
}
}

}
5. 關于Insert語句
對于Insert語句,如果插入的數據非常多,涉及很多個column,insert語句就顯得不是那么直觀了。你甚至要數著第幾個column是什么類型,要插入相應的數據類型。
statement = connection.prepareStatement("insert into students(id,name,gender) values(?,?,?,
?)");
statement.setInt(1, id);
statement.setString(2, value2);
statement.setString(3, value3);

statement.setString(n, valueN);
經過我封裝的InsertSQL類
InsertSQL sql=new InsertSQL();
sql.insertInto("students")
.value("id", new Integer(id))
.value("name", name)
.value("gender",gender);
System.out.println(sql.toPreparedString());
System.out.println(sql.toString());
輸出:
insert into students (id,name,gender) values(?,?,?)
insert into students (id,name,gender) values('0','Heis','male')
6. 后記
如果你對于這個實現感興趣,可以下載源代碼來看。但是我不推薦你在項目中使用,因為這個實現并不完整,很多地方還欠考慮,而且我還在不斷地修改。寫這篇文章的目的是希望作為一個導讀,讓更多人可以來探討DSL,多交流java實現的DSL。
點擊下載源代碼
7. 延伸閱讀
7.1 JEQUEL(Java Embedded QUEry Language)
描述:比較完整的一個開源的SQL/DSL實現
官方主頁:http://www.jequel.de/index.php
官方示例:
public void testSimpleSql() {
final SqlString sql =
select(ARTICLE.OID)
.from(ARTICLE, ARTICLE_COLOR)
.where(ARTICLE.OID.eq(ARTICLE_COLOR.ARTICLE_OID)
.and(ARTICLE.ARTICLE_NO.is_not(NULL)));
assertEquals("select ARTICLE.OID" +
" from ARTICLE, ARTICLE_COLOR" +
" where ARTICLE.OID = ARTICLE_COLOR.ARTICLE_OID" +
" and ARTICLE.ARTICLE_NO is not NULL", sql.toString());
}
7.2 Quaere
描述:一個類似LINQ的java實現
官方主頁:http://quaere.codehaus.org/
官方示例:
Integer[] numbers={5, 4, 1, 3, 9, 8, 7, 2, 0};
Iterable<Integer> lowNumbers=
from("n").in(numbers).
where(lt("n",5).
select("n");
System.out.println("All numbers that are less than five:")
for (Integer n: lowNumbers) {
System.out.println(n);
}
7.3 EoD SQL
描述:利用Annotation來聲明SQL
官方主頁:https://eodsql.dev.java.net/
官方示例:
public interface UserQuery extends BaseQuery {
@Select("SELECT * FROM users WHERE id = ?1")
public User getUserById(long id);
@Select("SELECT * FROM users")
public DataSet<User> getAllUsers();
@Update("UPDATE users SET user_name = ?{1.userName}, email_address = ?{1.emailAddress} " +
"dob = ?{1.dob} WHERE id = ?{1.id}")
public void updateUser(User user);
@Update(sql = "INSERT INTO users (user_name, email_address, dob) VALUES " +
"(?{1.userName}, ?{1.emailAddress}, ?{1.dob})",
keys = GeneratedKeys.RETURNED_KEYS_FIRST_COLUMN)
public User insertUser(User user);
}
程序員的一生其實可短暫了,這電腦一開一關,一天過去了,嚎;電腦一開不關,那就成服務器了,嚎……