码迷,mamicode.com
首页 > 数据库 > 详细

基于SpringJDBC的类mybatis形式SQL语句管理的思考与实现

时间:2015-11-11 13:29:00      阅读:391      评论:0      收藏:0      [点我收藏+]

标签:

SpringJDBC为我们提供了一个非常方便的数据库访问接口,我们都知道使用JdbcTemplate对数据库进行操作时需要传入执行的SQL语句。在小型系统中,SQL语句可能并不会太多,这个时候我们无论采取什么方式进行管理都没有关系。但是当系统逐渐庞大后,我们就要考虑以一种恰当的方式对这些SQL进行管理了。我们将首先介绍比较常见的几种SQL管理方式,然后再讨论类mybatis形式的SQL管理方式。

  • 在方法中直接构造并传入

    这种方式是在需要执行数据库操作的方法内直接硬编码SQL语句。这样做的好处在于直观,我们能够很清晰的看到具体执行的SQL语句,但缺点是SQL无法复用,且维护困难。代码形式如下:

public List<User> users(Integer age) {
    String sql = "select id, username from kiiwow.user where age > ?";
    return jdbcTemplate.query(sql, age, new RowMapper<User>() {
        //解析ResultSet
    });
}
  • 在DAO层将SQL定义为常量进行管理

    这种方式在当前业务类型的DAO层内将所有需要执行的SQL语句定义为常量进行调用。这种形式相比于上一种形式的好处在于将SQL语句进行了一定程度的集中,也可以在当前DAO层进行复用,管理上也有所方便。但缺点是SQL依旧不能很方便的跨DAO层调用。代码形式如下:

//将SQL定义为常量进行调用
private final static String FIND_USERS = "select id, username from kiiwow.user where age > ?";

public List<User> users(Integer age) {
    return jdbcTemplate.query(FIND_USERS, age, new RowMapper<User>() {
        //解析ResultSet
    });
}
  • 将所有SQL放入一个不可变类中作为常量维护

    这种方式又比上一种方式更进一步,将各个DAO层中的所有SQL分别提取到专门的类中进行管理。好处在于SQL语句全部集中,复用性也较好,维护管理上也更为方便。但缺点在于SQL语句会参与类的编译,无法在运行时进行调整。

public final class UserSqlMapping {

    public static final String FIND_USERS = "select id, username from kiiwow.user where age > ?";

    public static final String DELETE_USER = "delete from kiiwow.user where id = ?";

}

通过对以上三种形式的介绍,我们可以发现,其实无论采用哪一种方式对SQL语句进行管理,SQL语句都参与了类的编译过程,最终成为字节码。这样我们就无法在运行时很方便的对SQL进行管理调整,这显然不是很优雅的SQL语句管理方式。对此,我们想到了mybatis基于xml文件的SQL语句管理模式。

mybatis对于SQL管理的原理已经有很多优秀的文章讲解了,我们便不再赘述。其大概流程是,当mybatis进行配置加载时会将配置的SQL语句解析成为一个MappedStatement对象,然后将这个MappedStatement对象放入一个Map进行管理,键的值为SQL的ID属性值。

所以,我们希望实现的功能是:我们将所有的SQL按照业务类型分别配置到不同的xml文件中,在项目启动的时候自动去解析这些xml,将所有的SQL管理到一个Map中。当我们在DAO层需要调用SQL语句执行时,只需要从这个Map中获取到SQL即可。

  • 定义我们的xml文件格式

    我们约定所有管理SQL语句的xml文件都形如以下格式,根元素为<mapper>,他有一个属性叫namespace,用于指定这份文件中所管理的SQL语句属于哪个业务模块。然后是N (N >= 0)个<sql>子元素,这些子元素用于定义具体的SQL语句,同时这些子元素也有一个属性叫name,用于指定SQL语句的名称,您可以将其看做为mybatis中定义SQL时填写的ID属性值。另外,为了避免SQL中出现一些特殊字符,我们使用CDATA包裹SQL语句。最后,我们定义所有管理SQL语句的xml文件的名称都必须符合*-sql.xml这种形式。

<?xml version="1.0" encoding="utf-8" ?>
<mapper namespace="user">
    <sql name="getUsers">
	<![CDATA[ select id, username, created_date from user ]]>
    </sql>
	
    <sql name="getUserById">
        <![CDATA[ select * from user where id = ? ]]>
    </sql>
</mapper>
  • 让项目在启动时加载我们的SQL管理文件

    为了让项目能够在启动时自动去加载我们的配置文件,我们提供一个监听器来完成这个工作。

package com.kiiwow.framework.platform.sqlmapping;

import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Properties;

import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;

import com.kiiwow.framework.util.PropertyFileUtils;

/**
 * SQL映射文件解析初始化监听器
 * 这个监听器用于将指定目录下的所有sql映射xml文件解析出来
 * 把所有SQL放入Map中管理
 *
 * @author leon.gan
 *
 */
public class SQLMappingInitListener implements ServletContextListener {

    /**
     * xml文件所在位置
     */
    private String mappingFilePath;

    public void contextInitialized(ServletContextEvent contextEvent) {
        /*
         * PropertyFileUtils是一个用于加载配置文件的工具类,您可以使用任何方式来加载配置文件
         * 我们需要拿到配置文件中path.sqlmapping所定义的管理SQL的xml文件所在位置
         */
        Properties props = PropertyFileUtils.getProperties(SQLMappingInitListener.class, "kiiwow.properties");
        mappingFilePath = props.getProperty("path.sqlmapping");
        
        //每一份xml文件都会被解析成为一个File对象
        List<File> files = new ArrayList<File>();
        //取得项目根目录
        String parentPath = contextEvent.getServletContext().getRealPath("/");
        //去除多余空格
        mappingFilePath = mappingFilePath.trim();
        //取得目录与文件名分隔符的位置(即最后一次出现分隔符的位置)
        int lastIndex = mappingFilePath.lastIndexOf("/");
        //拿到映射文件所在目录
        String path = mappingFilePath.substring(0, lastIndex);
        //拿到映射文件的名称
        String pattern = mappingFilePath.substring(lastIndex + 1);
        //将所有映射文件添加到集合
        files.addAll(Arrays.asList(getFileList(parentPath, path, pattern)));
        
        if(!files.isEmpty()) {
            //SqlMappingAnalyzer单例
            SqlMappingAnalyzer analyzer = SqlMappingAnalyzer.createAnalyzer();
            //分析映射文件
            analyzer.setSqlFiles(files);
        }
    }
    
    private File[] getFileList(String parentFile, String chileFile,
            String pattern) {
        return getFileList(new File(parentFile), chileFile, pattern);
    }
    
    /**
     * 将符合命名规范的文件过滤出来
     * 因为我们定义所有管理SQL的xml文件都必须符合*-sql.xml这种形式
     * @param parentFile
     * @param chileFile
     * @param pattern
     * @return
     */
    private File[] getFileList(File parentFile, String chileFile, String pattern) {
        File file = new File(parentFile, chileFile);
        return file.listFiles(new SQLFileFilter(pattern));
    }
    
    public void contextDestroyed(ServletContextEvent contextEvent) {
        
    }
}

        监听器将所有的文件加载成为File对象后,我们就可以使用分析器对这些文件进行分析了,将里面的SQL全部分析出来放入Map管理。我们定义最后的分析结果应该放入一个嵌套Map中,Map<String, Map<String, String>>。外层的Map是业务模块与其所有SQL语句的映射,内存的Map是具体SQL的名称与内容的映射。同时,我们需要提供一些方法对Map中的这些SQL进行管理。分析器代码如下:

package com.kiiwow.framework.platform.sqlmapping;

import java.io.File;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;

/**
 * SQL映射文件分析器
 * 将所有SQL映射文件分析成具体的SQL语句
 *
 * @author leon.gan
 *
 */
public class SqlMappingAnalyzer {

    /**
     * 单例
     */
    private static SqlMappingAnalyzer analyzer = new SqlMappingAnalyzer();
    
    /**
     * 与Web容器生命周期相同SQL存储对象
     */
    private static Map<String, Map<String, String>> map = new HashMap<String, Map<String, String>>();
    
    /**
     * 待分析的所有SQL映射文件
     */
    private List<File> sqlFiles = Collections.emptyList();
    
    private SqlMappingAnalyzer() {
        
    }
    
    public static SqlMappingAnalyzer createAnalyzer() {
        return analyzer;
    }
    
    public void setSqlFiles(List<File> sqlFiles) {
        this.sqlFiles = sqlFiles;
        analyzing();
    }
    
    /**
     * SQL映射文件分析
     */
    private void analyzing() {
        if (this.sqlFiles == null) {
            throw new NullPointerException("Can not find any SQL mapping file");
        }
        //分析映射文件
        for (File sqlFile : sqlFiles) {
            Document document = getDocument(sqlFile);
            //获取文档根节点
            Element root = document.getRootElement();
            //获取命名空间,命名空间用于区分不同模块
            String nameSpace = root.attributeValue("namespace");
            //所有sql子节点
            List<Element> sqlNodes = root.elements("sql");
            //从SQL子节点中分析出每一条SQL语句
            Map<String, String> sqls = analyzeSql(sqlNodes);
            //放入总映射
            map.put(nameSpace, sqls);
        }
    }
    
    /**
     * 分析每个SQL映射文件中的SQL语句
     * @param sqlNodes
     * @return
     */
    private Map<String, String> analyzeSql(List<Element> sqlNodes) { 
        Map<String, String> sqls = new HashMap<String, String>();
        //遍历sql节点
        for (Element sqlNode : sqlNodes) {
            //sql名称
            String sqlName = sqlNode.attributeValue("name");
            //sql语句
            String sql = sqlNode.getText();
            //添加映射
            sqls.put(sqlName, sql);
        }
        return sqls;
    }
    
    /**
     * 获取总映射
     * @return
     */
    public static Map<String, Map<String, String>> getGlobalSQLMap() {
        return map;
    }

    /**
     * 获取指定模块的SQL映射
     * @param name
     * @return
     */
    public static Map<String, String> getSpecificSQLMap(String moduleName) {
        return map.get(moduleName);
    }
    
    /**
     * 在调用映射SQL时传入的SQL名称格式为  namespace.sqlname
     * 这个方法将分析出命名空间和SQL名称,然后从映射集合中找到确定的SQL
     * @param alias
     * @return
     */
    public static String getSpecificSql(String alias) {
        String[] data = alias.split("\\.");
        return map.get(data[0]).get(data[1]);
    }
    
    /**
     * 销毁总映射
     */
    public static void destroy(){
        map.clear();
    }
    
    /**
     * 将文件转换为Document对象
     * @param file
     * @return
     */
    private Document getDocument(File file) {
        SAXReader reader = new SAXReader();
        Document document = null;
        try {
            document = reader.read(file);
        } catch (DocumentException e) {
            e.printStackTrace();
        }
        return document;
    }
    
}

        至此,我们对于管理SQL的xml文件的加载分析工作就全部结束了。在DAO层中我们就可以对这些SQL进行方便的调用了,调用示例如下:

public List<User> users(Integer age) {
    //我们只需要以"命名空间.SQL名称"的形式指定SQL即可获取SQL内容
    String sql = SqlMappingAnalyzer.getSpecificSql("user.getUsers");
    return jdbcTemplate.query(sql, age, new RowMapper<User>() {
        //解析ResultSet
    });
}

        上面的监听器代码中出现了一个SQLFileFilter类,这个其实是一个文件名过滤器,用于过滤掉不符合我们定义的管理SQL的xml文件的命名规范的文件。代码如下:

package com.kiiwow.framework.platform.sqlmapping;

import java.io.File;
import java.io.FilenameFilter;
import java.util.regex.Pattern;

/**
 * 文件名过滤器
 * 
 * @author leon.gan
 *
 */
public class SQLFileFilter implements FilenameFilter {

	private String pattern;
	
	public SQLFileFilter(){
		this.pattern = "^*[a-zA-z0-9]*-sql.xml$";
	}
	
	public SQLFileFilter(String pattern){
		this.pattern = "^" + pattern.replaceAll("\\*", "[a-zA-z0-9]*") + "$";
	}
	
	public boolean accept(File f, String name) {
		Pattern pattern = Pattern.compile(this.pattern);
		return pattern.matcher(name).matches();
	}

}

        最后,可别忘了将我们的监听器配置到web.xml文件中。

<listener>  
	<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>

        到这里,我们基于SpringJDBC的类mybatis形式SQL语句管理功能就全部实现了。采用这种方式来管理我们的SQL语句,可以避免SQL语句参与项目编译过程,当我们变更SQL语句时不需要重新编译整个项目。但是可能从上面的调用示例来看,我们依然需要去执行一次SQL获取的过程,略显麻烦。下一篇我们将介绍如何对SpringJDBC原生的JdbcTemplate进行简易封装,使其更适用于我们这种SQL管理模式。

基于SpringJDBC的类mybatis形式SQL语句管理的思考与实现

标签:

原文地址:http://my.oschina.net/devleon/blog/528839

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!