如何根据动态SQL代码自动生成DTO

当前的情况

通常作数据库相关开发, 除非学习, 不然不多有人愿意直接使用JDBC。原本Java代码就比较啰嗦了,而直接用JDBC写代码之啰嗦简直有些使人发狂!因此在实际开发过程当中,咱们一般都会使用一些框架/库来帮助咱们操做数据库。并且开源市场上的选择也比较多,就我我的接触到的有:Hibernate,MyBatis,JdbcTemplate,DbUtils,ActiveRecord,JavaLite等等。 这些框架都能大幅的提升开发效率,对于一些基本CRUD操做来讲,虽然各有差别,但总的来讲基本是够用了。java

然而对于稍微复杂点的数据查询来讲,总免不了须要手工编写SQL代码,甚至还须要根据参数来动态拼接SQL。各类框架基本上都有一套本身拼接动态SQL的方案,也都能很轻松的将查询出来的数据转为对象(DTO)。 git

不过到目前为止,这些框架虽然可以很轻松的帮助咱们完成数据的映射,可是这些DTO还得须要咱们手工一个个的去编写。程序员

存在的问题

一般咱们在写完SQL的查询代码后, 须要有一个对应的DTO,将数据库中查询出的数据映射到DTO,以便于调用的程序可以更好的使用这些数据。固然,为了省事,有时也会把数据直接存储在像Map这样的数据结构中。不过, Map这种方式虽然很轻便,可是会带来几个比重要的潜在问题:github

  • 调用者须要记住Map里面每一个key的名称,这就会给程序员带来一些所谓的记忆负担sql

  • 太重的记忆负担,就会致使系统的逻辑复杂,理解困难,维护更困难数据库

  • SQL更改致使Key发生变化后,很难发现问题,须要程序员很是当心的处理这些更改数组

若是想要避免Map带来的这些问题,咱们须要为每一个SQL查询都单独编写DTO。尽管书写这些DTO并无什么难度,可是很是枯燥乏味,特别是字段不少的时候更是如此;而且,若是SQL查询的字段出现更改,也仍是要记得回来修改这个DTO。单独编写DTO虽然减轻了Map带来的部分问题,同时也额外增长了新的工做量。数据结构

若是有一种方法可以在SQL代码(包括动态拼接的SQL)编写完成后,就自动的作到下面2点就很是完美了:框架

  1. 根据SQL代码,直接生成对应的DTOeclipse

  2. 变动SQL代码,自动修改对应的DTO

这样,一方面解决了手工书写DTO的麻烦; 另外一方面,当修改SQL致使某个字段发生更改时, 因为自动生成的DTO也会同步修改,在那些引用到这个字段的地方,编译器就会当即给出错误提示! 使得问题一产生就能当即被发现,这样能够避免了不少潜在的问题。

本文正是试图要解决如何根据SQL代码自动生成DTO的问题,省去手工编写的麻烦,提升程序员的开发效率。

解决的思路

理想老是很美好,现实老是很残酷!

那么,到底可否实现这个想法呢,咱们首先来初步分析一下自动产生DTO的可行性:

要实现自动产生DTO,其核心就是要拿到SQL查询所对应的每一个列名及其数据类型。有了列名和数据类型,就能很容易写一个方法来产生DTO了。

咱们知道,在通常状况下,SQL查询写完以后,包括调用存储过程和那些根据调用参数来动态拼接的SQL,虽然最终运行的SQL可能不尽相同,可是其查询结果的字段部分都是相对固定的。

固然,也有极少状况下会碰到字段都不肯定的查询,不过在这种极端状况下,即便手工也无法写DTO了,反却是用Map更合适, 咱们这里不作讨论。

那么,怎么才能拿到列名和类型呢?

一种方案是分析SQL代码中SELECT部分的字段,不过其局限性比较大:

  • 对于拼接的SQL代码,分析难度比较大

  • 字段的类型也难以判断

  • SELECT * ...; CALL statement 这样常见的查询方式分析起来难度也很大

上述方案对像Mybatis这种采用配置文件(xml)来写SQL的方式,彷佛有些可行性,我没有具体试验过,但估计面临的困难不会少。

另外一种方案是想办法直接运行包含SQL的这些代码:

咱们知道JDBC执行一个SQL查询,会返回ResultSet对象,经过该对象中的方法getMetaData(),可以获得此次查询的一些元数据:如列名称,列类型,以及该列所在的表名等,这些信息就已经足够咱们来产生须要的那个类了。

那么,怎么才可以运行这些包含SQL的代码呢?

对于那些固定的SQL语句还稍微好说点,咱们拿到这个固定的SQL,调用JDBC就能拿到MetaData,而后就能够很容易的根据这些信息来生成DTO。可是,对于那些复杂的须要根据一系列参数来动态产生的SQL查询,在参数设置好前是没法直接运行的,也就没法获得MetaData,得不到MetaData咱们就没法生成DTO。

怎么办?

前面已经讨论了,即使是动态SQL,不管输入什么样的参数,虽然执行的SQL语句可能不同,可是最终产生结果列倒是固定的。 咱们当前须要解决的问题不正是要获取这些列信息吗? 既然如此,那咱们就构造一系列默认的参数值。这些参数并无实际用处,仅仅是为了让咱们正在编辑SQL代码得以正常运行,以便拿到须要的MetaData,至于可否查询到数据并不紧要。

一般咱们编写的SQL代码,有2种存在形式:一是直接在Java代码中, 另一种是放在配置文件中。这里不讨论哪一种形式更好,之后我会单独再找地方来讨论。这里主要讨论的是在Java代码中拼接的SQL, 如何实现一个代码生成器来自动生成这些DTO:

要全自动化的解决这个问题,咱们先来看看这个代码生成器所要面临的一些挑战及应对的思路:

  • 如何标识一段须要生成DTO的SQL代码

首先,咱们须要标识出这段代码,以便于代码生成器能够运行这段须要生成DTO代码。而一般状况下,咱们的数据接口都是方法级别的,所以咱们能够经过对方法进行注解,用注解来标识这个方法要返回一个DTO对象是个不错的选择。

  • 如何定义DTO的类名

一种很容易想到的方法就是经过SQL代码所在的类名+方法名自动组合出一个名称, 固然有时为了灵活控制,应该容许程序员指定一个名字。

  • 如何执行代码

执行代码的关键是构造一批可以调用注解方法的合适参数。固然首先须要对注解的方法进行代码分析,提取方法参数名及类型。代码分析能够用相似JavaCC这样的工具,或者一些语法分析器,这里不作细究。下面主要探讨下默认参数的构造:

为了简化问题,默认状况下咱们能够按以下规则进行构造:

数字型参数,默认为:0, 例如:public Object find(int arg){...} 构造 int arg=0;  
字符串参数,默认为:"",     构造 String arg="";  
布尔型参数,默认为:false,  构造 boolean arg=false;  
数组型参数,默认为:类型[0], 构造 int[] arg=new int[0];  
对象型参数,默认为:new 类型(), 例如:public Object find(User arg){...} 构造 User arg=new User();

固然,对于一些简单参数的状况下,上面构造规则基本上都可以奏效。 可是,对于有些参数:好比参数是一个接口,或者是一个须要动态链接的表名,又或者是SQL拼接代码的逻辑要求参数必须是某些特殊值等等,默认构造出的参数就会致使程序没法执行。

可是,怎么才可以让咱们的代码生成器可以继续执行下去呢? 好像确实没有什么能自动处理的办法,只好把这个问题交给程序员来处理了,让程序员来帮助代码生成器完成参数的初始化。

咱们能够在注解上提供一个参数, 该参数主要完成对默认规则下没法初始化的参数进行设置。 固然,这个参数中的初始化代码也能够覆盖默认规则,以便于咱们在编辑阶段就能够测试执行不一样的SQL流程。

  • 如何生成DTO

通过以上一系列的处理,咱们终于能自动的把包含SQL查询代码的方法运行起来了。不过,如今咱们还没获得想要的MetaData,还没法生成DTO。

一种可能的方式是包装一个JDBC,截获本次方法调用时执行的SQL查询, 但面临的问题是,若是方法中有屡次查询就比较麻烦了。

另外一种方式依赖于框架的支持,能够截获到方法的return语句,获取其执行的SQL语句, 有了SQL语句,生成DTO就没有什么难度了。

  • 如何修改代码

为了尽可能减小程序员的工做,咱们的代码生成器在生成完DTO后, 还须要将方法的返回值自动修改为这个DTO类。

  • 如何处理SQL的变动

简单的作法是:一旦有某个SQL代码发生变化,就把全部的DTO都按照前面的方法从新生成一遍。 不过,很显然当查询方法不少的时候,DTO代码生成的过程将缓慢到难以忍受。

另一种更合理的作法是:咱们在生成DTO时增长一个指纹字段,其值能够用SQL代码中所包含的信息来产生,例如:代码长度+代码的hashCode.代码生成器在决定是否须要处理这个方法前,先计算该方法的指纹和存在于DTO里面的指纹进行比较,若是相同就跳过,不然就认为本方法的SQL发生了变动,须要更新DTO。

具体的实现

到此为止,基本上DTO代码生成器的主要障碍都有了相应的处理办法。最后,咱们用一个具体的实现来作个简单示例。

这里须要引入2个项目:

这是一个功能强大且很是容易使用的ORM框架,经过@DB(jdbc_url,username,password)注解来引入数据库。

这是一个相应的Eclipse插件,它能够:

  1. @DB注解的接口,在文件保存时 ,自动生成表的CRUD操做

  2. @Select注解的方法,在文件保存时 ,自动生成DTO

  3. 很轻松的书写多行字符串

插件安装和设置能够参考: https://github.com/11039850/monalisa-orm/wiki/Code-Generator

下面是一个根据动态SQL自动生成DTO示例,完整的例子工程能够参考: https://github.com/11039850/monalisa-example

package test.dao;
    
    public class UserBlogDao {
        //@Select 注解指示该方法需自动生成DTO
        //默认类名: Result + 方法名, 默认包名:数据访问类的包名+"."+数据访问类的名称(小写)
        //可选参数:name 指定生成结果类的名称,若是未指定该参数,则采用默认类名
        //可选参数:build 初始化调用参数的Java片断代码,替换默认的参数构造规则
        @Select(name="test.result.UserBlogs") 
    
        //!!! 保存后会自动修改该函数的返回值为: List -> List<UserBlogs>
        //第一次编写时,因为结果类还不存在, 为了保证可以编译正常,
        //函数的返回值 和 查询结果要用 泛值 替代, 保存后,插件会自动修改.
        //函数的返回值 和 查询结果 泛值的对应关系分三类以下:
        //1. List查询
        //public DataTable   method_name(...){... return Query.getList();   }    或
        //public List        method_name(...){... return Query.getList();   }    
        //
        //2. Page查询
        //public Page   method_name(...){... return Query.Page();      }
        //
        //3. 单条记录
        //public Object method_name(...){... return Query.getResult(); }
        //
        public List  selectUserBlogs(int user_id){ 
            Query q=TestDB.DB.createQuery();
    
            q.add(""/**~{
                SELECT a.id,a.name,b.title, b.content,b.create_time
                    FROM user a, blog b   
                    WHERE a.id=b.user_id AND a.id=?
            }*/, user_id);    
            
            return q.getList(); 
        } 
    }

上述代码保存后,插件就会自动生成一个DTO类:test.result.UserBlogs, 并自动将方法修改为以下的声明:

public List<UserBlogs>  selectUserBlogs(int user_id){ 
            ...
            return q.getList(UserBlogs.class); 
        }

固然,若是对selectUserBlogs方法作了任何的修改(包括只是加了一个空格),保存文件后,插件也会自动更新UserBlogs。

同时,为了方便咱们调试,插件也会在Eclipse的控制台窗口输出相似下面的信息:

2016-06-27 17:00:31 [I] ****** Starting generate result classes from: test.dao.UserBlogDao ******    
2016-06-27 17:00:31 [I] Create class: test.result.UserBlogs, from: [selectUserBlogs(int)]
SELECT a.id,a.name,b.title, b.content,b.create_time
    FROM user a, blog b    
    WHERE a.id=b.user_id AND a.id=0

顺便补充一下:

在Java代码中书写SQL,很是使人讨厌的一件事情就是Java语言中字符串的链接问题。使得大段的SQL代码中间要插不少的换行/转义符号,写起来很麻烦,看着也不舒服。monalisa-eclipse插件顺便也解决了多行字符串的书写问题。

例如:

System.out.println(""/**~{
        SELECT * 
            FROM user
            WHERE name="zzg"
    }*/);

将会输出:

SELECT * 
        FROM user
        WHERE name="zzg"

固然,为了快速书写,能够在Eclipse中把多行字符串的语法设置为一个代码模板。关于多行语法的更多细节能够参考: https://github.com/11039850/monalisa-orm/wiki/Multiple-line-syntax

到这里,动态SQL代码自动生成DTO的思路和实现例子基本上就介绍完了, 欢迎你们提出各类有理无理的意见,一块儿讨论、进步,谢谢!

相关文章
相关标签/搜索