Heis的Blog

保持简单,保持愚蠢
随笔 - 29, 文章 - 1, 评论 - 122, 引用 - 0
数据加载中……

我使用DSL编写SQL的一个Java实现

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(
1new 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={541398720};
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);
 
 }









程序员的一生其实可短暂了,这电脑一开一关,一天过去了,嚎;电脑一开不关,那就成服务器了,嚎……

posted on 2010-03-21 23:41 Heis 阅读(4925) 评论(2)  编辑  收藏 所属分类: 杂七杂八

评论

# re: 我使用DSL编写SQL的一个Java实现  回复  更多评论   

创意不错
2010-03-22 17:48 | 隔叶黄莺

# re: 我使用DSL编写SQL的一个Java实现  回复  更多评论   

这个正是我想找的啦。呵呵
2010-03-23 15:16 | fantasy

只有注册用户登录后才能发表评论。


网站导航: