SpringBoot+JPA+Freemarker 生成动态SQL

在实际开发中,有时候会遇到复杂的统计SQL,这时候Hibernate并不能很好的满足我们的需求,一方面SQL语句写在代码中不美观,可读性比较差,另一方面不方便修改SQL语句。
如果可以像mybaits一样写在XML中的话,就方便许多了。
采用Freemarker模板就可以达到将SQL写在XML文件中的功能。

一、pom引用

		
			org.springframework.boot
			spring-boot-starter-data-jpa
			
				
					org.springframework.boot
					spring-boot-starter-logging
				
			
		
		
			org.springframework.boot
			spring-boot-starter-freemarker
		
		
			org.springframework.boot
			spring-boot-starter-jdbc
		
		
			org.springframework.boot
			spring-boot-starter-log4j2
		

二、写个DTD文件,定义XML文件格式

  
  
  
  
  
                              
  
  
  
  
      
  
  
  
  
      
    

三、DTD解析器

public class DynamicStatementDTDEntityResolver implements EntityResolver {

	private static final Logger LOGGER = LoggerFactory.getLogger(DynamicStatementDTDEntityResolver.class);
	private static final String HOP_DYNAMIC_STATEMENT = "http://www.wongws.com/dtd/";

	public InputSource resolveEntity(String publicId, String systemId) {
		InputSource source = null; // returning null triggers default behavior
		if (systemId != null) {
			LOGGER.debug("trying to resolve system-id [" + systemId + "]");
			if (systemId.startsWith(HOP_DYNAMIC_STATEMENT)) {
				LOGGER.debug(
						"recognized hop dyanmic statement namespace; attempting to resolve on classpath under com/haier/openplatform/dao/hibernate/");
				source = resolveOnClassPath(publicId, systemId, HOP_DYNAMIC_STATEMENT);
			}
		}
		return source;
	}

	private InputSource resolveOnClassPath(String publicId, String systemId, String namespace) {
		InputSource source = null;
		String path = "dtd/" + systemId.substring(namespace.length());
		InputStream dtdStream = resolveInHibernateNamespace(path);
		if (dtdStream == null) {
			LOGGER.debug("unable to locate [" + systemId + "] on classpath");
			if (systemId.substring(namespace.length()).indexOf("2.0") > -1) {
				LOGGER.error("Don't use old DTDs, read the Hibernate 3.x Migration Guide!");
			}
		} else {
			LOGGER.debug("located [" + systemId + "] in classpath");
			source = new InputSource(dtdStream);
			source.setPublicId(publicId);
			source.setSystemId(systemId);
		}
		return source;
	}

	protected InputStream resolveInHibernateNamespace(String path) {
		return this.getClass().getClassLoader().getResourceAsStream(path);
	}

}

四、构造存储动态SQL的对象

public class StatementTemplate {
	public enum TYPE {

		/** hql 查询 */

		HQL,

		/** sql 查询 */

		SQL

	}

	public StatementTemplate() {

	}

	public StatementTemplate(TYPE type, Template template) {

		this.type = type;

		this.template = template;

	}

	private TYPE type;

	private Template template;

	public TYPE getType() {

		return type;

	}

	public void setType(TYPE type) {

		this.type = type;

	}

	public Template getTemplate() {

		return template;

	}

	public void setTemplate(Template template) {

		this.template = template;

	}
}

五、构造动态SQL加载器

public class DefaultDynamicHibernateStatementBuilder implements DynamicHibernateStatementBuilder, ResourceLoaderAware {
	private static final Logger LOGGER = LoggerFactory.getLogger(DefaultDynamicHibernateStatementBuilder.class);
	private Map namedHQLQueries;
	private Map namedSQLQueries;
	private String[] fileNames = new String[0];
	private ResourceLoader resourceLoader;
	private EntityResolver entityResolver = new DynamicStatementDTDEntityResolver();
	/**
	 * 查询语句名称缓存,不允许重复
	 */
	private Set nameCache = new HashSet();

	public void setFileNames(String[] fileNames) {
		this.fileNames = fileNames;
	}

	@Override
	public Map getNamedHQLQueries() {
		return namedHQLQueries;
	}

	@Override
	public Map getNamedSQLQueries() {
		return namedSQLQueries;
	}

	@Override
	public void init() throws IOException {
		namedHQLQueries = new HashMap();
		namedSQLQueries = new HashMap();
		boolean flag = this.resourceLoader instanceof ResourcePatternResolver;
		for (String file : fileNames) {
			if (flag) {
				Resource[] resources = ((ResourcePatternResolver) this.resourceLoader).getResources(file);
				buildMap(resources);
			} else {
				Resource resource = resourceLoader.getResource(file);
				buildMap(resource);
			}
		}
		// clear name cache
		nameCache.clear();
	}

	@Override
	public void setResourceLoader(ResourceLoader resourceLoader) {
		this.resourceLoader = resourceLoader;
	}

	private void buildMap(Resource[] resources)  {
		if (resources == null) {
			return;
		}
		for (Resource resource : resources) {
			buildMap(resource);
		}
	}

	@SuppressWarnings({ "rawtypes" })
	private void buildMap(Resource resource) {
		InputSource inputSource = null;
		try {
			inputSource = new InputSource(resource.getInputStream());
			XmlDocument metadataXml = readMappingDocument(entityResolver, inputSource,
					new OriginImpl("file", resource.getFilename()));
			if (isDynamicStatementXml(metadataXml)) {
				final Document doc = metadataXml.getDocumentTree();
				final Element dynamicHibernateStatement = doc.getRootElement();
				Iterator rootChildren = dynamicHibernateStatement.elementIterator();
				while (rootChildren.hasNext()) {
					final Element element = (Element) rootChildren.next();
					final String elementName = element.getName();
					if ("sql-query".equals(elementName)) {
						putStatementToCacheMap(resource, element, namedSQLQueries);
					} else if ("hql-query".equals(elementName)) {
						putStatementToCacheMap(resource, element, namedHQLQueries);
					}
				}
			}
		} catch (Exception e) {
			LOGGER.error(e.toString());
			throw new RuntimeException(e);
		} finally {
			if (inputSource != null && inputSource.getByteStream() != null) {
				try {
					inputSource.getByteStream().close();
				} catch (IOException e) {
					LOGGER.error(e.toString());
					throw new RuntimeException(e);
				}
			}
		}

	}

	private void putStatementToCacheMap(Resource resource, final Element element, Map statementMap)
			throws Exception {
		String sqlQueryName = element.attribute("name").getText();
		 Validate.notEmpty(sqlQueryName);
		
		if (nameCache.contains(sqlQueryName)) {
			throw new RuntimeException("重复的sql-query/hql-query语句定义在文件:" + resource.getURI() + "中,必须保证name的唯一.");
		}
		nameCache.add(sqlQueryName);
		String queryText = element.getText();
		statementMap.put(sqlQueryName, queryText);
	}

	private static boolean isDynamicStatementXml(XmlDocument xmlDocument) {
		return "dynamic-hibernate-statement".equals(xmlDocument.getDocumentTree().getRootElement().getName());
	}

	public XmlDocument readMappingDocument(EntityResolver entityResolver, InputSource source, Origin origin) {
		return legacyReadMappingDocument(entityResolver, source, origin);
//		return readMappingDocument( source, origin );
	}

	private XmlDocument legacyReadMappingDocument(EntityResolver entityResolver, InputSource source, Origin origin) {
		// IMPL NOTE : this is the legacy logic as pulled from the old
		// AnnotationConfiguration code

		Exception failure;

		ErrorLogger errorHandler = new ErrorLogger();

		SAXReader saxReader = new SAXReader();
		saxReader.setEntityResolver(entityResolver);
		saxReader.setErrorHandler(errorHandler);
		saxReader.setMergeAdjacentText(true);
		saxReader.setValidation(true);

		Document document = null;
		try {
			// first try with orm 2.1 xsd validation
			setValidationFor(saxReader, "orm_2_1.xsd");
			document = saxReader.read(source);
			if (errorHandler.hasErrors()) {
				throw errorHandler.getErrors().get(0);
			}
			return new XmlDocumentImpl(document, origin.getType(), origin.getName());
		} catch (Exception e) {

			LOGGER.debug("Problem parsing XML using orm 2.1 xsd, trying 2.0 xsd : {}", e.getMessage());

			failure = e;
			errorHandler.reset();

			if (document != null) {
				// next try with orm 2.0 xsd validation
				try {
					setValidationFor(saxReader, "orm_2_0.xsd");
					document = saxReader.read(new StringReader(document.asXML()));
					if (errorHandler.hasErrors()) {
						errorHandler.logErrors();
						throw errorHandler.getErrors().get(0);
					}
					return new XmlDocumentImpl(document, origin.getType(), origin.getName());
				} catch (Exception e2) {

					LOGGER.debug("Problem parsing XML using orm 2.0 xsd, trying 1.0 xsd : {}", e2.getMessage());

					errorHandler.reset();

					if (document != null) {
						// next try with orm 1.0 xsd validation
						try {
							setValidationFor(saxReader, "orm_1_0.xsd");
							document = saxReader.read(new StringReader(document.asXML()));
							if (errorHandler.hasErrors()) {
								errorHandler.logErrors();
								throw errorHandler.getErrors().get(0);
							}
							return new XmlDocumentImpl(document, origin.getType(), origin.getName());
						} catch (Exception e3) {

							LOGGER.debug("Problem parsing XML using orm 1.0 xsd : {}", e3.getMessage());

						}
					}
				}
			}
		}
		throw new InvalidMappingException("Unable to read XML", origin.getType(), origin.getName(), failure);
	}

	private void setValidationFor(SAXReader saxReader, String xsd) {
		try {
			saxReader.setFeature("http://apache.org/xml/features/validation/schema", true);
			// saxReader.setFeature( "http://apache.org/xml/features/validation/dynamic",
			// true );
			if ("orm_2_1.xsd".equals(xsd)) {
				saxReader.setProperty("http://apache.org/xml/properties/schema/external-schemaLocation",
						"http://xmlns.jcp.org/xml/ns/persistence/orm " + xsd);
			} else {
				saxReader.setProperty("http://apache.org/xml/properties/schema/external-schemaLocation",
						"http://java.sun.com/xml/ns/persistence/orm " + xsd);
			}
		} catch (SAXException e) {
			saxReader.setValidation(false);
		}
	}

}

public interface DynamicHibernateStatementBuilder {
	/**
	 * hql语句map
	 * 
	 * @return
	 */
	Map getNamedHQLQueries();

	/**
	 * sql语句map
	 * 
	 * @return
	 */
	Map getNamedSQLQueries();

	/**
	 * 初始化
	 * 
	 * @throws IOException
	 */
	void init() throws IOException;

	void setFileNames(String[] fileNames);

	void setResourceLoader(ResourceLoader resourceLoader);
}

六、利用Freemarker模板书写动态SQL

  




	  
     
            and t.password=${password} 
          
    ]]>
	
  

以上就是功能实现的关键代码,但是利用Freemarker有个问题,就是没法解决SQL注入的问题,这里我的操作就是可以使用Freemarker生成动态SQL,然后再利用JdbcTemplate或NamedParameterJdbcTemplate来执行SQL。如将上面的SQL改写成这样:

	  
     
            and t.password=:password
          
    ]]>
	

这样的话就可以绕过Freemarker的占位符使用,然后再通过NamedParameterJdbcTemplate执行SQL。
当然实际SQL不可能这么简单,肯定会遇到Map或者List等问题,这些只要网上查询Freemarker的语法,就能迎刃而解。
下面奉上源码,由于这只是个Demo,代码实在是粗制滥造了点,但是功能还是能够实现的,在com.example.demo下的DemoApplicationTests类上,运行Junit Test。当然,要是实现mysql访问,还得自己配数据库连接。

https://download.csdn.net/download/dashuaigege642/10681049

参考:

https://blog.csdn.net/crazycoder2010/article/details/7414152

你可能感兴趣的:(Java)