标签:
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